mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 13:17:08 +00:00
More fiddling
This commit is contained in:
parent
80f4795c82
commit
fefe9da5fe
@ -10,6 +10,7 @@
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"prepare": "yarn build",
|
||||
"start": "node lib/App/BridgeApp.js",
|
||||
"start:app": "node lib/App/BridgeApp.js",
|
||||
"start:webhooks": "node lib/App/GithubWebhookApp.js",
|
||||
"start:matrixsender": "node lib/App/MatrixSenderApp.js",
|
||||
|
176
src/AdminRoom.ts
176
src/AdminRoom.ts
@ -27,10 +27,20 @@ export const BRIDGE_GITLAB_NOTIF_TYPE = "uk.half-shot.matrix-github.gitlab.notif
|
||||
export interface AdminAccountData {
|
||||
// eslint-disable-next-line camelcase
|
||||
admin_user: string;
|
||||
notifications?: {
|
||||
enabled: boolean;
|
||||
participating?: boolean;
|
||||
github?: {
|
||||
notifications?: {
|
||||
enabled: boolean;
|
||||
participating?: boolean;
|
||||
};
|
||||
};
|
||||
gitlab?: {
|
||||
[instanceUrl: string]: {
|
||||
notifications: {
|
||||
enabled: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
export class AdminRoom extends EventEmitter {
|
||||
public static helpMessage: MatrixMessageContent;
|
||||
@ -54,19 +64,40 @@ export class AdminRoom extends EventEmitter {
|
||||
return this.pendingOAuthState;
|
||||
}
|
||||
|
||||
public get notificationsEnabled() {
|
||||
return !!this.data.notifications?.enabled;
|
||||
public notificationsEnabled(type: "github"|"gitlab", instanceName?: string) {
|
||||
if (type === "github") {
|
||||
return this.data.github?.notifications?.enabled;
|
||||
}
|
||||
return (type === "gitlab" &&
|
||||
!!instanceName &&
|
||||
this.data.gitlab &&
|
||||
this.data.gitlab[instanceName].notifications.enabled
|
||||
);
|
||||
}
|
||||
|
||||
public get notificationsParticipating() {
|
||||
return !!this.data.notifications?.participating;
|
||||
public notificationsParticipating(type: string) {
|
||||
if (type !== "github") {
|
||||
return false;
|
||||
}
|
||||
return this.data.github?.notifications?.participating || false;
|
||||
}
|
||||
|
||||
public clearOauthState() {
|
||||
this.pendingOAuthState = null;
|
||||
}
|
||||
|
||||
public async getNotifSince() {
|
||||
public async getNotifSince(type: "github"|"gitlab", instanceName?: string) {
|
||||
if (type === "gitlab") {
|
||||
try {
|
||||
const { since } = await this.botIntent.underlyingClient.getRoomAccountData(
|
||||
`${BRIDGE_GITLAB_NOTIF_TYPE}:${instanceName}`, this.roomId
|
||||
);
|
||||
return since;
|
||||
} catch {
|
||||
// TODO: We should look at this error.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const { since } = await this.botIntent.underlyingClient.getRoomAccountData(BRIDGE_NOTIF_TYPE, this.roomId);
|
||||
return since;
|
||||
@ -76,7 +107,14 @@ export class AdminRoom extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
public async setNotifSince(since: number) {
|
||||
public async setNotifSince(type: "github"|"gitlab", since: number, instanceName?: string) {
|
||||
if (type === "gitlab") {
|
||||
return this.botIntent.underlyingClient.setRoomAccountData(
|
||||
`${BRIDGE_GITLAB_NOTIF_TYPE}:${instanceName}`,
|
||||
this.roomId, {
|
||||
since,
|
||||
});
|
||||
}
|
||||
return this.botIntent.underlyingClient.setRoomAccountData(BRIDGE_NOTIF_TYPE, this.roomId, {
|
||||
since,
|
||||
});
|
||||
@ -143,33 +181,38 @@ export class AdminRoom extends EventEmitter {
|
||||
@botCommand("github notifications toggle", "Toggle enabling/disabling GitHub notifications in this room")
|
||||
// @ts-ignore - property is used
|
||||
private async setGitHubNotificationsStateToggle() {
|
||||
const data: AdminAccountData = await this.botIntent.underlyingClient.getRoomAccountData(
|
||||
BRIDGE_ROOM_TYPE, this.roomId,
|
||||
);
|
||||
const oldState = data.notifications || {
|
||||
enabled: false,
|
||||
participating: true,
|
||||
};
|
||||
data.notifications = { enabled: !oldState?.enabled, participating: oldState?.participating };
|
||||
await this.botIntent.underlyingClient.setRoomAccountData(BRIDGE_ROOM_TYPE, this.roomId, data);
|
||||
this.emit("settings.changed", this, data);
|
||||
await this.sendNotice(`${data.notifications.enabled ? "En" : "Dis"}abled GitHub notifcations`);
|
||||
const data = await this.saveAccountData((data) => {
|
||||
return {
|
||||
...data,
|
||||
github: {
|
||||
notifications: {
|
||||
enabled: !(data.github?.notifications?.enabled ?? false),
|
||||
participating: data.github?.notifications?.participating,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
await this.sendNotice(`${data.github?.notifications?.enabled ? "En" : "Dis"}abled GitHub notifcations`);
|
||||
}
|
||||
|
||||
@botCommand("github notifications filter participating", "Toggle enabling/disabling GitHub notifications in this room")
|
||||
// @ts-ignore - property is used
|
||||
private async setGitHubNotificationsStateParticipating() {
|
||||
const data: AdminAccountData = await this.botIntent.underlyingClient.getRoomAccountData(
|
||||
BRIDGE_ROOM_TYPE, this.roomId,
|
||||
);
|
||||
const oldState = data.notifications || {
|
||||
enabled: false,
|
||||
participating: true,
|
||||
};
|
||||
data.notifications = { enabled: oldState?.enabled, participating: !oldState?.participating };
|
||||
await this.botIntent.underlyingClient.setRoomAccountData(BRIDGE_ROOM_TYPE, this.roomId, data);
|
||||
this.emit("settings.changed", this, data);
|
||||
await this.sendNotice(`${data.notifications.participating ? "En" : "Dis"}abled filtering for participating notifications`);
|
||||
const data = await this.saveAccountData((data) => {
|
||||
if (!data.github?.notifications?.enabled) {
|
||||
throw Error('Notifications are not enabled')
|
||||
}
|
||||
return {
|
||||
...data,
|
||||
github: {
|
||||
notifications: {
|
||||
participating: !(data.github?.notifications?.participating ?? false),
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
await this.sendNotice(`${data.github?.notifications?.enabled ? "" : "Not"} filtering for events you are participating in`);
|
||||
}
|
||||
|
||||
@botCommand("github project list-for-user", "List GitHub projects for a user", [], ['user', 'repo'])
|
||||
@ -273,27 +316,30 @@ export class AdminRoom extends EventEmitter {
|
||||
|
||||
/* GitLab commands */
|
||||
|
||||
@botCommand("gitlab open issue", "Open or join a issue room for GitLab", ['instanceName', 'projectParts', 'issueNumber'])
|
||||
@botCommand("gitlab open issue", "Open or join a issue room for GitLab", ['url'])
|
||||
// @ts-ignore - property is used
|
||||
private async gitLabOpenIssue(instanceName: string, projectParts: string, issueNumber: string) {
|
||||
private async gitLabOpenIssue(url: string) {
|
||||
if (!this.config.gitlab) {
|
||||
return this.sendNotice("The bridge is not configured with GitLab support");
|
||||
}
|
||||
const instance = this.config.gitlab.instances[instanceName];
|
||||
if (!instance) {
|
||||
return this.sendNotice("The bridge is not configured for this GitLab instance");
|
||||
|
||||
const urlResult = GitLabClient.splitUrlIntoParts(this.config.gitlab.instances, url);
|
||||
if (!urlResult) {
|
||||
return this.sendNotice("The URL was not understood. The URL must be an issue and the bridge must know of the GitLab instance.");
|
||||
}
|
||||
const [instanceName, parts] = urlResult;
|
||||
const instance = this.config.gitlab.instances[instanceName];
|
||||
const client = await this.tokenStore.getGitLabForUser(this.userId, instance.url);
|
||||
if (!client) {
|
||||
return this.sendNotice("You have not added a personal access token for GitLab");
|
||||
}
|
||||
const getIssueOpts = {
|
||||
issue: parseInt(issueNumber),
|
||||
projects: projectParts.split("/"),
|
||||
issue: parseInt(parts[parts.length-1]),
|
||||
projects: parts.slice(0, parts.length-3), // Remove - and /issues
|
||||
};
|
||||
log.info(`Looking up issue ${instanceName} ${getIssueOpts.projects.join("/")}#${getIssueOpts.issue}`);
|
||||
const issue = await client.issues.get(getIssueOpts);
|
||||
this.emit('open.gitlab-issue', getIssueOpts, issue, instance);
|
||||
|
||||
this.emit('open.gitlab-issue', getIssueOpts, issue, instanceName, instance);
|
||||
}
|
||||
|
||||
@botCommand("gitlab personaltoken", "Set your personal access token for GitLab", ['instanceName', 'accessToken'])
|
||||
@ -338,19 +384,47 @@ export class AdminRoom extends EventEmitter {
|
||||
await this.sendNotice("A token is stored for your GitLab account.");
|
||||
}
|
||||
|
||||
@botCommand("gitlab notifications toggle", "Toggle enabling/disabling GitHub notifications in this room")
|
||||
@botCommand("gitlab notifications toggle", "Toggle enabling/disabling GitHub notifications in this room", ["instanceName"])
|
||||
// @ts-ignore - property is used
|
||||
private async setGitLabNotificationsStateToggle() {
|
||||
const data: AdminAccountData = await this.botIntent.underlyingClient.getRoomAccountData(
|
||||
BRIDGE_GITLAB_NOTIF_TYPE, this.roomId,
|
||||
private async setGitLabNotificationsStateToggle(instanceName: string) {
|
||||
if (!this.config.gitlab) {
|
||||
return this.sendNotice("The bridge is not configured with GitLab support");
|
||||
}
|
||||
const instance = this.config.gitlab.instances[instanceName];
|
||||
if (!instance) {
|
||||
return this.sendNotice("The bridge is not configured for this GitLab instance");
|
||||
}
|
||||
const hasClient = await this.tokenStore.getGitLabForUser(this.userId, instance.url);
|
||||
if (!hasClient) {
|
||||
return this.sendNotice("You do not have a GitLab token configured for this instance");
|
||||
}
|
||||
let newValue = false;
|
||||
await this.saveAccountData((data) => {
|
||||
const currentNotifs = (data.gitlab || {})[instanceName].notifications;
|
||||
console.log("current:", currentNotifs.enabled);
|
||||
newValue = !currentNotifs.enabled;
|
||||
return {
|
||||
...data,
|
||||
gitlab: {
|
||||
[instanceName]: {
|
||||
notifications: {
|
||||
enabled: newValue,
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
await this.sendNotice(`${newValue ? "En" : "Dis"}abled GitLab notifications for ${instanceName}`);
|
||||
}
|
||||
|
||||
private async saveAccountData(updateFn: (record: AdminAccountData) => AdminAccountData) {
|
||||
const oldData: AdminAccountData = await this.botIntent.underlyingClient.getRoomAccountData(
|
||||
BRIDGE_ROOM_TYPE, this.roomId,
|
||||
);
|
||||
const oldState = data.notifications || {
|
||||
enabled: false,
|
||||
};
|
||||
data.notifications = { enabled: !oldState?.enabled };
|
||||
await this.botIntent.underlyingClient.setRoomAccountData(BRIDGE_GITLAB_NOTIF_TYPE, this.roomId, data);
|
||||
this.emit("settings.changed", this, data);
|
||||
await this.sendNotice(`${data.notifications.enabled ? "En" : "Dis"}abled GitLab notifcations`);
|
||||
const newData = updateFn(oldData);
|
||||
await this.botIntent.underlyingClient.setRoomAccountData(BRIDGE_ROOM_TYPE, this.roomId, newData);
|
||||
this.emit("settings.changed", this, oldData, newData);
|
||||
return newData;
|
||||
}
|
||||
|
||||
public async handleCommand(eventId: string, command: string) {
|
||||
|
@ -7,6 +7,7 @@ import LogWrapper from "./LogWrapper";
|
||||
import axios from "axios";
|
||||
import { FormatUtil } from "./FormatUtil";
|
||||
import { IssuesGetCommentResponseData, ReposGetResponseData, IssuesGetResponseData } from "@octokit/types";
|
||||
import { IGitLabWebhookNoteEvent } from "./Gitlab/WebhookTypes";
|
||||
|
||||
const REGEX_MENTION = /(^|\s)(@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})(\s|$)/ig;
|
||||
const REGEX_MATRIX_MENTION = /<a href="https:\/\/matrix\.to\/#\/(.+)">(.*)<\/a>/gmi;
|
||||
@ -56,7 +57,7 @@ export class CommentProcessor {
|
||||
return body;
|
||||
}
|
||||
|
||||
public async getEventBodyForComment(comment: IssuesGetCommentResponseData,
|
||||
public async getEventBodyForGitHubComment(comment: IssuesGetCommentResponseData,
|
||||
repo?: ReposGetResponseData,
|
||||
issue?: IssuesGetResponseData): Promise<IMatrixCommentEvent> {
|
||||
let body = comment.body;
|
||||
@ -73,6 +74,21 @@ export class CommentProcessor {
|
||||
};
|
||||
}
|
||||
|
||||
public async getEventBodyForGitLabNote(comment: IGitLabWebhookNoteEvent): Promise<MatrixMessageContent> {
|
||||
let body = comment.object_attributes.description;
|
||||
body = this.replaceMentions(body);
|
||||
body = await this.replaceImages(body, true);
|
||||
body = emoji.emojify(body);
|
||||
const htmlBody = md.render(body);
|
||||
return {
|
||||
body,
|
||||
formatted_body: htmlBody,
|
||||
msgtype: "m.text",
|
||||
format: "org.matrix.custom.html",
|
||||
// ...FormatUtil.getPartialBodyForComment(comment, repo, issue)
|
||||
};
|
||||
}
|
||||
|
||||
private replaceMentions(body: string): string {
|
||||
return body.replace(REGEX_MENTION, (match: string, part1: string, githubId: string) => {
|
||||
const userId = this.as.getUserIdForSuffix(githubId.substr(1));
|
||||
|
@ -163,8 +163,11 @@ export class GitHubIssueConnection implements IConnection {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const commentIntent = await getIntentForUser(comment.user, this.as, this.github.octokit);
|
||||
const matrixEvent = await this.commentProcessor.getEventBodyForComment(comment, event.repository, event.issue);
|
||||
const commentIntent = await getIntentForUser({
|
||||
login: comment.user.login,
|
||||
avatarUrl: comment.user.avatar_url,
|
||||
}, this.as);
|
||||
const matrixEvent = await this.commentProcessor.getEventBodyForGitHubComment(comment, event.repository, event.issue);
|
||||
|
||||
await this.messageClient.sendMatrixMessage(this.roomId, matrixEvent, "m.room.message", commentIntent.userId);
|
||||
if (!updateState) {
|
||||
@ -189,7 +192,10 @@ export class GitHubIssueConnection implements IConnection {
|
||||
|
||||
if (this.state.comments_processed === -1) {
|
||||
// This has a side effect of creating a profile for the user.
|
||||
const creator = await getIntentForUser(issue.data.user, this.as, this.github.octokit);
|
||||
const creator = await getIntentForUser({
|
||||
login: issue.data.user.login,
|
||||
avatarUrl: issue.data.user.avatar_url
|
||||
}, this.as);
|
||||
// We've not sent any messages into the room yet, let's do it!
|
||||
if (issue.data.body) {
|
||||
await this.messageClient.sendMatrixMessage(this.roomId, {
|
||||
|
@ -8,7 +8,6 @@ export interface GitHubProjectConnectionState {
|
||||
project_id: number;
|
||||
state: "open"|"closed";
|
||||
}
|
||||
|
||||
const log = new LogWrapper("GitHubProjectConnection");
|
||||
|
||||
/**
|
||||
|
@ -9,14 +9,16 @@ import { MessageSenderClient } from "../MatrixSender";
|
||||
import { FormatUtil } from "../FormatUtil";
|
||||
import { IGitHubWebhookEvent } from "../GithubWebhooks";
|
||||
import { GitLabInstance } from "../Config";
|
||||
import { GetIssueResponse } from "../Gitlab/Types";
|
||||
import { IGitLabWebhookNoteEvent } from "../Gitlab/WebhookTypes";
|
||||
import { getIntentForUser } from "../IntentUtils";
|
||||
|
||||
export interface GitLabIssueConnectionState {
|
||||
instance: string;
|
||||
projects: string[];
|
||||
state: string;
|
||||
issue: number;
|
||||
// eslint-disable-next-line camelcase
|
||||
comments_processed: number;
|
||||
iid: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
const log = new LogWrapper("GitLabIssueConnection");
|
||||
@ -44,8 +46,34 @@ export class GitLabIssueConnection implements IConnection {
|
||||
|
||||
static readonly QueryRoomRegex = /#gitlab_(.+)_(.+)_(\d+):.*/;
|
||||
|
||||
public static createRoomForIssue() {
|
||||
// Fill me in
|
||||
public static async createRoomForIssue(instanceName: string, instance: GitLabInstance,
|
||||
issue: GetIssueResponse, projects: string[], as: Appservice,
|
||||
tokenStore: UserTokenStore, commentProcessor: CommentProcessor,
|
||||
messageSender: MessageSenderClient) {
|
||||
const state: GitLabIssueConnectionState = {
|
||||
projects,
|
||||
state: issue.state,
|
||||
iid: issue.iid,
|
||||
id: issue.id,
|
||||
instance: instanceName,
|
||||
};
|
||||
|
||||
const roomId = await as.botClient.createRoom({
|
||||
visibility: "private",
|
||||
name: `${issue.references.full}`,
|
||||
topic: `Author: ${issue.author.name} | State: ${issue.state}`,
|
||||
preset: "private_chat",
|
||||
invite: [],
|
||||
initial_state: [
|
||||
{
|
||||
type: this.CanonicalEventType,
|
||||
content: state,
|
||||
state_key: issue.web_url,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return new GitLabIssueConnection(roomId, as, state, issue.web_url, tokenStore, commentProcessor, messageSender, instance);
|
||||
}
|
||||
|
||||
public get projectPath() {
|
||||
@ -63,7 +91,7 @@ export class GitLabIssueConnection implements IConnection {
|
||||
private tokenStore: UserTokenStore,
|
||||
private commentProcessor: CommentProcessor,
|
||||
private messageClient: MessageSenderClient,
|
||||
private instance: GitLabInstance) {
|
||||
private instance: GitLabInstance,) {
|
||||
}
|
||||
|
||||
public isInterestedInStateEvent(eventType: string, stateKey: string) {
|
||||
@ -71,33 +99,29 @@ export class GitLabIssueConnection implements IConnection {
|
||||
}
|
||||
|
||||
public get issueNumber() {
|
||||
return this.state.issue;
|
||||
return this.state.iid;
|
||||
}
|
||||
|
||||
// public async onCommentCreated(event: IGitHubWebhookEvent, updateState = true) {
|
||||
// const comment = event.comment!;
|
||||
// if (event.repository) {
|
||||
// // Delay to stop comments racing sends
|
||||
// await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
// if (this.commentProcessor.hasCommentBeenProcessed(this.state.org, this.state.repo, this.state.issues[0], comment.id)) {
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// const commentIntent = await getIntentForUser(comment.user, this.as, this.octokit);
|
||||
// const matrixEvent = await this.commentProcessor.getEventBodyForComment(comment, event.repository, event.issue);
|
||||
public async onCommentCreated(event: IGitLabWebhookNoteEvent) {
|
||||
if (event.repository) {
|
||||
// Delay to stop comments racing sends
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
if (this.commentProcessor.hasCommentBeenProcessed(
|
||||
this.state.instance,
|
||||
this.state.projects.join("/"),
|
||||
this.state.iid.toString(),
|
||||
event.object_attributes.noteable_id)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const commentIntent = await getIntentForUser({
|
||||
login: event.user.name,
|
||||
avatarUrl: event.user.avatar_url,
|
||||
}, this.as);
|
||||
const matrixEvent = await this.commentProcessor.getEventBodyForGitLabNote(event);
|
||||
|
||||
// await this.messageClient.sendMatrixMessage(this.roomId, matrixEvent, "m.room.message", commentIntent.userId);
|
||||
// if (!updateState) {
|
||||
// return;
|
||||
// }
|
||||
// this.state.comments_processed++;
|
||||
// await this.as.botIntent.underlyingClient.sendStateEvent(
|
||||
// this.roomId,
|
||||
// GitLabIssueConnection.CanonicalEventType,
|
||||
// this.stateKey,
|
||||
// this.state,
|
||||
// );
|
||||
// }
|
||||
await this.messageClient.sendMatrixMessage(this.roomId, matrixEvent, "m.room.message", commentIntent.userId);
|
||||
}
|
||||
|
||||
// private async syncIssueState() {
|
||||
// log.debug("Syncing issue state for", this.roomId);
|
||||
@ -171,7 +195,6 @@ export class GitLabIssueConnection implements IConnection {
|
||||
|
||||
|
||||
public async onMatrixIssueComment(event: MatrixEvent<MatrixMessageContent>, allowEcho = false) {
|
||||
|
||||
console.log(this.messageClient, this.commentProcessor);
|
||||
const clientKit = await this.tokenStore.getGitLabForUser(event.sender, this.instanceUrl);
|
||||
if (clientKit === null) {
|
||||
@ -186,15 +209,20 @@ export class GitLabIssueConnection implements IConnection {
|
||||
return;
|
||||
}
|
||||
|
||||
// const result = await clientKit.issues.createComment({
|
||||
// repo: this.state.repo,
|
||||
// owner: this.state.org,
|
||||
// body: await this.commentProcessor.getCommentBodyForEvent(event, false),
|
||||
// issue_number: parseInt(this.state.issues[0], 10),
|
||||
// });
|
||||
const result = await clientKit.notes.createForIssue(
|
||||
this.state.projects,
|
||||
this.state.iid, {
|
||||
body: await this.commentProcessor.getCommentBodyForEvent(event, false),
|
||||
}
|
||||
);
|
||||
|
||||
if (!allowEcho) {
|
||||
//this.commentProcessor.markCommentAsProcessed(this.state.org, this.state.repo, this.state.issues[0], result.data.id);
|
||||
this.commentProcessor.markCommentAsProcessed(
|
||||
this.state.instance,
|
||||
this.state.projects.join("/"),
|
||||
this.state.iid.toString(),
|
||||
result.noteable_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,11 +17,6 @@ export interface IConnection {
|
||||
*/
|
||||
onMessageEvent?: (ev: MatrixEvent<MatrixMessageContent>) => Promise<void>;
|
||||
|
||||
/**
|
||||
* When a comment is created on a repo
|
||||
*/
|
||||
onCommentCreated?: (ev: IGitHubWebhookEvent) => Promise<void>;
|
||||
|
||||
onIssueCreated?: (ev: IGitHubWebhookEvent) => Promise<void>;
|
||||
|
||||
onIssueStateChange?: (ev: IGitHubWebhookEvent) => Promise<void>;
|
||||
|
@ -12,8 +12,8 @@ export class FormatUtil {
|
||||
return `${orgRepoName}#${issue.number}: ${issue.title}`;
|
||||
}
|
||||
|
||||
public static formatRepoRoomName(repo: {full_name: string, url: string, title: string, number: number}) {
|
||||
return `${repo.full_name}#${repo.number}: ${repo.title}`;
|
||||
public static formatRepoRoomName(repo: {full_name: string, description: string}) {
|
||||
return `${repo.full_name}: ${repo.description}`;
|
||||
}
|
||||
|
||||
public static formatRoomTopic(repo: {state: string, html_url: string}) {
|
||||
|
@ -31,7 +31,7 @@ export class GithubInstance {
|
||||
public async start() {
|
||||
// TODO: Make this generic.
|
||||
const auth = {
|
||||
id: parseInt(this.config.auth.id as string, 10),
|
||||
appId: parseInt(this.config.auth.id as string, 10),
|
||||
privateKey: await fs.readFile(this.config.auth.privateKeyFile, "utf-8"),
|
||||
installationId: parseInt(this.config.installationId as string, 10),
|
||||
};
|
||||
|
@ -25,6 +25,7 @@ import { GithubInstance } from "./Github/GithubInstance";
|
||||
import { IGitLabWebhookMREvent, IGitLabWebhookNoteEvent } from "./Gitlab/WebhookTypes";
|
||||
import { GitLabIssueConnection } from "./Connections/GitlabIssue";
|
||||
import { GetIssueResponse, GetIssueOpts } from "./Gitlab/Types"
|
||||
import { GitLabClient } from "./Gitlab/Client";
|
||||
// import { IGitLabWebhookMREvent } from "./Gitlab/WebhookTypes";
|
||||
|
||||
const log = new LogWrapper("GithubBridge");
|
||||
@ -97,18 +98,22 @@ export class GithubBridge {
|
||||
return state.map((event) => this.createConnectionForState(roomId, event)).filter((connection) => !!connection) as unknown as IConnection[];
|
||||
}
|
||||
|
||||
private getConnectionsForGithubIssue(org: string, repo: string, issueNumber: number) {
|
||||
private getConnectionsForGithubIssue(org: string, repo: string, issueNumber: number): (GitHubIssueConnection|GitLabRepoConnection)[] {
|
||||
return this.connections.filter((c) => (c instanceof GitHubIssueConnection && c.org === org && c.repo === repo && c.issueNumber === issueNumber) ||
|
||||
(c instanceof GitHubRepoConnection && c.org === org && c.repo === repo));
|
||||
(c instanceof GitHubRepoConnection && c.org === org && c.repo === repo)) as (GitHubIssueConnection|GitLabRepoConnection)[];
|
||||
}
|
||||
|
||||
private getConnectionsForGitLabIssue(instance: GitLabInstance, projects: string[], issueNumber: number) {
|
||||
private getConnectionsForGithubRepo(org: string, repo: string): GitHubRepoConnection[] {
|
||||
return this.connections.filter((c) => (c instanceof GitHubRepoConnection && c.org === org && c.repo === repo)) as GitHubRepoConnection[];
|
||||
}
|
||||
|
||||
private getConnectionsForGitLabIssue(instance: GitLabInstance, projects: string[], issueNumber: number): GitLabIssueConnection[] {
|
||||
return this.connections.filter((c) => (
|
||||
c instanceof GitLabIssueConnection &&
|
||||
c.issueNumber == issueNumber &&
|
||||
c.instanceUrl == instance.url &&
|
||||
c.projectPath == projects.join("/")
|
||||
));
|
||||
)) as GitLabIssueConnection[];
|
||||
}
|
||||
|
||||
public stop() {
|
||||
@ -204,7 +209,7 @@ export class GithubBridge {
|
||||
const connections = this.getConnectionsForGithubIssue(repository.owner.login, repository.name, issue.number);
|
||||
connections.map(async (c) => {
|
||||
try {
|
||||
if (c.onCommentCreated)
|
||||
if (c instanceof GitHubIssueConnection)
|
||||
await c.onCommentCreated(data);
|
||||
} catch (ex) {
|
||||
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||
@ -213,12 +218,11 @@ export class GithubBridge {
|
||||
});
|
||||
|
||||
this.queue.on<IGitHubWebhookEvent>("issue.opened", async ({ data }) => {
|
||||
const { repository, issue } = validateRepoIssue(data);
|
||||
const connections = this.getConnectionsForGithubIssue(repository.owner.login, repository.name, issue.number);
|
||||
const { repository } = validateRepoIssue(data);
|
||||
const connections = this.getConnectionsForGithubRepo(repository.owner.login, repository.name);
|
||||
connections.map(async (c) => {
|
||||
try {
|
||||
if (c.onIssueCreated)
|
||||
await c.onIssueCreated(data);
|
||||
await c.onIssueCreated(data);
|
||||
} catch (ex) {
|
||||
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||
}
|
||||
@ -230,7 +234,7 @@ export class GithubBridge {
|
||||
const connections = this.getConnectionsForGithubIssue(repository.owner.login, repository.name, issue.number);
|
||||
connections.map(async (c) => {
|
||||
try {
|
||||
if (c.onIssueEdited)
|
||||
if (c instanceof GitHubIssueConnection)
|
||||
await c.onIssueEdited(data);
|
||||
} catch (ex) {
|
||||
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||
@ -243,8 +247,8 @@ export class GithubBridge {
|
||||
const connections = this.getConnectionsForGithubIssue(repository.owner.login, repository.name, issue.number);
|
||||
connections.map(async (c) => {
|
||||
try {
|
||||
if (c.onIssueStateChange)
|
||||
await c.onIssueStateChange(data);
|
||||
if (c instanceof GitHubIssueConnection)
|
||||
await c.onIssueStateChange();
|
||||
} catch (ex) {
|
||||
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||
}
|
||||
@ -256,8 +260,8 @@ export class GithubBridge {
|
||||
const connections = this.getConnectionsForGithubIssue(repository.owner.login, repository.name, issue.number);
|
||||
connections.map(async (c) => {
|
||||
try {
|
||||
if (c.onIssueStateChange)
|
||||
await c.onIssueStateChange(data);
|
||||
if (c instanceof GitHubIssueConnection)
|
||||
await c.onIssueStateChange();
|
||||
} catch (ex) {
|
||||
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||
}
|
||||
@ -306,17 +310,24 @@ export class GithubBridge {
|
||||
await this.tokenStore.storeUserToken("github", adminRoom.userId, msg.data.access_token);
|
||||
});
|
||||
|
||||
this.queue.on<IGitLabWebhookNoteEvent>("gitlab.note.created", async (msg) => {
|
||||
console.log(msg);
|
||||
// const connections = this.getConnectionsForGitLabIssue(msg.data.repository!.owner.login, msg.data.repository!.name, msg.data.issue!.number);
|
||||
// connections.map(async (c) => {
|
||||
// try {
|
||||
// if (c.onCommentCreated)
|
||||
// await c.onCommentCreated(msg.data);
|
||||
// } catch (ex) {
|
||||
// log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||
// }
|
||||
// })
|
||||
this.queue.on<IGitLabWebhookNoteEvent>("gitlab.note.created", async ({data}) => {
|
||||
if (!this.config.gitlab) {
|
||||
throw Error('GitLab configuration missing, cannot handle note');
|
||||
}
|
||||
const res = GitLabClient.splitUrlIntoParts(this.config.gitlab.instances, data.repository.homepage);
|
||||
if (!res) {
|
||||
throw Error('No instance found for note');
|
||||
}
|
||||
const instance = this.config.gitlab.instances[res[0]];
|
||||
const connections = this.getConnectionsForGitLabIssue(instance, res[1], data.issue.iid);
|
||||
connections.map(async (c) => {
|
||||
try {
|
||||
if (c.onCommentCreated)
|
||||
await c.onCommentCreated(data);
|
||||
} catch (ex) {
|
||||
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Fetch all room state
|
||||
@ -354,7 +365,7 @@ export class GithubBridge {
|
||||
}
|
||||
|
||||
for (const roomId of joinedRooms) {
|
||||
log.info("Fetching state for " + roomId);
|
||||
log.debug("Fetching state for " + roomId);
|
||||
const connections = await this.createConnectionsForRoomId(roomId);
|
||||
this.connections.push(...connections);
|
||||
if (connections.length === 0) {
|
||||
@ -365,7 +376,7 @@ export class GithubBridge {
|
||||
);
|
||||
const adminRoom = this.setupAdminRoom(roomId, accountData);
|
||||
// Call this on startup to set the state
|
||||
await this.onAdminRoomSettingsChanged(adminRoom, accountData);
|
||||
await this.onAdminRoomSettingsChanged(adminRoom, accountData, { admin_user: accountData.admin_user });
|
||||
} catch (ex) {
|
||||
log.warn(`Room ${roomId} has no connections and is not an admin room`);
|
||||
}
|
||||
@ -390,7 +401,7 @@ export class GithubBridge {
|
||||
}
|
||||
await retry(() => this.as.botIntent.joinRoom(roomId), 5);
|
||||
if (event.content.is_direct) {
|
||||
const room = this.setupAdminRoom(roomId, {admin_user: event.sender, notifications: { enabled: false, participating: false}});
|
||||
const room = this.setupAdminRoom(roomId, {admin_user: event.sender});
|
||||
await this.as.botIntent.underlyingClient.setRoomAccountData(
|
||||
BRIDGE_ROOM_TYPE, roomId, room.data,
|
||||
);
|
||||
@ -541,9 +552,10 @@ export class GithubBridge {
|
||||
throw Error('No regex matching query pattern');
|
||||
}
|
||||
|
||||
private async onAdminRoomSettingsChanged(adminRoom: AdminRoom, settings: AdminAccountData) {
|
||||
log.info(`Settings changed for ${adminRoom.userId} ${settings}`);
|
||||
if (adminRoom.notificationsEnabled) {
|
||||
private async onAdminRoomSettingsChanged(adminRoom: AdminRoom, settings: AdminAccountData, oldSettings: AdminAccountData) {
|
||||
log.info(`Settings changed for ${adminRoom.userId}`, settings);
|
||||
// Make this more efficent.
|
||||
if (!oldSettings.github?.notifications?.enabled && settings.github?.notifications?.enabled) {
|
||||
log.info(`Notifications enabled for ${adminRoom.userId}`);
|
||||
const token = await this.tokenStore.getUserToken("github", adminRoom.userId);
|
||||
if (token) {
|
||||
@ -555,8 +567,8 @@ export class GithubBridge {
|
||||
userId: adminRoom.userId,
|
||||
roomId: adminRoom.roomId,
|
||||
token,
|
||||
since: await adminRoom.getNotifSince(),
|
||||
filterParticipating: adminRoom.notificationsParticipating,
|
||||
since: await adminRoom.getNotifSince("github"),
|
||||
filterParticipating: adminRoom.notificationsParticipating("github"),
|
||||
type: "github",
|
||||
instanceUrl: undefined,
|
||||
},
|
||||
@ -564,7 +576,7 @@ export class GithubBridge {
|
||||
} else {
|
||||
log.warn(`Notifications enabled for ${adminRoom.userId} but no token stored!`);
|
||||
}
|
||||
} else {
|
||||
} else if (oldSettings.github?.notifications?.enabled && !settings.github?.notifications?.enabled) {
|
||||
await this.queue.push<NotificationsDisableEvent>({
|
||||
eventName: "notifications.user.disable",
|
||||
sender: "GithubBridge",
|
||||
@ -575,6 +587,39 @@ export class GithubBridge {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const [instanceName, instanceSettings] of Object.entries(settings.gitlab || {})) {
|
||||
const instanceUrl = this.config.gitlab?.instances[instanceName].url;
|
||||
const token = await this.tokenStore.getUserToken("gitlab", adminRoom.userId, instanceUrl);
|
||||
if (token && instanceSettings.notifications.enabled) {
|
||||
log.info(`GitLab ${instanceName} notifications enabled for ${adminRoom.userId}`);
|
||||
await this.queue.push<NotificationsEnableEvent>({
|
||||
eventName: "notifications.user.enable",
|
||||
sender: "GithubBridge",
|
||||
data: {
|
||||
userId: adminRoom.userId,
|
||||
roomId: adminRoom.roomId,
|
||||
token,
|
||||
since: await adminRoom.getNotifSince("gitlab", instanceName),
|
||||
filterParticipating: adminRoom.notificationsParticipating("gitlab"),
|
||||
type: "gitlab",
|
||||
instanceUrl,
|
||||
},
|
||||
});
|
||||
} else if (!instanceSettings.notifications.enabled) {
|
||||
log.info(`GitLab ${instanceName} notifications disabled for ${adminRoom.userId}`);
|
||||
await this.queue.push<NotificationsDisableEvent>({
|
||||
eventName: "notifications.user.disable",
|
||||
sender: "GithubBridge",
|
||||
data: {
|
||||
userId: adminRoom.userId,
|
||||
type: "gitlab",
|
||||
instanceUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private setupAdminRoom(roomId: string, accountData: AdminAccountData) {
|
||||
@ -586,13 +631,23 @@ export class GithubBridge {
|
||||
const connection = await GitHubProjectConnection.onOpenProject(project, this.as, adminRoom.userId);
|
||||
this.connections.push(connection);
|
||||
});
|
||||
adminRoom.on("open.gitlab-issue", async (issueInfo: GetIssueOpts, res: GetIssueResponse, instance: GitLabInstance) => {
|
||||
adminRoom.on("open.gitlab-issue", async (issueInfo: GetIssueOpts, res: GetIssueResponse, instanceName: string, instance: GitLabInstance) => {
|
||||
const [ connection ] = this.getConnectionsForGitLabIssue(instance, issueInfo.projects, issueInfo.issue);
|
||||
if (connection) {
|
||||
return this.as.botClient.inviteUser(adminRoom.userId, connection.roomId);
|
||||
}
|
||||
// connection = await GitLabIssueConnection.createRoomForIssue(instance, res, this.as);
|
||||
// this.connections.push(connection);
|
||||
}
|
||||
const newConnection = await GitLabIssueConnection.createRoomForIssue(
|
||||
instanceName,
|
||||
instance,
|
||||
res,
|
||||
issueInfo.projects,
|
||||
this.as,
|
||||
this.tokenStore,
|
||||
this.commentProcessor,
|
||||
this.messageClient
|
||||
);
|
||||
this.connections.push(newConnection);
|
||||
return this.as.botClient.inviteUser(adminRoom.userId, newConnection.roomId);
|
||||
});
|
||||
this.adminRooms.set(roomId, adminRoom);
|
||||
log.info(`Setup ${roomId} as an admin room for ${adminRoom.userId}`);
|
||||
|
@ -1,44 +1,89 @@
|
||||
import axios from "axios";
|
||||
import { GetIssueResponse, GetUserResponse, CreateIssueOpts, CreateIssueResponse, GetIssueOpts, EditIssueOpts, GetTodosResponse } from "./Types";
|
||||
import { GitLabInstance } from "../Config";
|
||||
import { GetIssueResponse, GetUserResponse, CreateIssueOpts, CreateIssueResponse, GetIssueOpts, EditIssueOpts, GetTodosResponse, EventsOpts, CreateIssueNoteOpts, CreateIssueNoteResponse } from "./Types";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { URLSearchParams } from "url";
|
||||
|
||||
const log = new LogWrapper("GitLabClient");
|
||||
export class GitLabClient {
|
||||
constructor(private instanceUrl: string, private token: string) {
|
||||
|
||||
}
|
||||
|
||||
public static splitUrlIntoParts(instances: {[name: string]: GitLabInstance}, url: string): [string, string[]]|null {
|
||||
for (const [instanceKey, instanceConfig] of Object.entries(instances)) {
|
||||
if (url.startsWith(instanceConfig.url)) {
|
||||
return [instanceKey, url.substr(instanceConfig.url.length).split("/").filter(part => part.length > 0)];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get defaultConfig() {
|
||||
return {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
UserAgent: "matrix-github v0.0.1",
|
||||
"Authorization": `Bearer ${this.token}`,
|
||||
"User-Agent": "matrix-github v0.0.1",
|
||||
},
|
||||
baseURL: this.instanceUrl
|
||||
};
|
||||
}
|
||||
|
||||
async version() {
|
||||
return (await axios.get(`${this.instanceUrl}/api/v4/versions`, this.defaultConfig)).data;
|
||||
return (await axios.get("api/v4/versions", this.defaultConfig)).data;
|
||||
}
|
||||
|
||||
async user(): Promise<GetUserResponse> {
|
||||
return (await axios.get(`${this.instanceUrl}/api/v4/user`, this.defaultConfig)).data;
|
||||
return (await axios.get("api/v4/user", this.defaultConfig)).data;
|
||||
}
|
||||
|
||||
private async createIssue(opts: CreateIssueOpts): Promise<CreateIssueResponse> {
|
||||
return (await axios.post(`${this.instanceUrl}/api/v4/projects/${opts.id}/issues`, opts, this.defaultConfig)).data;
|
||||
return (await axios.post(`api/v4/projects/${opts.id}/issues`, opts, this.defaultConfig)).data;
|
||||
}
|
||||
|
||||
private async getIssue(opts: GetIssueOpts): Promise<GetIssueResponse> {
|
||||
const projectBit = opts.projects.join("%2F");
|
||||
const url = `${this.instanceUrl}/api/v4/projects/${projectBit}/issues/${opts.issue}`;
|
||||
return (await axios.get(url, this.defaultConfig)).data;
|
||||
try {
|
||||
return (await axios.get(`api/v4/projects/${opts.projects.join("%2F")}/issues/${opts.issue}`, this.defaultConfig)).data;
|
||||
} catch (ex) {
|
||||
log.warn(`Failed to get issue:`, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private async editIssue(opts: EditIssueOpts): Promise<CreateIssueResponse> {
|
||||
return (await axios.put(`${this.instanceUrl}/api/v4/projects/${opts.id}/issues/${opts.issue_iid}`, opts, this.defaultConfig)).data;
|
||||
return (await axios.put(`api/v4/projects/${opts.id}/issues/${opts.issue_iid}`, opts, this.defaultConfig)).data;
|
||||
}
|
||||
|
||||
public async getTodos() {
|
||||
return (await axios.get(`${this.instanceUrl}/api/v4/todos`, this.defaultConfig)).data as GetTodosResponse[];
|
||||
private async getProject(projectParts: string[]): Promise<GetIssueResponse> {
|
||||
try {
|
||||
return (await axios.get(`api/v4/projects/${projectParts.join("%2F")}`, this.defaultConfig)).data;
|
||||
} catch (ex) {
|
||||
log.warn(`Failed to get issue:`, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
public async getEvents(opts: EventsOpts) {
|
||||
const after = `${opts.after.getFullYear()}-` +
|
||||
`${(opts.after.getMonth()+1).toString().padStart(2, "0")}`+
|
||||
`-${opts.after.getDay().toString().padStart(2, "0")}`;
|
||||
return (await axios.get(
|
||||
`api/v4/events?after=${after}`,
|
||||
this.defaultConfig)
|
||||
).data as GetTodosResponse[];
|
||||
}
|
||||
|
||||
public async createIssueNote(projectParts: string[], issueId: number, opts: CreateIssueNoteOpts): Promise<CreateIssueNoteResponse> {
|
||||
try {
|
||||
const qp = new URLSearchParams({
|
||||
body: opts.body,
|
||||
confidential: (opts.confidential || false).toString(),
|
||||
}).toString();
|
||||
return (await axios.post(`api/v4/projects/${projectParts.join("%2F")}/issues/${issueId}/notes?${qp}`, undefined, this.defaultConfig)).data as CreateIssueNoteResponse;
|
||||
} catch (ex) {
|
||||
log.warn(`Failed to create issue note:`, ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
get issues() {
|
||||
@ -48,4 +93,15 @@ export class GitLabClient {
|
||||
get: this.getIssue.bind(this),
|
||||
}
|
||||
}
|
||||
get projects() {
|
||||
return {
|
||||
get: this.getProject.bind(this),
|
||||
}
|
||||
}
|
||||
|
||||
get notes() {
|
||||
return {
|
||||
createForIssue: this.createIssueNote.bind(this),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
/* eslint-disable camelcase */
|
||||
export interface GitLabAuthor {
|
||||
author: {
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
state: 'active';
|
||||
avatar_url: string;
|
||||
web_url: string;
|
||||
};
|
||||
id: number;
|
||||
name: string;
|
||||
username: string;
|
||||
state: 'active';
|
||||
avatar_url: string;
|
||||
web_url: string;
|
||||
}
|
||||
|
||||
export interface GetUserResponse {
|
||||
@ -127,4 +125,31 @@ export interface GetTodosResponse {
|
||||
body: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
}
|
||||
|
||||
export interface EventsOpts {
|
||||
after: Date;
|
||||
}
|
||||
|
||||
export interface CreateIssueNoteOpts {
|
||||
body: string;
|
||||
confidential?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateIssueNoteResponse {
|
||||
id: number;
|
||||
type: string|null;
|
||||
body: string;
|
||||
attachment: null;
|
||||
author: GitLabAuthor;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
system: boolean;
|
||||
noteable_id: number;
|
||||
noteable_type: 'Issue';
|
||||
resolvable: boolean;
|
||||
confidential: boolean;
|
||||
noteable_iid: string;
|
||||
commands_changes: unknown;
|
||||
}
|
||||
|
@ -9,19 +9,19 @@ export interface IGitLabWebhookEvent {
|
||||
}
|
||||
}
|
||||
|
||||
interface IGitlabUser {
|
||||
export interface IGitlabUser {
|
||||
name: string;
|
||||
username: string;
|
||||
avatar_url: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface IGitlabProject {
|
||||
export interface IGitlabProject {
|
||||
path_with_namespace: string;
|
||||
web_url: string;
|
||||
}
|
||||
|
||||
interface IGitlabIssue {
|
||||
export interface IGitlabIssue {
|
||||
iid: number;
|
||||
description: string;
|
||||
}
|
||||
@ -37,4 +37,14 @@ export interface IGitLabWebhookNoteEvent {
|
||||
user: IGitlabUser;
|
||||
project: IGitlabProject;
|
||||
issue: IGitlabIssue;
|
||||
repository: {
|
||||
name: string;
|
||||
url: string;
|
||||
description: string;
|
||||
homepage: string;
|
||||
};
|
||||
object_attributes: {
|
||||
noteable_id: number;
|
||||
description: string;
|
||||
}
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import LogWrapper from "./LogWrapper";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import axios from "axios";
|
||||
|
||||
const log = new LogWrapper("IntentUtils");
|
||||
|
||||
export async function getIntentForUser(user: {avatar_url?: string, login: string}, as: Appservice, octokit: Octokit) {
|
||||
export async function getIntentForUser(user: {avatarUrl?: string, login: string}, as: Appservice) {
|
||||
const intent = as.getIntentForSuffix(user.login);
|
||||
const displayName = `${user.login}`;
|
||||
// Verify up-to-date profile
|
||||
@ -22,19 +22,20 @@ export async function getIntentForUser(user: {avatar_url?: string, login: string
|
||||
await intent.underlyingClient.setDisplayName(displayName);
|
||||
}
|
||||
|
||||
if (!profile.avatar_url && user.avatar_url) {
|
||||
if (!profile.avatar_url && user.avatarUrl) {
|
||||
log.debug(`Updating ${intent.userId}'s avatar`);
|
||||
const buffer = await octokit.request(user.avatar_url);
|
||||
log.info(`uploading ${user.avatar_url}`);
|
||||
const buffer = await axios.get(user.avatarUrl, {
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
log.info(`Uploading ${user.avatarUrl}`);
|
||||
// This does exist, but headers is silly and doesn't have content-type.
|
||||
// tslint:disable-next-line: no-any
|
||||
const contentType = (buffer.headers as any)["content-type"];
|
||||
const contentType = buffer.headers["content-type"];
|
||||
const mxc = await intent.underlyingClient.uploadContent(
|
||||
Buffer.from(buffer.data as ArrayBuffer),
|
||||
contentType,
|
||||
);
|
||||
await intent.underlyingClient.setAvatarUrl(mxc);
|
||||
|
||||
}
|
||||
|
||||
return intent;
|
||||
|
@ -38,6 +38,11 @@ export default class LogWrapper {
|
||||
};
|
||||
LogService.setLogger({
|
||||
info: (module: string, ...messageOrObject: any[]) => {
|
||||
// These are noisy, redirect to debug.
|
||||
if (module.startsWith("MatrixLiteClient")) {
|
||||
log.debug(getMessageString(messageOrObject), { module });
|
||||
return;
|
||||
}
|
||||
log.info(getMessageString(messageOrObject), { module });
|
||||
},
|
||||
warn: (module: string, ...messageOrObject: any[]) => {
|
||||
|
37
src/Notifications/GitLabWatcher.ts
Normal file
37
src/Notifications/GitLabWatcher.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { EventEmitter } from "events";
|
||||
import { GitLabClient } from "../Gitlab/Client";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { NotificationWatcherTask } from "./NotificationWatcherTask";
|
||||
|
||||
const log = new LogWrapper("GitLabWatcher");
|
||||
|
||||
export class GitLabWatcher extends EventEmitter implements NotificationWatcherTask {
|
||||
private client: GitLabClient;
|
||||
private interval?: NodeJS.Timeout;
|
||||
public readonly type = "gitlab";
|
||||
public failureCount = 0;
|
||||
constructor(token: string, url: string, public userId: string, public roomId: string, public since: number) {
|
||||
super();
|
||||
this.client = new GitLabClient(url, token);
|
||||
}
|
||||
|
||||
public start(intervalMs: number) {
|
||||
this.interval = setTimeout(() => {
|
||||
this.getNotifications();
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
private async getNotifications() {
|
||||
log.info(`Fetching events from GitLab for ${this.userId}`);
|
||||
const events = await this.client.getEvents({
|
||||
after: new Date(this.since)
|
||||
});
|
||||
console.log(events);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import { MessageSenderClient } from "../MatrixSender";
|
||||
import { NotificationWatcherTask } from "./NotificationWatcherTask";
|
||||
import { GitHubWatcher } from "./GitHubWatcher";
|
||||
import { GitHubUserNotification } from "../Github/Types";
|
||||
import { GitLabWatcher } from "./GitLabWatcher";
|
||||
|
||||
export interface UserNotificationsEvent {
|
||||
roomId: string;
|
||||
@ -59,14 +60,14 @@ Check your token is still valid, and then turn notifications back on.`, "m.notic
|
||||
let task: NotificationWatcherTask;
|
||||
const key = UserNotificationWatcher.constructMapKey(data.userId, data.type, data.instanceUrl);
|
||||
if (data.type === "github") {
|
||||
this.userIntervals.get(key)?.stop();
|
||||
task = new GitHubWatcher(data.token, data.userId, data.roomId, data.since, data.filterParticipating);
|
||||
task.start(MIN_INTERVAL_MS);
|
||||
}/* else if (data.type === "gitlab") {
|
||||
|
||||
}*/ else {
|
||||
} else if (data.type === "gitlab" && data.instanceUrl) {
|
||||
task = new GitLabWatcher(data.token, data.instanceUrl, data.userId, data.roomId, data.since);
|
||||
} else {
|
||||
throw Error('Notification type not known');
|
||||
}
|
||||
this.userIntervals.get(key)?.stop();
|
||||
task.start(MIN_INTERVAL_MS);
|
||||
task.on("fetch_failure", this.onFetchFailure.bind(this));
|
||||
task.on("new_events", (payload) => {
|
||||
this.queue.push<UserNotificationsEvent>(payload);
|
||||
|
@ -140,7 +140,7 @@ export class NotificationProcessor {
|
||||
}
|
||||
}
|
||||
try {
|
||||
await adminRoom.setNotifSince(msg.lastReadTs);
|
||||
await adminRoom.setNotifSince("github", msg.lastReadTs);
|
||||
} catch (ex) {
|
||||
log.error("Failed to update stream position for notifications:", ex);
|
||||
}
|
||||
|
@ -9,6 +9,13 @@ const ACCOUNT_DATA_TYPE = "uk.half-shot.matrix-github.password-store:";
|
||||
const ACCOUNT_DATA_GITLAB_TYPE = "uk.half-shot.matrix-github.gitlab.password-store:";
|
||||
const log = new LogWrapper("UserTokenStore");
|
||||
|
||||
function tokenKey(type: "github"|"gitlab", userId: string, instanceUrl?: string) {
|
||||
if (type === "github") {
|
||||
return `${ACCOUNT_DATA_TYPE}${userId}`;
|
||||
}
|
||||
return `${ACCOUNT_DATA_GITLAB_TYPE}${instanceUrl}${userId}`;
|
||||
}
|
||||
|
||||
export class UserTokenStore {
|
||||
private key!: Buffer;
|
||||
private userTokens: Map<string, string>;
|
||||
@ -21,32 +28,35 @@ export class UserTokenStore {
|
||||
this.key = await fs.readFile(this.keyPath);
|
||||
}
|
||||
|
||||
public async storeUserToken(type: "github"|"gitlab", userId: string, token: string, instance?: string): Promise<void> {
|
||||
const prefix = type === "github" ? ACCOUNT_DATA_TYPE : ACCOUNT_DATA_GITLAB_TYPE;
|
||||
await this.intent.underlyingClient.setAccountData(`${prefix}${userId}`, {
|
||||
public async storeUserToken(type: "github"|"gitlab", userId: string, token: string, instanceUrl?: string): Promise<void> {
|
||||
const key = tokenKey(type, userId, instanceUrl);
|
||||
const data = {
|
||||
encrypted: publicEncrypt(this.key, Buffer.from(token)).toString("base64"),
|
||||
instance: instance,
|
||||
});
|
||||
this.userTokens.set(userId, token);
|
||||
instance: instanceUrl,
|
||||
};
|
||||
await this.intent.underlyingClient.setAccountData(key, data);
|
||||
this.userTokens.set(key, token);
|
||||
log.info(`Stored new ${type} token for ${userId}`);
|
||||
log.debug(`Stored`, data);
|
||||
}
|
||||
|
||||
public async getUserToken(type: "github"|"gitlab", userId: string, instance?: string): Promise<string|null> {
|
||||
const existingToken = this.userTokens.get(userId);
|
||||
public async getUserToken(type: "github"|"gitlab", userId: string, instanceUrl?: string): Promise<string|null> {
|
||||
const key = tokenKey(type, userId, instanceUrl);
|
||||
const existingToken = this.userTokens.get(key);
|
||||
if (existingToken) {
|
||||
return existingToken;
|
||||
}
|
||||
let obj;
|
||||
try {
|
||||
if (type === "github") {
|
||||
obj = await this.intent.underlyingClient.getAccountData(`${ACCOUNT_DATA_TYPE}${userId}`);
|
||||
obj = await this.intent.underlyingClient.getAccountData(key);
|
||||
} else if (type === "gitlab") {
|
||||
obj = await this.intent.underlyingClient.getAccountData(`${ACCOUNT_DATA_GITLAB_TYPE}${instance}${userId}`);
|
||||
obj = await this.intent.underlyingClient.getAccountData(key);
|
||||
}
|
||||
const encryptedTextB64 = obj.encrypted;
|
||||
const encryptedText = Buffer.from(encryptedTextB64, "base64");
|
||||
const token = privateDecrypt(this.key, encryptedText).toString("utf-8");
|
||||
this.userTokens.set(userId, token);
|
||||
this.userTokens.set(key, token);
|
||||
return token;
|
||||
} catch (ex) {
|
||||
log.error(`Failed to get token for user ${userId}`);
|
||||
|
@ -11,9 +11,21 @@ const SIMPLE_ISSUE = {
|
||||
repository_url: "https://api.github.com/repos/evilcorp/lab",
|
||||
};
|
||||
|
||||
const SIMPLE_REPO = {
|
||||
description: "A simple description",
|
||||
full_name: "evilcorp/lab",
|
||||
html_url: "https://github.com/evilcorp/lab/issues/123",
|
||||
};
|
||||
|
||||
|
||||
describe("FormatUtilTest", () => {
|
||||
it("correctly formats a room name", () => {
|
||||
expect(FormatUtil.formatRepoRoomName(SIMPLE_ISSUE)).to.equal(
|
||||
it("correctly formats a repo room name", () => {
|
||||
expect(FormatUtil.formatRepoRoomName(SIMPLE_REPO)).to.equal(
|
||||
"evilcorp/lab: A simple description",
|
||||
);
|
||||
});
|
||||
it("correctly formats a issue room name", () => {
|
||||
expect(FormatUtil.formatIssueRoomName(SIMPLE_ISSUE)).to.equal(
|
||||
"evilcorp/lab#123: A simple title",
|
||||
);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user