Fix GitHub and GitLab ghost user intent creation (#303)

* Fix GitHub and GitLab ghost user intent creation, and add config

* changelog
This commit is contained in:
Will Hunt 2022-04-12 18:46:14 +01:00 committed by GitHub
parent e1b5f989b9
commit 27fc699e0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 84 additions and 34 deletions

1
changelog.d/303.bugfix Normal file
View File

@ -0,0 +1 @@
Fix GitHub / GitLab issue rooms breaking due to being unable to generate ghost users.

View File

@ -32,6 +32,10 @@ github:
showIssueRoomLink: false showIssueRoomLink: false
hotlinkIssues: hotlinkIssues:
prefix: "#" prefix: "#"
userIdPrefix:
# (Optional) Prefix used when creating ghost users for GitHub accounts.
#
_github_
gitlab: gitlab:
# (Optional) Configure this to enable GitLab support # (Optional) Configure this to enable GitLab support
# #
@ -40,6 +44,10 @@ gitlab:
url: https://gitlab.com url: https://gitlab.com
webhook: webhook:
secret: secrettoken secret: secrettoken
userIdPrefix:
# (Optional) Prefix used when creating ghost users for GitLab accounts.
#
_gitlab_
figma: figma:
# (Optional) Configure this to enable Figma support # (Optional) Configure this to enable Figma support
# #

View File

@ -417,7 +417,7 @@ export class Bridge {
); );
this.queue.on<GitHubWebhookTypes.DiscussionCreatedEvent>("github.discussion.created", async ({data}) => { this.queue.on<GitHubWebhookTypes.DiscussionCreatedEvent>("github.discussion.created", async ({data}) => {
if (!this.github) { if (!this.github || !this.config.github) {
return; return;
} }
const spaces = connManager.getConnectionsForGithubRepoDiscussion(data.repository.owner.login, data.repository.name); const spaces = connManager.getConnectionsForGithubRepoDiscussion(data.repository.owner.login, data.repository.name);
@ -439,6 +439,7 @@ export class Bridge {
this.tokenStore, this.tokenStore,
this.commentProcessor, this.commentProcessor,
this.messageClient, this.messageClient,
this.config.github,
); );
connManager.push(discussionConnection); connManager.push(discussionConnection);
} catch (ex) { } catch (ex) {
@ -1093,6 +1094,9 @@ export class Bridge {
} }
}); });
adminRoom.on("open.gitlab-issue", async (issueInfo: GetIssueOpts, res: GetIssueResponse, instanceName: string, instance: GitLabInstance) => { adminRoom.on("open.gitlab-issue", async (issueInfo: GetIssueOpts, res: GetIssueResponse, instanceName: string, instance: GitLabInstance) => {
if (!this.config.gitlab) {
return;
}
const [ connection ] = this.connectionManager?.getConnectionsForGitLabIssue(instance, issueInfo.projects, issueInfo.issue) || []; const [ connection ] = this.connectionManager?.getConnectionsForGitLabIssue(instance, issueInfo.projects, issueInfo.issue) || [];
if (connection) { if (connection) {
return this.as.botClient.inviteUser(adminRoom.userId, connection.roomId); return this.as.botClient.inviteUser(adminRoom.userId, connection.roomId);
@ -1105,7 +1109,8 @@ export class Bridge {
this.as, this.as,
this.tokenStore, this.tokenStore,
this.commentProcessor, this.commentProcessor,
this.messageClient this.messageClient,
this.config.gitlab,
); );
this.connectionManager?.push(newConnection); this.connectionManager?.push(newConnection);
return this.as.botClient.inviteUser(adminRoom.userId, newConnection.roomId); return this.as.botClient.inviteUser(adminRoom.userId, newConnection.roomId);

View File

@ -45,20 +45,21 @@ interface BridgeConfigGitHubYAML {
redirect_uri: string; redirect_uri: string;
}; };
defaultOptions?: GitHubRepoConnectionOptions; defaultOptions?: GitHubRepoConnectionOptions;
userIdPrefix?: string;
} }
export class BridgeConfigGitHub { export class BridgeConfigGitHub {
@configKey("Authentication for the GitHub App.", false) @configKey("Authentication for the GitHub App.", false)
auth: { readonly auth: {
id: number|string; id: number|string;
privateKeyFile: string; privateKeyFile: string;
}; };
@configKey("Webhook settings for the GitHub app.", false) @configKey("Webhook settings for the GitHub app.", false)
webhook: { readonly webhook: {
secret: string; secret: string;
}; };
@configKey("Settings for allowing users to sign in via OAuth.", true) @configKey("Settings for allowing users to sign in via OAuth.", true)
oauth?: { readonly oauth?: {
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
client_id: string; client_id: string;
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
@ -67,13 +68,17 @@ export class BridgeConfigGitHub {
redirect_uri: string; redirect_uri: string;
}; };
@configKey("Default options for GitHub connections.", true) @configKey("Default options for GitHub connections.", true)
defaultOptions?: GitHubRepoConnectionOptions; readonly defaultOptions?: GitHubRepoConnectionOptions;
@configKey("Prefix used when creating ghost users for GitHub accounts.", true)
readonly userIdPrefix: string;
constructor(yaml: BridgeConfigGitHubYAML) { constructor(yaml: BridgeConfigGitHubYAML) {
this.auth = yaml.auth; this.auth = yaml.auth;
this.webhook = yaml.webhook; this.webhook = yaml.webhook;
this.oauth = yaml.oauth; this.oauth = yaml.oauth;
this.defaultOptions = yaml.defaultOptions; this.defaultOptions = yaml.defaultOptions;
this.userIdPrefix = yaml.userIdPrefix || "_github_";
} }
} }
@ -155,11 +160,28 @@ export interface GitLabInstance {
// }; // };
} }
interface BridgeConfigGitLab { export interface BridgeConfigGitLabYAML {
webhook: { webhook: {
secret: string; secret: string;
}, },
instances: {[name: string]: GitLabInstance}; instances: {[name: string]: GitLabInstance};
userIdPrefix: string;
}
export class BridgeConfigGitLab {
readonly instances: {[name: string]: GitLabInstance};
readonly webhook: {
secret: string;
};
@configKey("Prefix used when creating ghost users for GitLab accounts.", true)
readonly userIdPrefix: string;
constructor(yaml: BridgeConfigGitLabYAML) {
this.instances = yaml.instances;
this.webhook = yaml.webhook;
this.userIdPrefix = yaml.userIdPrefix || "_gitlab_";
}
} }
export interface BridgeConfigFigma { export interface BridgeConfigFigma {
@ -303,7 +325,7 @@ export interface BridgeConfigRoot {
figma?: BridgeConfigFigma; figma?: BridgeConfigFigma;
generic?: BridgeGenericWebhooksConfigYAML; generic?: BridgeGenericWebhooksConfigYAML;
github?: BridgeConfigGitHub; github?: BridgeConfigGitHub;
gitlab?: BridgeConfigGitLab; gitlab?: BridgeConfigGitLabYAML;
permissions?: BridgeConfigActorPermission[]; permissions?: BridgeConfigActorPermission[];
provisioning?: BridgeConfigProvisioning; provisioning?: BridgeConfigProvisioning;
jira?: BridgeConfigJira; jira?: BridgeConfigJira;
@ -369,7 +391,7 @@ export class BridgeConfig {
if (this.github?.oauth && env["GITHUB_OAUTH_REDIRECT_URI"]) { if (this.github?.oauth && env["GITHUB_OAUTH_REDIRECT_URI"]) {
this.github.oauth.redirect_uri = env["GITHUB_OAUTH_REDIRECT_URI"]; this.github.oauth.redirect_uri = env["GITHUB_OAUTH_REDIRECT_URI"];
} }
this.gitlab = configData.gitlab; this.gitlab = configData.gitlab && new BridgeConfigGitLab(configData.gitlab);
this.figma = configData.figma; this.figma = configData.figma;
this.jira = configData.jira && new BridgeConfigJira(configData.jira); this.jira = configData.jira && new BridgeConfigJira(configData.jira);
this.generic = configData.generic && new BridgeConfigGenericWebhooks(configData.generic); this.generic = configData.generic && new BridgeConfigGenericWebhooks(configData.generic);

View File

@ -65,7 +65,8 @@ export const DefaultConfig = new BridgeConfig({
hotlinkIssues: { hotlinkIssues: {
prefix: "#" prefix: "#"
} }
} },
userIdPrefix: "_github_",
}, },
gitlab: { gitlab: {
instances: { instances: {
@ -75,7 +76,8 @@ export const DefaultConfig = new BridgeConfig({
}, },
webhook: { webhook: {
secret: "secrettoken", secret: "secrettoken",
} },
userIdPrefix: "_gitlab_",
}, },
jira: { jira: {
webhook: { webhook: {

View File

@ -149,13 +149,13 @@ export class ConnectionManager {
} }
if (GitHubDiscussionConnection.EventTypes.includes(state.type)) { if (GitHubDiscussionConnection.EventTypes.includes(state.type)) {
if (!this.github) { if (!this.github || !this.config.github) {
throw Error('GitHub is not configured'); throw Error('GitHub is not configured');
} }
this.assertStateAllowed(state, "github"); this.assertStateAllowed(state, "github");
return new GitHubDiscussionConnection( return new GitHubDiscussionConnection(
roomId, this.as, state.content, state.stateKey, this.tokenStore, this.commentProcessor, roomId, this.as, state.content, state.stateKey, this.tokenStore, this.commentProcessor,
this.messageClient, this.messageClient, this.config.github,
); );
} }
@ -171,12 +171,15 @@ export class ConnectionManager {
} }
if (GitHubIssueConnection.EventTypes.includes(state.type)) { if (GitHubIssueConnection.EventTypes.includes(state.type)) {
if (!this.github) { if (!this.github || !this.config.github) {
throw Error('GitHub is not configured'); throw Error('GitHub is not configured');
} }
this.assertStateAllowed(state, "github"); this.assertStateAllowed(state, "github");
const issue = new GitHubIssueConnection(roomId, this.as, state.content, state.stateKey || "", this.tokenStore, this.commentProcessor, this.messageClient, this.github); const issue = new GitHubIssueConnection(
roomId, this.as, state.content, state.stateKey || "", this.tokenStore,
this.commentProcessor, this.messageClient, this.github, this.config.github,
);
await issue.syncIssueState(); await issue.syncIssueState();
return issue; return issue;
} }
@ -206,7 +209,7 @@ export class ConnectionManager {
} }
if (GitLabIssueConnection.EventTypes.includes(state.type)) { if (GitLabIssueConnection.EventTypes.includes(state.type)) {
if (!this.config.gitlab) { if (!this.github || !this.config.gitlab) {
throw Error('GitLab is not configured'); throw Error('GitLab is not configured');
} }
this.assertStateAllowed(state, "gitlab"); this.assertStateAllowed(state, "gitlab");
@ -219,7 +222,9 @@ export class ConnectionManager {
this.tokenStore, this.tokenStore,
this.commentProcessor, this.commentProcessor,
this.messageClient, this.messageClient,
instance); instance,
this.config.gitlab,
);
} }
if (JiraProjectConnection.EventTypes.includes(state.type)) { if (JiraProjectConnection.EventTypes.includes(state.type)) {

View File

@ -12,6 +12,7 @@ import { DiscussionCommentCreatedEvent } from "@octokit/webhooks-types";
import { GithubGraphQLClient } from "../Github/GithubInstance"; import { GithubGraphQLClient } from "../Github/GithubInstance";
import LogWrapper from "../LogWrapper"; import LogWrapper from "../LogWrapper";
import { BaseConnection } from "./BaseConnection"; import { BaseConnection } from "./BaseConnection";
import { BridgeConfigGitHub } from "../Config/Config";
export interface GitHubDiscussionConnectionState { export interface GitHubDiscussionConnectionState {
owner: string; owner: string;
repo: string; repo: string;
@ -43,11 +44,12 @@ export class GitHubDiscussionConnection extends BaseConnection implements IConne
public static async createDiscussionRoom( public static async createDiscussionRoom(
as: Appservice, userId: string|null, owner: string, repo: string, discussion: Discussion, as: Appservice, userId: string|null, owner: string, repo: string, discussion: Discussion,
tokenStore: UserTokenStore, commentProcessor: CommentProcessor, messageClient: MessageSenderClient, tokenStore: UserTokenStore, commentProcessor: CommentProcessor, messageClient: MessageSenderClient,
config: BridgeConfigGitHub,
) { ) {
const commentIntent = await getIntentForUser({ const commentIntent = await getIntentForUser({
login: discussion.user.login, login: discussion.user.login,
avatarUrl: discussion.user.avatar_url, avatarUrl: discussion.user.avatar_url,
}, as); }, as, config.userIdPrefix);
const state: GitHubDiscussionConnectionState = { const state: GitHubDiscussionConnectionState = {
owner, owner,
repo, repo,
@ -79,16 +81,17 @@ export class GitHubDiscussionConnection extends BaseConnection implements IConne
format: 'org.matrix.custom.html', format: 'org.matrix.custom.html',
}); });
await as.botIntent.ensureJoined(roomId); await as.botIntent.ensureJoined(roomId);
return new GitHubDiscussionConnection(roomId, as, state, '', tokenStore, commentProcessor, messageClient); return new GitHubDiscussionConnection(roomId, as, state, '', tokenStore, commentProcessor, messageClient, config);
} }
constructor(roomId: string, constructor(roomId: string,
private readonly as: Appservice, private readonly as: Appservice,
private state: GitHubDiscussionConnectionState, private readonly state: GitHubDiscussionConnectionState,
stateKey: string, stateKey: string,
private tokenStore: UserTokenStore, private readonly tokenStore: UserTokenStore,
private commentProcessor: CommentProcessor, private readonly commentProcessor: CommentProcessor,
private messageClient: MessageSenderClient) { private readonly messageClient: MessageSenderClient,
private readonly config: BridgeConfigGitHub) {
super(roomId, stateKey, GitHubDiscussionConnection.CanonicalEventType); super(roomId, stateKey, GitHubDiscussionConnection.CanonicalEventType);
} }
@ -130,7 +133,7 @@ export class GitHubDiscussionConnection extends BaseConnection implements IConne
if (this.sentEvents.has(data.comment.node_id)) { if (this.sentEvents.has(data.comment.node_id)) {
return; return;
} }
const intent = await getIntentForUser(data.comment.user, this.as); const intent = await getIntentForUser(data.comment.user, this.as, this.config.userIdPrefix);
await this.messageClient.sendMatrixMessage(this.roomId, { await this.messageClient.sendMatrixMessage(this.roomId, {
body: data.comment.body, body: data.comment.body,
formatted_body: md.render(data.comment.body), formatted_body: md.render(data.comment.body),

View File

@ -13,6 +13,7 @@ import { GithubInstance } from "../Github/GithubInstance";
import { IssuesGetCommentResponseData, IssuesGetResponseData, ReposGetResponseData} from "../Github/Types"; import { IssuesGetCommentResponseData, IssuesGetResponseData, ReposGetResponseData} from "../Github/Types";
import { IssuesEditedEvent, IssueCommentCreatedEvent } from "@octokit/webhooks-types"; import { IssuesEditedEvent, IssueCommentCreatedEvent } from "@octokit/webhooks-types";
import { BaseConnection } from "./BaseConnection"; import { BaseConnection } from "./BaseConnection";
import { BridgeConfigGitHub } from "../Config/Config";
export interface GitHubIssueConnectionState { export interface GitHubIssueConnectionState {
org: string; org: string;
@ -139,7 +140,8 @@ export class GitHubIssueConnection extends BaseConnection implements IConnection
private tokenStore: UserTokenStore, private tokenStore: UserTokenStore,
private commentProcessor: CommentProcessor, private commentProcessor: CommentProcessor,
private messageClient: MessageSenderClient, private messageClient: MessageSenderClient,
private github: GithubInstance) { private github: GithubInstance,
private config: BridgeConfigGitHub,) {
super(roomId, stateKey, GitHubIssueConnection.CanonicalEventType); super(roomId, stateKey, GitHubIssueConnection.CanonicalEventType);
} }
@ -187,7 +189,7 @@ export class GitHubIssueConnection extends BaseConnection implements IConnection
const commentIntent = await getIntentForUser({ const commentIntent = await getIntentForUser({
login: comment.user.login, login: comment.user.login,
avatarUrl: comment.user.avatar_url, avatarUrl: comment.user.avatar_url,
}, this.as); }, this.as, this.config.userIdPrefix);
const matrixEvent = await this.commentProcessor.getEventBodyForGitHubComment(comment, event.repository, event.issue); const matrixEvent = await this.commentProcessor.getEventBodyForGitHubComment(comment, event.repository, event.issue);
// Comment body may be blank // Comment body may be blank
if (matrixEvent) { if (matrixEvent) {
@ -219,7 +221,7 @@ export class GitHubIssueConnection extends BaseConnection implements IConnection
// TODO: Fix // TODO: Fix
login: issue.data.user?.login as string, login: issue.data.user?.login as string,
avatarUrl: issue.data.user?.avatar_url || undefined avatarUrl: issue.data.user?.avatar_url || undefined
}, this.as); }, this.as, this.config.userIdPrefix);
// We've not sent any messages into the room yet, let's do it! // We've not sent any messages into the room yet, let's do it!
if (issue.data.body) { if (issue.data.body) {
await this.messageClient.sendMatrixMessage(this.roomId, { await this.messageClient.sendMatrixMessage(this.roomId, {

View File

@ -5,7 +5,7 @@ import { UserTokenStore } from "../UserTokenStore";
import LogWrapper from "../LogWrapper"; import LogWrapper from "../LogWrapper";
import { CommentProcessor } from "../CommentProcessor"; import { CommentProcessor } from "../CommentProcessor";
import { MessageSenderClient } from "../MatrixSender"; import { MessageSenderClient } from "../MatrixSender";
import { GitLabInstance } from "../Config/Config"; import { BridgeConfigGitLab, GitLabInstance } from "../Config/Config";
import { GetIssueResponse } from "../Gitlab/Types"; import { GetIssueResponse } from "../Gitlab/Types";
import { IGitLabWebhookNoteEvent } from "../Gitlab/WebhookTypes"; import { IGitLabWebhookNoteEvent } from "../Gitlab/WebhookTypes";
import { getIntentForUser } from "../IntentUtils"; import { getIntentForUser } from "../IntentUtils";
@ -51,7 +51,7 @@ export class GitLabIssueConnection extends BaseConnection implements IConnection
public static async createRoomForIssue(instanceName: string, instance: GitLabInstance, public static async createRoomForIssue(instanceName: string, instance: GitLabInstance,
issue: GetIssueResponse, projects: string[], as: Appservice, issue: GetIssueResponse, projects: string[], as: Appservice,
tokenStore: UserTokenStore, commentProcessor: CommentProcessor, tokenStore: UserTokenStore, commentProcessor: CommentProcessor,
messageSender: MessageSenderClient) { messageSender: MessageSenderClient, config: BridgeConfigGitLab) {
const state: GitLabIssueConnectionState = { const state: GitLabIssueConnectionState = {
projects, projects,
state: issue.state, state: issue.state,
@ -76,7 +76,7 @@ export class GitLabIssueConnection extends BaseConnection implements IConnection
], ],
}); });
return new GitLabIssueConnection(roomId, as, state, issue.web_url, tokenStore, commentProcessor, messageSender, instance); return new GitLabIssueConnection(roomId, as, state, issue.web_url, tokenStore, commentProcessor, messageSender, instance, config);
} }
public get projectPath() { public get projectPath() {
@ -94,7 +94,8 @@ export class GitLabIssueConnection extends BaseConnection implements IConnection
private tokenStore: UserTokenStore, private tokenStore: UserTokenStore,
private commentProcessor: CommentProcessor, private commentProcessor: CommentProcessor,
private messageClient: MessageSenderClient, private messageClient: MessageSenderClient,
private instance: GitLabInstance,) { private instance: GitLabInstance,
private config: BridgeConfigGitLab) {
super(roomId, stateKey, GitLabIssueConnection.CanonicalEventType); super(roomId, stateKey, GitLabIssueConnection.CanonicalEventType);
} }
@ -122,7 +123,7 @@ export class GitLabIssueConnection extends BaseConnection implements IConnection
const commentIntent = await getIntentForUser({ const commentIntent = await getIntentForUser({
login: event.user.name, login: event.user.name,
avatarUrl: event.user.avatar_url, avatarUrl: event.user.avatar_url,
}, this.as); }, this.as, this.config.userIdPrefix);
const matrixEvent = await this.commentProcessor.getEventBodyForGitLabNote(event); const matrixEvent = await this.commentProcessor.getEventBodyForGitLabNote(event);
await this.messageClient.sendMatrixMessage(this.roomId, matrixEvent, "m.room.message", commentIntent.userId); await this.messageClient.sendMatrixMessage(this.roomId, matrixEvent, "m.room.message", commentIntent.userId);

View File

@ -4,8 +4,9 @@ import axios from "axios";
const log = new LogWrapper("IntentUtils"); const log = new LogWrapper("IntentUtils");
export async function getIntentForUser(user: {avatarUrl?: string, login: string}, as: Appservice) { export async function getIntentForUser(user: {avatarUrl?: string, login: string}, as: Appservice, prefix: string) {
const intent = as.getIntentForSuffix(user.login); const domain = as.botUserId.split(":")[1];
const intent = as.getIntentForUserId(`@${prefix}${user.login}:${domain}`);
const displayName = `${user.login}`; const displayName = `${user.login}`;
// Verify up-to-date profile // Verify up-to-date profile
let profile; let profile;