diff --git a/bridge/__init__.py b/bridge/__init__.py index 682f57d..00fdc66 100644 --- a/bridge/__init__.py +++ b/bridge/__init__.py @@ -4,7 +4,9 @@ from flask import redirect from flask import abort import requests -import base64 + +from .webgit import Gitea, Github +from .utils import issue_sentinel, generate_sentinel app = Flask(__name__) app.config.from_envvar('GIT_BRIDGE_SETTINGS') @@ -15,6 +17,19 @@ def index(): @app.route("/bridge/endpoints/gitea/repo", methods=["POST"]) def gitea_handle_repo_action(): + """ + Our plan of action for handling these events: + - ignore deleting repositories -- this is a potentially destructive operation + that *only* *the* *user* *should* *do* + - create a cooresponding repo on github + - create a push mirror to github so pushes to gitea get to github + - create webhooks for issues (and in the future, pull requests) on both + Gitea and Github ends + """ + + gitea = Gitea(app.config["GITEA_ACCESS_TOKEN"]) + github = Github(app.config["GITHUB_ACCESS_TOKEN"]) + data = request.json try: @@ -29,20 +44,10 @@ def gitea_handle_repo_action(): except KeyError: abort(400) # the data isn't formatted correctly - """ - Our plan of action for handling these events: - - ignore deleting repositories -- this is a potentially destructive operation - that *only* *the* *user* *should* *do* - - create a cooresponding repo on github - - create a push mirror to github so pushes to gitea get to github - - create webhooks for issues (and in the future, pull requests) on both - Gitea and Github ends - """ - if not repo_action == "created": return '' - github_created_repo_result = requests.post( + github_created_repo_result = github.post( "https://api.github.com/user/repos", json={ "name": repo_name, @@ -53,21 +58,11 @@ def gitea_handle_repo_action(): repo_name, ), }, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": "token " + app.config["GITHUB_ACCESS_TOKEN"], - "X-GitHub-Api-Version": "2022-11-28", - }, ) - try: - github_created_repo_result.raise_for_status() - except requests.exceptions.HTTPError: - abort(500) - new_github_repo_url = github_created_repo_result.json()["html_url"] - gitea_add_github_repo_url_result = requests.patch( + gitea_add_github_repo_url_result = gitea.patch( "https://{}/api/v1/repos/{}/{}".format( app.config["GITEA_INSTANCE_DOMAIN"], repo_owner, @@ -76,17 +71,9 @@ def gitea_handle_repo_action(): json={ "website": new_github_repo_url, }, - headers={ - "Authorization": "token " + app.config["GITEA_ACCESS_TOKEN"], - }, ) - try: - gitea_add_github_repo_url_result.raise_for_status() - except requests.exceptions.HTTPError: - abort(500) - - gitea_push_target_result = requests.post( + gitea_push_target_result = gitea.post( "https://{}/api/v1/repos/{}/{}/push_mirrors".format( app.config["GITEA_INSTANCE_DOMAIN"], repo_owner, @@ -99,33 +86,17 @@ def gitea_handle_repo_action(): "remote_username": repo_owner, "sync_on_commit": True, }, - headers={ - "Authorization": "token " + app.config["GITEA_ACCESS_TOKEN"], - }, ) - try: - gitea_push_target_result.raise_for_status() - except requests.exceptions.HTTPError: - abort(500) - - gitea_force_target_push = requests.post( + gitea_force_target_push = gitea.post( "https://{}/api/v1/repos/{}/{}/push_mirrors-sync".format( app.config["GITEA_INSTANCE_DOMAIN"], repo_owner, repo_name ), - headers={ - "Authorization": "token " + app.config["GITEA_ACCESS_TOKEN"], - }, ) - try: - gitea_force_target_push.raise_for_status() - except requests.exceptions.HTTPError: - abort(500) - - github_create_webhook_result = requests.post( + github_create_webhook_result = github.post( "https://api.github.com/repos/{}/{}/hooks".format( repo_owner, repo_name, @@ -142,19 +113,9 @@ def gitea_handle_repo_action(): "issues", "issue_comment", ], }, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": "token " + app.config["GITHUB_ACCESS_TOKEN"], - "X-GitHub-Api-Version": "2022-11-28", - }, ) - try: - github_create_webhook_result.raise_for_status() - except requests.exceptions.HTTPError: - abort(500) - - gitea_create_webhook_result = requests.post( + gitea_create_webhook_result = gitea.post( "https://{}/api/v1/repos/{}/{}/hooks".format( app.config["GITEA_INSTANCE_DOMAIN"], repo_owner, @@ -174,20 +135,31 @@ def gitea_handle_repo_action(): "issues", "issue_comment", ], }, - headers={ - "Authorization": "token " + app.config["GITEA_ACCESS_TOKEN"], - }, ) - - try: - gitea_create_webhook_result.raise_for_status() - except requests.exceptions.HTTPError: - abort(500) return '' @app.route("/bridge/endpoints/gitea/issue", methods=["POST"]) def gitea_handle_issue_action(): + """ + firstly, check if the sentinal is in the issue body + - if it is, then stop processing the event + (we don't want infinite loops) + if we've opened an issue: + - create a new one on the Github side + - make it relate to the Gitea one + - make the Gitea one related to the Github one + + if we've commented: + - add a comment to the cooresponding Github issue + + if we've closed: + - add a cooresponding comment and close the Github issue + """ + + gitea = Gitea(app.config["GITEA_ACCESS_TOKEN"]) + github = Github(app.config["GITHUB_ACCESS_TOKEN"]) + data = request.json try: @@ -218,22 +190,6 @@ def gitea_handle_issue_action(): print(e, type(e)) abort(400) # the data isn't formatted correctly - """ - firstly, check if the sentinal is in the issue body - - if it is, then stop processing the event - (we don't want infinite loops) - if we've opened an issue: - - create a new one on the Github side - - make it relate to the Gitea one - - make the Gitea one related to the Github one - - if we've commented: - - add a comment to the cooresponding Github issue - - if we've closed: - - add a cooresponding comment and close the Github issue - """ - if "GITEA_GITHUB_ISSUE_SYNC_SENTINAL" in event_body: return '' @@ -248,9 +204,9 @@ def gitea_handle_issue_action():
Internal issue metadata - GITEA_GITHUB_ISSUE_SYNC_SENTINAL {} + {}
- """.format(base64.b64encode(event_url.encode("utf-8")).decode("utf-8")) + """.format(generate_sentinel(event_url)) issue_body = "\n\n".join([ issue_header, @@ -258,7 +214,7 @@ def gitea_handle_issue_action(): issue_footer ]) - github_create_issue_result = requests.post( + github_create_issue_result = github.post( "https://api.github.com/repos/{}/{}/issues".format( repo_owner, repo_name, @@ -267,18 +223,8 @@ def gitea_handle_issue_action(): "title": event_title, "body": issue_body, }, - headers={ - "Accept": "application/vnd.github+json", - "Authorization": "token " + app.config["GITHUB_ACCESS_TOKEN"], - "X-GitHub-Api-Version": "2022-11-28", - }, ) - try: - github_create_issue_result.raise_for_status() - except requests.exceptions.HTTPError: - abort(500) - returned_data = github_create_issue_result.json() issue_comment_body = """ *This issue is being mirrored on Github [here]({}).* @@ -286,14 +232,14 @@ def gitea_handle_issue_action():
Internal issue metadata - GITEA_GITHUB_ISSUE_SYNC_SENTINEL {} + {}
""".format( returned_data["html_url"], - base64.b64encode(returned_data["url"].encode("utf-8")).decode("utf-8") + generate_sentinel(returned_data["url"]) ) - gitea_issue_comment_result = requests.post( + gitea_issue_comment_result = gitea.post( "https://{}/api/v1/repos/{}/{}/issues/{}/comments".format( app.config["GITEA_INSTANCE_DOMAIN"], repo_owner, @@ -303,14 +249,6 @@ def gitea_handle_issue_action(): json={ "body": issue_comment_body, }, - headers={ - "Authorization": "token " + app.config["GITEA_ACCESS_TOKEN"], - }, ) - try: - gitea_issue_comment_result.raise_for_status() - except requests.exceptions.HTTPError: - abort(500) - - return '' + return '' \ No newline at end of file diff --git a/bridge/utils.py b/bridge/utils.py new file mode 100644 index 0000000..d37761f --- /dev/null +++ b/bridge/utils.py @@ -0,0 +1,16 @@ +import base64 + +def to_base64(s: str) -> str: + return base64.b64encode( + s.encode("utf-8") + ).decode("utf-8") + +def from_base64(s: str) -> str: + return base64.b64decode( + s.encode("utf-8") + ).decode("utf-8") + +issue_sentinel = "GITEA_GITHUB_ISSUE_SYNC_SENTINEL" + +def generate_sentinel(s: url) -> str: + return ' '.join([sentinel, to_base64(url)]) \ No newline at end of file diff --git a/bridge/webgit.py b/bridge/webgit.py new file mode 100644 index 0000000..3b6a099 --- /dev/null +++ b/bridge/webgit.py @@ -0,0 +1,65 @@ +from typing import Any +from dataclasses import dataclass, field + +import requests +from requests import Request, Session +from requests.exceptions import HTTPError + +from flask import abort + +@dataclass +class WebgitClient: + """ + A quite thin wrapper around various code forges' REST APIs. + Designed to be subclassed. + """ + api_token: str + headers: Any = field(init=False) + + def _post_request(self, request_obj): + try: + request_obj.raise_for_status() + except HTTPError as e: + print("An exception occured: {}({})".format( + type(e).__name__, e + )) + abort(500) + + def _request_wrapper(self, method, *args, **kwargs): + s = Session() + r = Request(method, *args, **kwargs, headers=self.headers) + prepped = s.prepare_request(r) + settings = s.merge_environment_settings(prepped.url, {}, None, None, None) + r = s.send(prepped, **settings) + + self._post_request(r) + return r + + + def post(self, *args, **kwargs): + return self._request_wrapper("POST", *args, **kwargs) + + def patch(self, *args, **kwargs): + return self._request_wrapper("PATCH", *args, **kwargs) + +@dataclass +class Github(WebgitClient): + """ + A quite thin wrapper around Github's REST API. + """ + def __post_init__(self): + self.headers = { + "Accept": "application/vnd.github+json", + "Authorization": "token " + self.api_token, + "X-GitHub-Api-Version": "2022-11-28", + } + +@dataclass +class Gitea(WebgitClient): + """ + A quite thin wrapper around Gitea's REST API. + """ + def __post_init__(self): + self.headers = { + "Authorization": "token " + self.api_token, + }