Skip to content

Commit

Permalink
feat: Added a management bot for github comments
Browse files Browse the repository at this point in the history
Signed-off-by: Trey <[email protected]>
  • Loading branch information
TreyWW committed May 31, 2024
1 parent a726e2b commit ff79668
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/management_bot/pulumi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.pyc
venv/
12 changes: 12 additions & 0 deletions .github/management_bot/pulumi/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: myfinances
main: .
stackConfigDir: .
runtime:
name: python
options:
virtualenv: venv
description: MyFinances Management Bot infrastructure
config:
pulumi:tags:
value:
pulumi:template: aws-python
Empty file.
99 changes: 99 additions & 0 deletions .github/management_bot/pulumi/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""An AWS Python Pulumi program"""

from __future__ import annotations

import json
from contextlib import closing

import pulumi

from pulumi_aws import apigateway, iam
from pulumi_aws import lambda_

config = pulumi.Config()

site_name: str = pulumi.get_project()
account_id = config.get("accountId", ":")
region = config.get("region", "eu-west-2")
tags = {"app": site_name}

# lambda_layer
lambda_access_role = iam.Role(
"lambda_access_role",
assume_role_policy=json.dumps(
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com",
},
"Effect": "Allow",
"Sid": "",
}
],
}
),
)

lambda_layer = lambda_.LayerVersion(
"lambda_layer", code=pulumi.FileArchive(config.require("lambda_zip_path")), layer_name="PyGithub_for_myfinances_management_bot"
)

lambda_func = lambda_.Function(
"webhook_lambda",
name="myfinances_github_bot_webhooks",
role=lambda_access_role.arn,
code=pulumi.AssetArchive({".": pulumi.FileArchive("./src")}),
handler="lambda_handler.lambda_handler",
runtime=lambda_.Runtime.PYTHON3D12,
environment=lambda_.FunctionEnvironmentArgs(
variables={"app_id": config.require_secret("app_id"), "private_key": config.require_secret("private_key")}
),
layers=[lambda_layer.arn],
tags={"project": "MyFinancesBot"},
)

rest_api = apigateway.RestApi("managementApi")

# rest_api_main_resource = apigateway.Resource("main_resource", rest_api=rest_api.id, parent_id=rest_api.root_resource_id, path_part="/")

rest_api_main_resource_post = apigateway.Method(
"main_post_method_for_webhook",
rest_api=rest_api.id,
resource_id=rest_api.root_resource_id,
http_method="POST",
authorization="NONE",
api_key_required=False,
)

rest_api_lambda_integration = apigateway.Integration(
"lambda_integration",
rest_api=rest_api.id,
resource_id=rest_api.root_resource_id,
http_method="POST",
integration_http_method="POST",
type="AWS",
timeout_milliseconds=4000,
uri=lambda_func.invoke_arn,
)

api_gw_200_resp = apigateway.MethodResponse(
"200_resp", rest_api=rest_api.id, resource_id=rest_api.root_resource_id, http_method="POST", status_code="200"
)

api_gw_lambda = lambda_.Permission(
"apigw_lambda",
statement_id="AllowExecutionFromAPIGateway",
action="lambda:InvokeFunction",
function=lambda_func.name,
principal="apigateway.amazonaws.com",
source_arn=pulumi.Output.all(rest_api.id).apply(lambda id: f"arn:aws:execute-api:{region}:{account_id}:{id[0]}/*/POST/"),
)

deployment = apigateway.Deployment("deployment_resource", rest_api=rest_api.id)

prod_stage = apigateway.Stage("productionStage", stage_name="production", rest_api=rest_api.id, deployment=deployment.id)

pulumi.export("invoke_url", prod_stage.invoke_url)
2 changes: 2 additions & 0 deletions .github/management_bot/pulumi/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pulumi>=3.0.0,<4.0.0
pulumi-aws>=6.0.2,<7.0.0
101 changes: 101 additions & 0 deletions .github/management_bot/pulumi/src/lambda_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import json, os, base64

from github import Github, Issue
from github import Auth

raw_private_key = os.environ.get("private_key")
PRIVATE_KEY = base64.b64decode(raw_private_key).decode("ascii")
APP_ID = os.environ.get("app_id")


def check_if_user_perm_issue(issue: dict, sender: dict, repository: dict):
return issue.get("user", {}).get("id") == sender.get("id") or sender.get("id") == repository.get("owner", {}).get("id")


TEMPLATE_ERROR_MESSAGE = """
{body}
_Replying to @{sender}_
Invalid command, you inserted {msg_len} out of the {required} required parameters.
<details><summary>Additional Details</summary>
<p>
> Example: `/{example_cmd}`
> You may use `/help` for more commands.
</p>
</details>"""


def send_error(issue, *, body, sender, msg_len, required, example_cmd):
return issue.create_comment(
TEMPLATE_ERROR_MESSAGE.format(sender=sender, body=body, msg_len=msg_len, required=required, example_cmd=example_cmd)
)


def lambda_handler(event, context):
auth = Auth.AppAuth(APP_ID, PRIVATE_KEY).get_installation_auth(event.get("installation", {}).get("id"))
g = Github(auth=auth)

ACTION = event.get("action", {})
ISSUE = event.get("issue", {})
COMMENT = event.get("comment", {})
SENDER = event.get("sender", {})
REPOSITORY = event.get("repository", {})

repo = g.get_repo(event.get("repository", {}).get("full_name"))

if ISSUE and COMMENT and str(SENDER.get("id")) != "171095439":
if ACTION == "created": # sent comment
issue = repo.get_issue(number=ISSUE.get("number"))
msg = COMMENT.get("body", "")
msg_stripped = msg.strip().split(" ")
msg_len = len(msg_stripped)

if msg_stripped[0] == "/add_label":
if not msg_len == 2:
send_error(
issue, sender=SENDER["login"], body=COMMENT["body"], msg_len=msg_len, required=2, example_cmd="add_label bug"
)

return g.close()

issue.add_to_labels(msg_stripped[1])
issue.create_comment(f"Okay @{SENDER['login']}, I have added the label '{msg_stripped[1]}'")
elif msg_stripped[0] == "/add_labels":
issue.add_to_labels(*msg_stripped[1:])
issue.create_comment(f"Okay @{SENDER['login']}, I have added the labels \"{', '.join(msg_stripped[1:])}\"")
elif msg_stripped[0] == "/remove_label":
if not msg_len == 2:
send_error(
issue, sender=SENDER["login"], body=COMMENT["body"], msg_len=msg_len, required=2, example_cmd="remove_label bug"
)

return g.close()

issue.remove_from_labels(msg_stripped[1])
issue.create_comment(f"Okay @{SENDER['login']}, I have removed the label \"{msg_stripped[1]}\"")
elif msg_stripped[0] == "/remove_labels":
issue.remove_from_labels(*msg_stripped[1:])
issue.create_comment(f"Okay @{SENDER['login']}, I have removed the labels \"{', '.join(msg_stripped[1:])}\"")
elif msg_stripped[0] == "/help":
issue.create_comment(
f"""
Hi @{SENDER["login"]},
<details><summary>My available commands:</summary>
<p>
| Command | Description | Arg Types | Example |
|---------|-------------|--------|--------|
| /add_label | Adds one label | string | /add_label bug |
| /add_labels | Adds multiple labels | list[string] | /add_label bug enhancement |
| /remove_label | Removes one label | string | /remove_label bug |
| /remove_labels | Removes multiple labels | list[string] | /remove_labels bug enhancement |
</p>
</details>
"""
)

return {"statusCode": 200, "body": json.dumps("Success")}
24 changes: 24 additions & 0 deletions backend/management/commands/encrypt_value.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import base64

from django.core.management import BaseCommand
from django.utils.termcolors import colorize


class Command(BaseCommand):
help = "Encrypt a value"
requires_system_checks = []
requires_migrations_checks = False

def add_arguments(self, parser):
parser.add_argument("type", type=str, help="file/text", choices=["file", "text"], default="text")
parser.add_argument("value", type=str, help="your value")
# parser.add_argument("encryption", )

def handle(self, *args, **kwargs):
if kwargs["type"] == "file":
with open(kwargs["value"], "r") as file:
self.stdout.write(colorize(str(base64.b64encode(file.read().encode("ascii"))), fg="green"))
else:
self.stdout.write(colorize(str(base64.b64encode(kwargs["value"].encode("ascii"))), fg="green"))

# opts=("bold",)))
69 changes: 69 additions & 0 deletions docs/getting-started/management_bot/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Management Bot

1. In terminal, type
```
cd .github/management_bot/pulumi/
```
2. Go to [Github Apps Settings](https://github.com/settings/apps) and press `New GitHub App`
- Any app name
- Any homepage url
- No callback URL
- Set the webhook URL temporarily to anything like https://example.com
- `Create Github App`
3. Now press "edit" on the github app
4. **copy** the `App ID` field (may look like 912345)
5. Now go back to terminal, and type:
```
pulumi config set --secret app_id <your copied App ID>
```
6. Scroll down to `Private keys` and press `Generate a private key`
7. Copy the generated path of the file it generated
8. Open a new terminal and type
```
python ../../../manage.py encrypt_value file <path>

# e.g.
python ../../../manage.py encrypt_value file "E:\Downloads\myfinances-management.2024-05-31.private-key.pem"
```
- Copy the value that is inputted (not the quotation marks or the first b letter though!)
9. Now go back to terminal, and type:
```
pulumi config set --secret private_key <private_key_value>
```

!!! danger "Note"
If you have spaces in your value, you may need to manually put the value in the `Pulumi.xyz.yaml` file with `>` before
the first line.


10. Go to [Github Actions](https://github.com/TreyWW/MyFinances/actions)
- "Click on `"Lambda Zip for Management Bot"`
- Click on the latest run
- Scroll down on the "Summary" tab down to "Artifacts"
- Press on `"python-package"`
11. Unzip the zip to reveal python.zip
12. Copy the path to python.zip
13. Go back to the terminal and type

```
pulumi config set lambda_zip_path <path>
```
14. Finally you can run
```
pulumi up
```
15. Copy the `"Invoke URL"` from the output, head back over to https://github.com/settings/apps and go to your app settings
- scroll down to `"Webhook"` and paste the Invoke URL into the Webhook URL field and press `Save changes`
16. Now scroll back up and click on the `"Permissions & Events"`
17. In `"Repository Permissions"` select:
- Issues (read + write)
- Metadata (read only)
- Pull requests (read + write)
18. Scroll down to Subscribe to events and tick
- Issue Comment
19. Scroll down and press Save Changes
20. Scroll up to `Install App`, and install it to your repositories! Now you can use `/help` in issue comments

0 comments on commit ff79668

Please sign in to comment.