]> git.lizzy.rs Git - cheatdb.git/commitdiff
Add automatic GitHub webhook creation
authorrubenwardy <rw@rubenwardy.com>
Fri, 24 Jan 2020 23:19:06 +0000 (23:19 +0000)
committerrubenwardy <rw@rubenwardy.com>
Fri, 24 Jan 2020 23:19:06 +0000 (23:19 +0000)
app/blueprints/github/__init__.py
app/models.py
app/templates/base.html
app/templates/github/setup_webhook.html [new file with mode: 0644]
app/templates/packages/view.html
migrations/versions/7a48dbd05780_.py [new file with mode: 0644]

index 6bf63a78c0c8abfaa18efb97daab083287ce6c17..d05dffce3e30baca23b028e4e1cdaa7aedea7e11 100644 (file)
@@ -18,19 +18,22 @@ from flask import Blueprint
 
 bp = Blueprint("github", __name__)
 
-from flask import redirect, url_for, request, flash, abort
-from flask_user import current_user
+from flask import redirect, url_for, request, flash, abort, render_template, jsonify
+from flask_user import current_user, login_required
 from sqlalchemy import func
 from flask_github import GitHub
 from app import github, csrf
 from app.models import db, User, APIToken, Package
-from app.utils import loginUser
+from app.utils import loginUser, randomString
 from app.blueprints.api.support import error, handleCreateRelease
-import hmac
+import hmac, requests, json
+
+from flask_wtf import FlaskForm
+from wtforms import SelectField, SubmitField
 
 @bp.route("/github/start/")
 def start():
-       return github.authorize("")
+       return github.authorize("", redirect_uri=url_for("github.callback"))
 
 @bp.route("/github/callback/")
 @github.authorized_handler
@@ -40,8 +43,6 @@ def callback(oauth_token):
                flash("Authorization failed [err=gh-oauth-login-failed]", "danger")
                return redirect(url_for("user.login"))
 
-       import requests
-
        # Get Github username
        url = "https://api.github.com/user"
        r = requests.get(url, headers={"Authorization": "token " + oauth_token})
@@ -121,11 +122,100 @@ def webhook():
        if event == "push":
                title = json["head_commit"]["message"].partition("\n")[0]
                ref = json["after"]
+       elif event == "ping":
+               return jsonify({ "success": True, "message": "Ping successful" })
        else:
-               return error(400, "Unknown event, expected 'push'")
+               return error(400, "Unsupported event. Only 'push' and 'ping' are supported.")
 
        #
        # Perform release
        #
 
        return handleCreateRelease(actual_token, package, title, ref)
+
+
+class SetupWebhookForm(FlaskForm):
+       event   = SelectField("Event Type", choices=[('push', 'Push'), ('tag', 'New tag')])
+       submit  = SubmitField("Save")
+
+
+@bp.route("/github/callback/webhook/")
+@github.authorized_handler
+def callback_webhook(oauth_token=None):
+       pid = request.args.get("pid")
+       if pid is None:
+               abort(404)
+
+       current_user.github_access_token = oauth_token
+       db.session.commit()
+
+       return redirect(url_for("github.setup_webhook", pid=pid))
+
+
+@bp.route("/github/webhook/new/", methods=["GET", "POST"])
+@login_required
+def setup_webhook():
+       pid = request.args.get("pid")
+       if pid is None:
+               abort(404)
+
+       package = Package.query.get(pid)
+       if package is None:
+               abort(404)
+
+       gh_user, gh_repo = package.getGitHubFullName()
+       if gh_user is None or gh_repo is None:
+               flash("Unable to get Github full name from repo address", "danger")
+               return redirect(package.getDetailsURL())
+
+       if current_user.github_access_token is None:
+               return github.authorize("write:repo_hook", \
+                       redirect_uri=url_for("github.callback_webhook", pid=pid, _external=True))
+
+       form = SetupWebhookForm(formdata=request.form)
+       if request.method == "POST" and form.validate():
+               token = APIToken()
+               token.name = "Github Webhook for " + package.title
+               token.owner = current_user
+               token.access_token = randomString(32)
+               token.package = package
+
+               event = form.event.data
+               if event != "push" and event != "tag":
+                       abort(500)
+
+               # Create webhook
+               url = "https://api.github.com/repos/{}/{}/hooks".format(gh_user, gh_repo)
+               data = {
+                       "name": "web",
+                       "active": True,
+                       "events": [event],
+                       "config": {
+                               "url": url_for("github.webhook", _external=True),
+                               "content_type": "json",
+                               "secret": token.access_token
+                       },
+               }
+
+               headers = {
+                       "Authorization": "token " + current_user.github_access_token
+               }
+
+               r = requests.post(url, headers=headers, data=json.dumps(data))
+               if r.status_code == 201:
+                       db.session.add(token)
+                       db.session.commit()
+
+                       return redirect(package.getDetailsURL())
+               elif r.status_code == 403:
+                       current_user.github_access_token = None
+                       db.session.commit()
+
+                       return github.authorize("write:repo_hook", \
+                               redirect_uri=url_for("github.callback_webhook", pid=pid, _external=True))
+               else:
+                       flash("Failed to create webhook, received response from Github: " +
+                               str(r.json().get("message") or r.status_code), "danger")
+
+       return render_template("github/setup_webhook.html", \
+               form=form, package=package)
index 5eff2ddf41273b1e3dc8c1dc0be7e745802a8b9b..86136f289fa164524dbdb927978251a5a3be74da 100644 (file)
@@ -126,6 +126,9 @@ class User(db.Model, UserMixin):
        github_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
        forums_username = db.Column(db.String(50, collation="NOCASE"), nullable=True, unique=True)
 
