FEATURE: Github Webhook Configuring Page

FEATURE: Github Webhook Configuring Page

diff --git a/app/controllers/discourse_code_review/admin_code_review_controller.rb b/app/controllers/discourse_code_review/admin_code_review_controller.rb
new file mode 100644
index 0000000..e29e449
--- /dev/null
+++ b/app/controllers/discourse_code_review/admin_code_review_controller.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module DiscourseCodeReview
+  class AdminCodeReviewController < ::ApplicationController
+    def index
+    end
+  end
+end
diff --git a/app/controllers/discourse_code_review/organizations_controller.rb b/app/controllers/discourse_code_review/organizations_controller.rb
new file mode 100644
index 0000000..378dc8d
--- /dev/null
+++ b/app/controllers/discourse_code_review/organizations_controller.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module DiscourseCodeReview
+  class OrganizationsController < ::ApplicationController
+    def index
+      render_json_dump(DiscourseCodeReview.github_organizations)
+    end
+  end
+end
diff --git a/app/controllers/discourse_code_review/repos_controller.rb b/app/controllers/discourse_code_review/repos_controller.rb
new file mode 100644
index 0000000..e8ad1ce
--- /dev/null
+++ b/app/controllers/discourse_code_review/repos_controller.rb
@@ -0,0 +1,108 @@
+# frozen_string_literal: true
+
+module DiscourseCodeReview
+  class ReposController < ::ApplicationController
+    before_action :set_organization
+    before_action :set_repo, only: [:has_configured_webhook, :configure_webhook]
+
+    def index
+      render_json_dump(
+        client
+          .organization_repositories(organization)
+          .map(&:name)
+      )
+    end
+
+    def has_configured_webhook
+      hook = get_hook
+
+      has_configured_webhook = hook.present?
+      has_configured_webhook &&= hook[:events].to_set == webhook_events.to_set
+      has_configured_webhook &&= hook[:config][:url] == webhook_config[:url]
+      has_configured_webhook &&= hook[:config][:content_type] == webhook_config[:content_type]
+
+      render_json_dump(
+        has_configured_webhook: has_configured_webhook
+      )
+    end
+
+    def configure_webhook
+      hook = get_hook
+
+      if hook.present?
+        client.edit_hook(
+          full_repo_name,
+          hook[:id],
+          'web',
+          webhook_config,
+          events: webhook_events,
+          active: true
+        )
+      else
+        client.create_hook(
+          full_repo_name,
+          'web',
+          webhook_config,
+          events: webhook_events,
+          active: true
+        )
+      end
+
+      render_json_dump(
+        has_configured_webhook: true
+      )
+    end
+
+    private
+
+    attr_reader :organization
+    attr_reader :repo
+
+    def full_repo_name
+      "#{organization}/#{repo}"
+    end
+
+    def set_organization
+      @organization = params[:organization_id]
+    end
+
+    def set_repo
+      @repo = params[:id]
+    end
+
+    def client
+      DiscourseCodeReview.octokit_bot_client
+    end
+
+    def get_hook
+      client
+        .hooks(full_repo_name)
+        .select { |hook|
+          config = hook[:config]
+          url = URI.parse(config[:url])
+
+          url.hostname == Discourse.current_hostname && url.path == '/code-review/webhook'
+        }
+        .first
+    end
+
+    def webhook_config
+      {
+        url: "https://#{Discourse.current_hostname}/code-review/webhook",
+        content_type: 'json',
+        secret: SiteSetting.code_review_github_webhook_secret
+      }
+    end
+
+    def webhook_events
+      [
+        "commit_comment",
+        "issue_comment",
+        "pull_request",
+        "pull_request_review",
+        "pull_request_review_comment",
+        "push"
+      ]
+    end
+  end
+end
diff --git a/assets/javascripts/discourse/code-review-route-map.js.es6 b/assets/javascripts/discourse/code-review-route-map.js.es6
new file mode 100644
index 0000000..4a484be
--- /dev/null
+++ b/assets/javascripts/discourse/code-review-route-map.js.es6
@@ -0,0 +1,7 @@
+export default {
+  resource: "admin.adminPlugins",
+  path: "/plugins",
+  map() {
+    this.route("code-review");
+  }
+};
diff --git a/assets/javascripts/discourse/controllers/admin-plugins-code-review.js.es6 b/assets/javascripts/discourse/controllers/admin-plugins-code-review.js.es6
new file mode 100644
index 0000000..701bee2
--- /dev/null
+++ b/assets/javascripts/discourse/controllers/admin-plugins-code-review.js.es6
@@ -0,0 +1,80 @@
+import { ajax } from "discourse/lib/ajax";
+
+const prefix = "/admin/plugins/code-review";
+
+export default Ember.Controller.extend({
+  init() {
+    this._super(...arguments);
+
+    const organizations = Ember.A([]);
+    this.set("organizations", organizations);
+
+    ajax(`${prefix}/organizations.json`).then(orgNames => {
+      for (const orgName of orgNames) {
+        let organization = Ember.Object.create({
+          name: orgName,
+          repos: Ember.A([])
+        });
+        organizations.pushObject(organization);
+
+        ajax(`${prefix}/organizations/${orgName}/repos.json`).then(
+          repoNames => {
+            for (const repoName of repoNames) {
+              let repo = Ember.Object.create({
+                name: repoName,
+                hasConfiguredWebhook: null,
+                receivedWebhookState: false
+              });
+              organization.repos.pushObject(repo);
+
+              ajax(
+                `${prefix}/organizations/${orgName}/repos/${repoName}/has-configured-webhook.json`
+              ).then(response => {
+                repo.set("receivedWebhookState", true);
+                repo.set(
+                  "hasConfiguredWebhook",
+                  response["has_configured_webhook"]
+                );
+              });
+            }
+          }
+        );
+      }
+    });
+  },
+
+  actions: {
+    configureWebhook(organization, repo) {
+      if (repo.hasConfiguredWebhook === false) {
+        ajax(
+          `${prefix}/organizations/${organization.name}/repos/${repo.name}/configure-webhook.json`,
+          {
+            type: "POST"
+          }
+        ).then(response => {
+          repo.set("hasConfiguredWebhook", response["has_configured_webhook"]);
+        });
+      }
+    },
+
+    configureWebhooks() {
+      for (const organization of this.organizations) {
+        for (const repo of organization.repos) {
+          if (repo.hasConfiguredWebhook === false) {
+            ajax(
+              `${prefix}/organizations/${organization.name}/repos/${repo.name}/configure-webhook.json`,
+              {
+                type: "POST"
+              }
+            ).then(response => {
+              repo.set(
+                "hasConfiguredWebhook",
+                response["has_configured_webhook"]
+              );
+            });
+          }
+        }
+      }
+    }
+  }
+});
diff --git a/assets/javascripts/discourse/routes/admin-plugins-code-review.js.es6 b/assets/javascripts/discourse/routes/admin-plugins-code-review.js.es6
new file mode 100644
index 0000000..9805d14
--- /dev/null
+++ b/assets/javascripts/discourse/routes/admin-plugins-code-review.js.es6
@@ -0,0 +1,3 @@
+export default Discourse.Route.extend({
+  controllerName: "admin-plugins-code-review"
+});
diff --git a/assets/javascripts/discourse/templates/admin/plugins-code-review.hbs b/assets/javascripts/discourse/templates/admin/plugins-code-review.hbs
new file mode 100644
index 0000000..64b5486
--- /dev/null
+++ b/assets/javascripts/discourse/templates/admin/plugins-code-review.hbs
@@ -0,0 +1,31 @@
+<h1>{{i18n 'code_review.github_webhooks'}}</h1>
+
+{{d-button action=(action "configureWebhooks") label="code_review.configure_webhooks" class="code-review-configure-webhooks-button"}}
+
+<div class=code-review-webhook-tree>
+  {{#each this.organizations as |organization|}}
+    <div class=code-review-webhook-org>
+      <h2>{{organization.name}}</h2>
+      {{#each organization.repos as |repo|}}
+        <div class=code-review-webhook-repo>
+          <h3>{{repo.name}}</h3>

[... diff too long, it was truncated ...]

GitHub sha: 86bed904

Usually we use Admin::AdminController for extra clarity, the scope indeed captures this, but it feels a bit safer to have this in place.

Also these should all be admin only routes I assume? not moderator routes?