diff --git a/src/Bridge.ts b/src/Bridge.ts index 9edf61d0c..b2ba4159b 100644 --- a/src/Bridge.ts +++ b/src/Bridge.ts @@ -15,6 +15,7 @@ import { MatrixError, RoomEvent, PLManager, + UserID, } from "matrix-bot-sdk"; import BotUsersManager from "./managers/BotUsersManager"; import { BridgeConfig, BridgePermissionLevel } from "./config/Config"; @@ -109,6 +110,7 @@ import { OpenProjectConnection } from "./Connections/OpenProjectConnection"; import { OAuthRequest, OAuthRequestResult } from "./tokens/Oauth"; import { IJsonType } from "matrix-bot-sdk/lib/helpers/Types"; import { GitLabInstance } from "./config/sections"; +import { ApiError, ErrCode } from "./api"; const log = new Logger("Bridge"); @@ -188,6 +190,7 @@ export class Bridge { this.config.github.auth.id, await fs.readFile(this.config.github.auth.privateKeyFile, "utf-8"), this.config.github.baseUrl, + this.tokenStore, ); await this.github.start(); } @@ -245,6 +248,58 @@ export class Bridge { Metrics.matrixAppserviceDecryptionFailed.inc(); }); + this.as.on( + "previewUrl.query", + ( + { url, userId }: { url: string; userId?: string }, + callback: (statusCode: number, data: any) => void, + ) => { + log.debug(`Got request to preview ${url} (for ${userId}`); + if (!userId) { + return callback( + 400, + new ApiError( + "user_id value must be provided", + ErrCode.ForbiddenUser, + ).jsonBody, + ); + } + + if (url.startsWith("https://github.com")) { + if (!this.github) { + return callback( + 400, + new ApiError( + "Service is not configured with GitHub", + ErrCode.DisabledFeature, + ).jsonBody, + ); + } + return this.github + .handleURLPreview(new URL(url), new UserID(userId)) + .then((response) => { + callback(200, response); + }) + .catch((ex) => { + log.warn(`Failed to preview ${url} (for ${userId})`, ex); + if (ex instanceof ApiError) { + callback(ex.statusCode, ex.jsonBody); + } else { + return callback( + 500, + new ApiError("Unknown error", ErrCode.Unknown).jsonBody, + ); + } + }); + } else { + return callback( + 404, + new ApiError("Unsupported URL", ErrCode.NotFound).jsonBody, + ); + } + }, + ); + this.queue.subscribe("response.matrix.message"); this.queue.subscribe("notifications.user.events"); this.queue.subscribe("github.*"); diff --git a/src/github/GithubInstance.ts b/src/github/GithubInstance.ts index 9d57651ba..b8a56d30f 100644 --- a/src/github/GithubInstance.ts +++ b/src/github/GithubInstance.ts @@ -11,6 +11,9 @@ import { } from "./Types"; import axios from "axios"; import UserAgent from "../UserAgent"; +import { UserID } from "matrix-bot-sdk"; +import { UserTokenStore } from "../tokens/UserTokenStore"; +import { ApiError, ErrCode } from "../api"; const log = new Logger("GithubInstance"); @@ -66,6 +69,7 @@ export class GithubInstance { private readonly appId: number | string, private readonly privateKey: string, private readonly baseUrl: URL, + private readonly tokenStore: UserTokenStore, ) { this.appId = parseInt(appId as string, 10); } @@ -283,6 +287,55 @@ export class GithubInstance { `login/oauth/${action}?${q}` ); } + + public async handleURLPreview(url: URL, userId: UserID): Promise { + // Try to get some info for the user. + // TODO: Fallback to public access? + const octokit = await this.tokenStore.getOctokitForUser(userId.toString()); + if (!octokit) { + throw new ApiError( + "User is not authenticated with GitHub", + ErrCode.ForbiddenUser, + ); + } + // Attempt to parse what the URL is about.. + const [_, owner, repo, type, number] = url.pathname.split("/"); + if (owner && repo) { + if (type === "pull") { + const pull = await octokit.pulls.get({ + owner, + repo, + pull_number: parseInt(number, 10), + }); + if (pull.status === 200) { + return { + "og:title": `${pull.data.base.repo.name} | ${pull.data.title} by @${pull.data.user.login}`, + "og:description": pull.data.body, + }; + } else { + throw new ApiError( + `Could not access pull request, status ${pull.status}`, + ErrCode.NotFound, + ); + } + } else if (type === "issues") { + throw new ApiError( + `Could not access issue request, not implemented`, + ErrCode.NotFound, + ); + } else { + throw new ApiError( + `URL not processable, unknown type ${type}`, + ErrCode.NotFound, + ); + } + } else { + throw new ApiError( + `URL not processable, must be in the format of org/repo`, + ErrCode.NotFound, + ); + } + } } export class GithubGraphQLClient {