+       # Access token for webhook setup
+       github_access_token = db.Column(db.String(50), nullable=True, server_default=None)
+
        # User email information
        email         = db.Column(db.String(255), nullable=True, unique=True)
        email_confirmed_at  = db.Column(db.DateTime())
@@ -461,6 +464,31 @@ class Package(db.Model):
        def getIsFOSS(self):
                return self.license.is_foss and self.media_license.is_foss
 
+       def getIsOnGitHub(self):
+               if self.repo is None:
+                       return False
+
+               url = urlparse(self.repo)
+               return url.netloc == "github.com"
+
+       def getGitHubFullName(self):
+               if self.repo is None:
+                       return None
+
+               url = urlparse(self.repo)
+               if url.netloc != "github.com":
+                       return None
+
+               import re
+               m = re.search(r"^\/([^\/]+)\/([^\/]+)\/?$", url.path)
+               if m is None:
+                       return
+
+               user = m.group(1)
+               repo = m.group(2).replace(".git", "")
+
+               return (user,repo)
+
        def getSortedDependencies(self, is_hard=None):
                query = self.dependencies
                if is_hard is not None:
index cfe2dfc7288330763c0cbbae3c1541879629cba9..4db84d74528325d6b66cf9335de3ee0678e7fa7f 100644 (file)
                                                <li class="alert alert-{{category}} container">
                                                        <span class="icon_message"></span>
 
-                                                       {{ message|safe }}
+                                                       {{ message }}
 
                                                        <div style="clear: both;"></div>
                                                </li>
diff --git a/app/templates/github/setup_webhook.html b/app/templates/github/setup_webhook.html
new file mode 100644 (file)
index 0000000..d0012e2
--- /dev/null
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+
+{% block title %}
+       {{ _("Setup GitHub webhook") }}
+{% endblock %}
+
+{% from "macros/forms.html" import render_field, render_submit_field, render_radio_field %}
+
+{% block content %}
+       <h1 class="mt-0">{{ self.title() }}</h1>
+
+       <div class="alert alert-info">
+               {{ _("You can delete the webhook at any time by going into Settings > Webhooks on the repository.") }}
+       </div>
+
+       <form method="POST" action="" enctype="multipart/form-data">
+               {{ form.hidden_tag() }}
+
+               {{ render_field(form.event) }}
+
+               {{ render_submit_field(form.submit) }}
+       </form>
+{% endblock %}
index 77b84da21aef690e076a624062b1106ab0d01867..e5ab1e4198072f6c27e6f66097b859dde0954f66 100644 (file)
                                </ul>
                        </div>
 
+                       {% if package.getIsOnGitHub() %}
+                       <p class="small text-centered">
+                               <a href="{{ url_for('github.setup_webhook', pid=package.id) }}">
+                                       Set up a webhook
+                               </a>
+                               to create releases automatically.
+                       </p>
+                       {% endif %}
+
                        <div class="card my-4">
                                <div class="card-header">
                                        {% if package.approved and package.checkPerm(current_user, "CREATE_THREAD") %}
diff --git a/migrations/versions/7a48dbd05780_.py b/migrations/versions/7a48dbd05780_.py
new file mode 100644 (file)
index 0000000..8f3b96e
--- /dev/null
@@ -0,0 +1,24 @@
+"""empty message
+
+Revision ID: 7a48dbd05780
+Revises: df66c78e6791
+Create Date: 2020-01-24 21:52:49.744404
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = '7a48dbd05780'
+down_revision = 'df66c78e6791'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.add_column('user', sa.Column('github_access_token', sa.String(length=50), nullable=True, server_default=None))
+
+
+def downgrade():
+    op.drop_column('user', 'github_access_token')