From 0c65acedd3d57b9cb6f44cb7a0178c0df95e7caf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Dec 2021 16:14:27 -0700 Subject: [PATCH] Partial implementation of github bridging --- .../DimensionHookshotGithubService.ts | 103 ++++++++++++++++++ src/bridges/HookshotGithubBridge.ts | 43 +++++++- src/bridges/models/hookshot.ts | 50 ++++++++- .../hookshot-github.bridge.component.html | 14 ++- .../hookshot-github.bridge.component.ts | 63 +++++++++-- .../hookshot-jira.bridge.component.ts | 7 +- web/app/shared/models/hookshot_github.ts | 19 +++- .../hookshot-github-api.service.ts | 14 ++- 8 files changed, 287 insertions(+), 26 deletions(-) create mode 100644 src/api/dimension/DimensionHookshotGithubService.ts diff --git a/src/api/dimension/DimensionHookshotGithubService.ts b/src/api/dimension/DimensionHookshotGithubService.ts new file mode 100644 index 0000000..dbf4134 --- /dev/null +++ b/src/api/dimension/DimensionHookshotGithubService.ts @@ -0,0 +1,103 @@ +import { Context, DELETE, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest"; +import { ApiError } from "../ApiError"; +import { LogService } from "matrix-bot-sdk"; +import { BridgedChannel, SlackBridge } from "../../bridges/SlackBridge"; +import { SlackChannel, SlackTeam } from "../../bridges/models/slack"; +import { ROLE_USER } from "../security/MatrixSecurity"; +import { + HookshotConnection, HookshotGithubOrg, HookshotGithubRepo, + HookshotJiraInstance, + HookshotJiraProject, + HookshotJiraRoomConfig +} from "../../bridges/models/hookshot"; +import { HookshotGithubBridge } from "../../bridges/HookshotGithubBridge"; + +interface BridgeRoomRequest { + orgId: string; + repoId: string; +} + +/** + * API for interacting with the Hookshot/Github bridge + */ +@Path("/api/v1/dimension/hookshot/github") +export class DimensionHookshotGithubService { + + @Context + private context: ServiceContext; + + @GET + @Path("auth") + @Security(ROLE_USER) + public async getAuthUrl(): Promise<{ authUrl: string }> { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotGithubBridge(userId); + const authUrl = await hookshot.getAuthUrl(); + return {authUrl}; + } catch (e) { + LogService.error("DimensionHookshotGithubService", e); + throw new ApiError(400, "Error getting auth info"); + } + } + + @GET + @Path("orgs") + @Security(ROLE_USER) + public async getOrgs(): Promise<{ orgs: HookshotGithubOrg[] }> { + const userId = this.context.request.user.userId; + + const hookshot = new HookshotGithubBridge(userId); + const userInfo = await hookshot.getLoggedInUserInfo(); + if (!userInfo.loggedIn) { + throw new ApiError(403, "Not logged in", "T2B_NOT_LOGGED_IN"); + } + return {orgs: userInfo.organisations}; + } + + @GET + @Path("org/:orgId/repos") + @Security(ROLE_USER) + public async getRepos(@PathParam("orgId") orgId: string): Promise<{ repos: HookshotGithubRepo[] }> { + const userId = this.context.request.user.userId; + + const hookshot = new HookshotGithubBridge(userId); + const repos = await hookshot.getRepos(orgId); + return {repos}; + } + + @POST + @Path("room/:roomId/connect") + @Security(ROLE_USER) + public async bridgeRoom(@PathParam("roomId") roomId: string, request: BridgeRoomRequest): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotGithubBridge(userId); + return hookshot.bridgeRoom(roomId, request.orgId, request.repoId); + } catch (e) { + LogService.error("DimensionHookshotGithubService", e); + throw new ApiError(400, "Error bridging room"); + } + } + + @DELETE + @Path("room/:roomId/connections/all") + @Security(ROLE_USER) + public async unbridgeRoom(@PathParam("roomId") roomId: string): Promise { + const userId = this.context.request.user.userId; + + try { + const hookshot = new HookshotGithubBridge(userId); + const connections = await hookshot.getRoomConfigurations(roomId); + for (const conn of connections) { + await hookshot.unbridgeRoom(roomId, conn.id); + } + return {}; // 200 OK + } catch (e) { + LogService.error("DimensionHookshotGithubService", e); + throw new ApiError(400, "Error unbridging room"); + } + } +} diff --git a/src/bridges/HookshotGithubBridge.ts b/src/bridges/HookshotGithubBridge.ts index 8ce8d4f..5d697c9 100644 --- a/src/bridges/HookshotGithubBridge.ts +++ b/src/bridges/HookshotGithubBridge.ts @@ -1,5 +1,11 @@ import HookshotGithubBridgeRecord from "../db/models/HookshotGithubBridgeRecord"; -import { HookshotConnection, HookshotGithubRoomConfig, HookshotTypes } from "./models/hookshot"; +import { + HookshotConnection, HookshotGithubRepo, + HookshotGithubRoomConfig, + HookshotGithubUserInfo, + HookshotJiraUserInfo, + HookshotTypes +} from "./models/hookshot"; import { HookshotBridge } from "./HookshotBridge"; export class HookshotGithubBridge extends HookshotBridge { @@ -16,6 +22,11 @@ export class HookshotGithubBridge extends HookshotBridge { return bridges[0]; } + public async getAuthUrl(): Promise { + const bridge = await this.getDefaultBridge(); + return this.doProvisionRequest(bridge, "GET", `/v1/github/oauth`).then(r => r['url']); + } + public async getBotUserId(): Promise { const confs = await this.getAllServiceInformation(); const conf = confs.find(c => c.eventType === HookshotTypes.Github); @@ -27,14 +38,40 @@ export class HookshotGithubBridge extends HookshotBridge { return !!bridges && bridges.length > 0 && !!(await this.getBotUserId()); } + public async getLoggedInUserInfo(): Promise { + const bridge = await this.getDefaultBridge(); + return this.doProvisionRequest(bridge, "GET", `/v1/github/account`); + } + + public async getRepos(orgId: string): Promise { + const bridge = await this.getDefaultBridge(); + const results: HookshotGithubRepo[] = []; + let more = true; + let page = 1; + let perPage = 10; + do { + const res = await this.doProvisionRequest(bridge, "GET", `/v1/github/orgs/${orgId}/repositories`, { + page, + perPage, + }); + results.push(...res); + if (res.length < perPage) more = false; + } while(more); + return results; + } + public async getRoomConfigurations(inRoomId: string): Promise { return (await this.getAllRoomConfigurations(inRoomId)).filter(c => c.eventType === HookshotTypes.Github); } - public async bridgeRoom(roomId: string): Promise { + public async bridgeRoom(roomId: string, orgId: string, repoId: string): Promise { const bridge = await this.getDefaultBridge(); - const body = {}; + const body = { + commandPrefix: "!github", + org: orgId, + repo: repoId, + }; return await this.doProvisionRequest(bridge, "PUT", `/v1/${roomId}/connections/${HookshotTypes.Github}`, null, body); } diff --git a/src/bridges/models/hookshot.ts b/src/bridges/models/hookshot.ts index 205463d..0d42c99 100644 --- a/src/bridges/models/hookshot.ts +++ b/src/bridges/models/hookshot.ts @@ -1,3 +1,8 @@ +export enum HookshotTypes { + Github = "uk.half-shot.matrix-hookshot.github.repository", + Jira = "uk.half-shot.matrix-hookshot.jira.project", +} + export interface HookshotConnection { type: string; eventType: string; // state key in the connection @@ -16,12 +21,46 @@ export interface HookshotConnectionTypeDefinition { botUserId: string; } -export interface HookshotGithubRoomConfig { - +export interface HookshotGithubRoomConfig extends HookshotConnection { + config: { + org: string; + repo: string; + ignoreHooks: SupportedGithubRepoEventType[]; + commandPrefix: string; + }; } -export enum SupportedJiraEventType { +export interface HookshotGithubOrg { + name: string; + avatarUrl: string; +} + +export interface HookshotGithubRepo { + name: string; + owner: string; + fullName: string; + avatarUrl: string; + description: string; +} + +export interface HookshotGithubUserInfo { + loggedIn: boolean; + organisations?: HookshotGithubOrg[]; +} + +export enum SupportedGithubRepoEventType { IssueCreated = "issue.created", + IssueChanged = "issue.changed", + IssueEdited = "issue.edited", + Issue = "issue", + PROpened = "pull_request.opened", + PRClosed = "pull_request.closed", + PRMerged = "pull_request.merged", + PRReadyForReview = "pull_request.ready_for_review", + PRReviewed = "pull_request.reviewed", + PR = "pull_request", + ReleaseCreated = "release.created", + Release = "release", } export interface HookshotJiraRoomConfig extends HookshotConnection { @@ -32,9 +71,8 @@ export interface HookshotJiraRoomConfig extends HookshotConnection { }; } -export enum HookshotTypes { - Github = "uk.half-shot.matrix-hookshot.github.repository", - Jira = "uk.half-shot.matrix-hookshot.jira.project", +export enum SupportedJiraEventType { + IssueCreated = "issue.created", } export interface HookshotJiraUserInfo { diff --git a/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.html b/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.html index 2fcfa2b..d61f010 100644 --- a/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.html +++ b/web/app/configs/bridge/hookshot-github/hookshot-github.bridge.component.html @@ -8,15 +8,21 @@
-
+
+

{{'This room is bridged to' | translate}} {{bridgedRepoSlug}}

+ +
+

{{'In order to bridge to Github, you\'ll need to authorize the bridge to access your organization(s). Please click the button below to do so.' | translate}}

- - sign in with slack + + {{'Sign in with GitHub' | translate}}
-
+