mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 21:19:13 +00:00
Merge remote-tracking branch 'origin/master' into hs/github-discussions
(sorry I messed this one up)
This commit is contained in:
commit
5bde8ca6fe
@ -1,4 +1,10 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
lib/
|
lib/
|
||||||
|
tests/
|
||||||
config.yml
|
config.yml
|
||||||
public/
|
public/
|
||||||
|
.github/
|
||||||
|
logging.txt
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
*.pem
|
||||||
|
registration.yml
|
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 12
|
node-version: 12
|
||||||
- run: yarn
|
- run: yarn
|
||||||
- run: node lib/Config/Defaults.js > expected-config.sample.yml
|
- run: node lib/Config/Defaults.js --config > expected-config.sample.yml
|
||||||
- run: cmp --silent config.sample.yml expected-config.sample.yml
|
- run: cmp --silent config.sample.yml expected-config.sample.yml
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -11,8 +11,9 @@ RUN yarn
|
|||||||
FROM node:12-alpine
|
FROM node:12-alpine
|
||||||
|
|
||||||
COPY --from=builder /src/lib/ /bin/matrix-github/
|
COPY --from=builder /src/lib/ /bin/matrix-github/
|
||||||
COPY --from=builder /src/public/ /bin/matrix-github/
|
COPY --from=builder /src/public/ /bin/matrix-github/public/
|
||||||
COPY --from=builder /src/package*.json /bin/matrix-github/
|
COPY --from=builder /src/package.json /bin/matrix-github/
|
||||||
|
COPY --from=builder /src/yarn.lock /bin/matrix-github/
|
||||||
WORKDIR /bin/matrix-github
|
WORKDIR /bin/matrix-github
|
||||||
RUN yarn --production
|
RUN yarn --production
|
||||||
|
|
||||||
|
68
package.json
68
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "matrix-github",
|
"name": "matrix-github",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"description": "A bridge that displays GitHub issues/PRs as rooms.",
|
"description": "A bridge that displays GitHub issues/PRs as rooms.",
|
||||||
"main": "lib/app.js",
|
"main": "lib/app.js",
|
||||||
"repository": "https://github.com/Half-Shot/matrix-github",
|
"repository": "https://github.com/Half-Shot/matrix-github",
|
||||||
@ -19,56 +19,56 @@
|
|||||||
"start:matrixsender": "node --require source-map-support/register lib/App/MatrixSenderApp.js",
|
"start:matrixsender": "node --require source-map-support/register lib/App/MatrixSenderApp.js",
|
||||||
"test": "mocha -r ts-node/register tests/*.ts",
|
"test": "mocha -r ts-node/register tests/*.ts",
|
||||||
"lint": "eslint -c .eslintrc.js src/**/*.ts",
|
"lint": "eslint -c .eslintrc.js src/**/*.ts",
|
||||||
"generate-default-config": "node lib/Config/Defaults.js > config.sample.yml"
|
"generate-default-config": "node lib/Config/Defaults.js --config > config.sample.yml"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/auth-app": "2.10.2",
|
"@octokit/auth-app": "^3.3.0",
|
||||||
"@octokit/auth-token": "^2.4.4",
|
"@octokit/auth-token": "^2.4.5",
|
||||||
"@octokit/rest": "18.0.9",
|
"@octokit/rest": "^18.5.2",
|
||||||
|
"@octokit/webhooks": "^9.1.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"fontsource-open-sans": "^3.1.5",
|
"ioredis": "^4.26.0",
|
||||||
"ioredis": "^4.19.2",
|
"markdown-it": "^12.0.4",
|
||||||
"markdown-it": "^12.0.2",
|
"matrix-bot-sdk": "^0.5.17",
|
||||||
"matrix-bot-sdk": "^0.5.8",
|
"matrix-widget-api": "^0.1.0-beta.13",
|
||||||
"matrix-widget-api": "^0.1.0-beta.10",
|
"micromatch": "^4.0.4",
|
||||||
"micromatch": "^4.0.2",
|
"mime": "^2.5.2",
|
||||||
"mime": "^2.4.6",
|
|
||||||
"mini.css": "^3.0.1",
|
|
||||||
"mocha": "^8.2.1",
|
"mocha": "^8.2.1",
|
||||||
"node-emoji": "^1.10.0",
|
"node-emoji": "^1.10.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"source-map-support": "^0.5.19",
|
"source-map-support": "^0.5.19",
|
||||||
"string-argv": "v0.3.1",
|
"string-argv": "^0.3.1",
|
||||||
"uuid": "^8.3.1",
|
"uuid": "^8.3.2",
|
||||||
"winston": "^3.3.3",
|
"winston": "^3.3.3",
|
||||||
"yaml": "^2.0.0-1"
|
"yaml": "^1.10.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@prefresh/snowpack": "^2.2.0",
|
"@fontsource/open-sans": "^4.2.2",
|
||||||
"@snowpack/plugin-typescript": "^1.1.1",
|
"@prefresh/snowpack": "^3.1.2",
|
||||||
"@types/chai": "^4.2.14",
|
"@snowpack/plugin-typescript": "^1.2.1",
|
||||||
"@types/cors": "^2.8.9",
|
"@types/chai": "^4.2.16",
|
||||||
"@types/express": "^4.17.9",
|
"@types/cors": "^2.8.10",
|
||||||
"@types/ioredis": "^4.17.8",
|
"@types/express": "^4.17.11",
|
||||||
"@types/markdown-it": "^10.0.3",
|
"@types/ioredis": "^4.22.3",
|
||||||
|
"@types/markdown-it": "^12.0.1",
|
||||||
"@types/micromatch": "^4.0.1",
|
"@types/micromatch": "^4.0.1",
|
||||||
"@types/mime": "^2.0.3",
|
"@types/mime": "^2.0.3",
|
||||||
"@types/mocha": "^8.0.4",
|
"@types/mocha": "^8.2.2",
|
||||||
"@types/node": "^12",
|
"@types/node": "^12",
|
||||||
"@types/node-emoji": "^1.8.1",
|
"@types/node-emoji": "^1.8.1",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.8.1",
|
"@typescript-eslint/eslint-plugin": "^4.21.0",
|
||||||
"@typescript-eslint/parser": "^4.8.1",
|
"@typescript-eslint/parser": "^4.21.0",
|
||||||
"autoprefixer": "^10.1.0",
|
"chai": "^4.3.4",
|
||||||
"chai": "^4.2.0",
|
"eslint": "^7.24.0",
|
||||||
"eslint": "^7.14.0",
|
"eslint-plugin-mocha": "^8.1.0",
|
||||||
"eslint-plugin-mocha": "^8.0.0",
|
"mini.css": "^3.0.1",
|
||||||
"preact": "^10.5.7",
|
"preact": "^10.5.13",
|
||||||
"snowpack": "^2.18.3",
|
"snowpack": "^3.2.2",
|
||||||
"tailwind": "^4.0.0",
|
"tailwind": "^4.0.0",
|
||||||
"ts-node": "^9.0.0",
|
"ts-node": "^9.1.1",
|
||||||
"typescript": "^4.1.2"
|
"typescript": "^4.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,25 +7,12 @@ module.exports = {
|
|||||||
'@prefresh/snowpack',
|
'@prefresh/snowpack',
|
||||||
['@snowpack/plugin-typescript', '--project tsconfig.web.json'],
|
['@snowpack/plugin-typescript', '--project tsconfig.web.json'],
|
||||||
],
|
],
|
||||||
install: [
|
packageOptions: {
|
||||||
/* ... */
|
|
||||||
],
|
|
||||||
installOptions: {
|
|
||||||
installTypes: true,
|
installTypes: true,
|
||||||
polyfillNode: true,
|
polyfillNode: true,
|
||||||
},
|
},
|
||||||
devOptions: {
|
|
||||||
/* ... */
|
|
||||||
},
|
|
||||||
buildOptions: {
|
buildOptions: {
|
||||||
out: 'public'
|
out: 'public'
|
||||||
/* ... */
|
|
||||||
},
|
|
||||||
proxy: {
|
|
||||||
/* ... */
|
|
||||||
},
|
|
||||||
alias: {
|
|
||||||
/* ... */
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
119
src/AdminRoom.ts
119
src/AdminRoom.ts
@ -14,8 +14,13 @@ import { GitLabClient } from "./Gitlab/Client";
|
|||||||
import { GetUserResponse } from "./Gitlab/Types";
|
import { GetUserResponse } from "./Gitlab/Types";
|
||||||
import { GithubGraphQLClient, GithubInstance } from "./Github/GithubInstance";
|
import { GithubGraphQLClient, GithubInstance } from "./Github/GithubInstance";
|
||||||
import { MatrixMessageContent } from "./MatrixEvent";
|
import { MatrixMessageContent } from "./MatrixEvent";
|
||||||
import { ProjectsListForUserResponseData, ProjectsListForRepoResponseData } from "@octokit/types";
|
|
||||||
import { BridgeRoomState, BridgeRoomStateGitHub } from "./Widgets/BridgeWidgetInterface";
|
import { BridgeRoomState, BridgeRoomStateGitHub } from "./Widgets/BridgeWidgetInterface";
|
||||||
|
import { Endpoints } from "@octokit/types";
|
||||||
|
import { ProjectsListResponseData } from "./Github/Types";
|
||||||
|
import { NotifFilter, NotificationFilterStateContent } from "./NotificationFilters";
|
||||||
|
|
||||||
|
type ProjectsListForRepoResponseData = Endpoints["GET /repos/{owner}/{repo}/projects"]["response"];
|
||||||
|
type ProjectsListForUserResponseData = Endpoints["GET /users/{username}/projects"]["response"];
|
||||||
|
|
||||||
|
|
||||||
const md = new markdown();
|
const md = new markdown();
|
||||||
@ -41,25 +46,32 @@ export interface AdminAccountData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdminRoom extends EventEmitter {
|
export class AdminRoom extends EventEmitter {
|
||||||
public static helpMessage: MatrixMessageContent;
|
public static helpMessage: MatrixMessageContent;
|
||||||
private widgetAccessToken = `abcdef`;
|
private widgetAccessToken = `abcdef`;
|
||||||
static botCommands: BotCommands;
|
static botCommands: BotCommands;
|
||||||
|
|
||||||
private pendingOAuthState: string|null = null;
|
private pendingOAuthState: string|null = null;
|
||||||
|
public readonly notifFilter: NotifFilter;
|
||||||
|
|
||||||
constructor(public readonly roomId: string,
|
constructor(public readonly roomId: string,
|
||||||
public readonly data: AdminAccountData,
|
private data: AdminAccountData,
|
||||||
|
notifContent: NotificationFilterStateContent,
|
||||||
private botIntent: Intent,
|
private botIntent: Intent,
|
||||||
private tokenStore: UserTokenStore,
|
private tokenStore: UserTokenStore,
|
||||||
private config: BridgeConfig) {
|
private config: BridgeConfig) {
|
||||||
super();
|
super();
|
||||||
|
this.notifFilter = new NotifFilter(notifContent);
|
||||||
// TODO: Move this
|
// TODO: Move this
|
||||||
this.backfillAccessToken();
|
this.backfillAccessToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get accountData() {
|
||||||
|
return {...this.data};
|
||||||
|
}
|
||||||
|
|
||||||
public get userId() {
|
public get userId() {
|
||||||
return this.data.admin_user;
|
return this.data.admin_user;
|
||||||
}
|
}
|
||||||
@ -83,7 +95,7 @@ export class AdminRoom extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public notificationsParticipating(type: string) {
|
public notificationsParticipating(type: "github"|"gitlab") {
|
||||||
if (type !== "github") {
|
if (type !== "github") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -191,9 +203,8 @@ export class AdminRoom extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@botCommand("github notifications toggle", "Toggle enabling/disabling GitHub notifications in this room")
|
@botCommand("github notifications toggle", "Toggle enabling/disabling GitHub notifications in this room")
|
||||||
// @ts-ignore - property is used
|
public async setGitHubNotificationsStateToggle() {
|
||||||
private async setGitHubNotificationsStateToggle() {
|
const newData = await this.saveAccountData((data) => {
|
||||||
const data = await this.saveAccountData((data) => {
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
github: {
|
github: {
|
||||||
@ -204,29 +215,44 @@ export class AdminRoom extends EventEmitter {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await this.sendNotice(`${data.github?.notifications?.enabled ? "En" : "Dis"}abled GitHub notifcations`);
|
await this.sendNotice(`${newData.github?.notifications?.enabled ? "En" : "Dis"}abled GitHub notifcations`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@botCommand("github notifications filter participating", "Toggle enabling/disabling GitHub notifications in this room")
|
@botCommand("github notifications filter participating", "Toggle enabling/disabling GitHub notifications in this room")
|
||||||
// @ts-ignore - property is used
|
// @ts-ignore - property is used
|
||||||
private async setGitHubNotificationsStateParticipating() {
|
private async setGitHubNotificationsStateParticipating() {
|
||||||
const data = await this.saveAccountData((data) => {
|
const newData = await this.saveAccountData((data) => {
|
||||||
if (!data.github?.notifications?.enabled) {
|
if (!data.github?.notifications?.enabled) {
|
||||||
throw Error('Notifications are not enabled')
|
throw Error('Notifications are not enabled')
|
||||||
}
|
}
|
||||||
|
const oldState = data.github?.notifications?.participating ?? false;
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
github: {
|
github: {
|
||||||
notifications: {
|
notifications: {
|
||||||
participating: !(data.github?.notifications?.participating ?? false),
|
participating: !oldState,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await this.sendNotice(`${data.github?.notifications?.enabled ? "" : "Not"} filtering for events you are participating in`);
|
console.log(newData);
|
||||||
|
if (newData.github?.notifications?.participating) {
|
||||||
|
return this.sendNotice(`Filtering for events you are participating in`);
|
||||||
|
}
|
||||||
|
return this.sendNotice(`Showing all events`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@botCommand("github notifications", "Show the current notification settings")
|
||||||
|
// @ts-ignore - property is used
|
||||||
|
private async getGitHubNotificationsState() {
|
||||||
|
if (!this.notificationsEnabled("github")) {
|
||||||
|
return this.sendNotice(`Notifications are disabled`);
|
||||||
|
}
|
||||||
|
return this.sendNotice(`Notifications are enabled, ${this.notificationsParticipating("github") ? "Showing only events you are particiapting in" : "Showing all events"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@botCommand("github project list-for-user", "List GitHub projects for a user", [], ['user', 'repo'])
|
@botCommand("github project list-for-user", "List GitHub projects for a user", [], ['user', 'repo'])
|
||||||
// @ts-ignore - property is used
|
// @ts-ignore - property is used
|
||||||
private async listProjects(username?: string, repo?: string) {
|
private async listProjects(username?: string, repo?: string) {
|
||||||
@ -240,10 +266,11 @@ export class AdminRoom extends EventEmitter {
|
|||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
const me = await octokit.users.getAuthenticated();
|
const me = await octokit.users.getAuthenticated();
|
||||||
username = me.data.name;
|
// TODO: Fix
|
||||||
|
username = me.data.name!;
|
||||||
}
|
}
|
||||||
|
|
||||||
let res: ProjectsListForUserResponseData|ProjectsListForRepoResponseData;
|
let res: ProjectsListResponseData;
|
||||||
try {
|
try {
|
||||||
if (repo) {
|
if (repo) {
|
||||||
res = (await octokit.projects.listForRepo({
|
res = (await octokit.projects.listForRepo({
|
||||||
@ -259,7 +286,7 @@ export class AdminRoom extends EventEmitter {
|
|||||||
return this.sendNotice(`Failed to fetch projects due to an error. See logs for details`);
|
return this.sendNotice(`Failed to fetch projects due to an error. See logs for details`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = `Projects for ${username}:\n` + res.map(r => ` - ${FormatUtil.projectListing([r])}\n`).join("\n");
|
const content = `Projects for ${username}:\n${FormatUtil.projectListing(res)}\n`;
|
||||||
return this.botIntent.sendEvent(this.roomId,{
|
return this.botIntent.sendEvent(this.roomId,{
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: content,
|
body: content,
|
||||||
@ -285,11 +312,11 @@ export class AdminRoom extends EventEmitter {
|
|||||||
res = (await octokit.projects.listForRepo({
|
res = (await octokit.projects.listForRepo({
|
||||||
repo,
|
repo,
|
||||||
owner: org,
|
owner: org,
|
||||||
})).data;
|
}));
|
||||||
}
|
}
|
||||||
res = (await octokit.projects.listForOrg({
|
res = (await octokit.projects.listForOrg({
|
||||||
org,
|
org,
|
||||||
})).data;
|
}));
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
if (ex.status === 404) {
|
if (ex.status === 404) {
|
||||||
return this.sendNotice('Not found');
|
return this.sendNotice('Not found');
|
||||||
@ -298,7 +325,7 @@ export class AdminRoom extends EventEmitter {
|
|||||||
return this.sendNotice(`Failed to fetch projects due to an error. See logs for details`);
|
return this.sendNotice(`Failed to fetch projects due to an error. See logs for details`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = `Projects for ${org}:\n` + res.map(r => ` - ${FormatUtil.projectListing([r])}\n`).join("\n");
|
const content = `Projects for ${org}:\n` + res.data.map(r => ` - ${FormatUtil.projectListing([r])}\n`).join("\n");
|
||||||
return this.botIntent.sendEvent(this.roomId,{
|
return this.botIntent.sendEvent(this.roomId,{
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: content,
|
body: content,
|
||||||
@ -428,8 +455,7 @@ export class AdminRoom extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@botCommand("gitlab notifications toggle", "Toggle enabling/disabling GitHub notifications in this room", ["instanceName"])
|
@botCommand("gitlab notifications toggle", "Toggle enabling/disabling GitHub notifications in this room", ["instanceName"])
|
||||||
// @ts-ignore - property is used
|
public async setGitLabNotificationsStateToggle(instanceName: string) {
|
||||||
private async setGitLabNotificationsStateToggle(instanceName: string) {
|
|
||||||
if (!this.config.gitlab) {
|
if (!this.config.gitlab) {
|
||||||
return this.sendNotice("The bridge is not configured with GitLab support");
|
return this.sendNotice("The bridge is not configured with GitLab support");
|
||||||
}
|
}
|
||||||
@ -457,7 +483,55 @@ export class AdminRoom extends EventEmitter {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await this.sendNotice(`${newValue ? "En" : "Dis"}abled GitLab notifications for ${instanceName}`);
|
return this.sendNotice(`${newValue ? "En" : "Dis"}abled GitLab notifications for ${instanceName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
@botCommand("filters list", "List your saved filters")
|
||||||
|
public async getFilters() {
|
||||||
|
if (this.notifFilter.empty) {
|
||||||
|
return this.sendNotice("You do not currently have any filters");
|
||||||
|
}
|
||||||
|
const filterText = Object.entries(this.notifFilter.filters).map(([name, value]) => {
|
||||||
|
const userText = value.users.length ? `users: ${value.users.join("|")}` : '';
|
||||||
|
const reposText = value.repos.length ? `users: ${value.repos.join("|")}` : '';
|
||||||
|
const orgsText = value.orgs.length ? `users: ${value.orgs.join("|")}` : '';
|
||||||
|
return `${name}: ${userText} ${reposText} ${orgsText}`
|
||||||
|
}).join("\n");
|
||||||
|
const enabledForInvites = [...this.notifFilter.forInvites].join(', ');
|
||||||
|
const enabledForNotifications = [...this.notifFilter.forNotifications].join(', ');
|
||||||
|
return this.sendNotice(`Your filters:\n ${filterText}\nEnabled for automatic room invites: ${enabledForInvites}\nEnabled for notifications: ${enabledForNotifications}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
@botCommand("filters set", "Create (or update) a filter. You can use 'orgs:', 'users:' or 'repos:' as filter parameters.", ["name", "...parameters"])
|
||||||
|
public async setFilter(name: string, ...parameters: string[]) {
|
||||||
|
const orgs = parameters.filter(param => param.toLowerCase().startsWith("orgs:")).map(param => param.toLowerCase().substring("orgs:".length).split(",")).flat();
|
||||||
|
const users = parameters.filter(param => param.toLowerCase().startsWith("users:")).map(param => param.toLowerCase().substring("users:".length).split(",")).flat();
|
||||||
|
const repos = parameters.filter(param => param.toLowerCase().startsWith("repos:")).map(param => param.toLowerCase().substring("repos:".length).split(",")).flat();
|
||||||
|
if (orgs.length + users.length + repos.length === 0) {
|
||||||
|
return this.sendNotice("You must specify some filter options like 'orgs:matrix-org,half-shot', 'users:Half-Shot' or 'repos:matrix-github'");
|
||||||
|
}
|
||||||
|
this.notifFilter.setFilter(name, {
|
||||||
|
orgs,
|
||||||
|
users,
|
||||||
|
repos,
|
||||||
|
});
|
||||||
|
await this.botIntent.underlyingClient.sendStateEvent(this.roomId, NotifFilter.StateType, "", this.notifFilter.getStateContent());
|
||||||
|
return this.sendNotice(`Stored new filter "${name}". You can now apply the filter by saying 'filters notifications toggle $name'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
@botCommand("filters notifications toggle", "Apply a filter as a whitelist to your notifications", ["name"])
|
||||||
|
public async setFiltersNotificationsToggle(name: string) {
|
||||||
|
if (!this.notifFilter.filters[name]) {
|
||||||
|
return this.sendNotice(`Filter "${name}" doesn't exist'`);
|
||||||
|
}
|
||||||
|
if (this.notifFilter.forNotifications.has(name)) {
|
||||||
|
this.notifFilter.forNotifications.delete(name);
|
||||||
|
await this.sendNotice(`Filter "${name}" disabled for notifications`);
|
||||||
|
} else {
|
||||||
|
this.notifFilter.forNotifications.add(name);
|
||||||
|
await this.sendNotice(`Filter "${name}" enabled for notifications`);
|
||||||
|
}
|
||||||
|
return this.botIntent.underlyingClient.sendStateEvent(this.roomId, NotifFilter.StateType, "", this.notifFilter.getStateContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async saveAccountData(updateFn: (record: AdminAccountData) => AdminAccountData) {
|
private async saveAccountData(updateFn: (record: AdminAccountData) => AdminAccountData) {
|
||||||
@ -467,6 +541,7 @@ export class AdminRoom extends EventEmitter {
|
|||||||
const newData = updateFn(oldData);
|
const newData = updateFn(oldData);
|
||||||
await this.botIntent.underlyingClient.setRoomAccountData(BRIDGE_ROOM_TYPE, this.roomId, newData);
|
await this.botIntent.underlyingClient.setRoomAccountData(BRIDGE_ROOM_TYPE, this.roomId, newData);
|
||||||
this.emit("settings.changed", this, oldData, newData);
|
this.emit("settings.changed", this, oldData, newData);
|
||||||
|
this.data = newData;
|
||||||
return newData;
|
return newData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,6 +635,10 @@ export class AdminRoom extends EventEmitter {
|
|||||||
log.info(`No widget access token for ${this.roomId}`);
|
log.info(`No widget access token for ${this.roomId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public toString() {
|
||||||
|
return `AdminRoom(${this.roomId}, ${this.userId})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
@ -6,7 +6,7 @@ import { MatrixMessageContent, MatrixEvent } from "./MatrixEvent";
|
|||||||
import LogWrapper from "./LogWrapper";
|
import LogWrapper from "./LogWrapper";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { FormatUtil } from "./FormatUtil";
|
import { FormatUtil } from "./FormatUtil";
|
||||||
import { IssuesGetCommentResponseData, ReposGetResponseData, IssuesGetResponseData } from "@octokit/types";
|
import { IssuesGetCommentResponseData, ReposGetResponseData, IssuesGetResponseData } from "./Github/Types"
|
||||||
import { IGitLabWebhookNoteEvent } from "./Gitlab/WebhookTypes";
|
import { IGitLabWebhookNoteEvent } from "./Gitlab/WebhookTypes";
|
||||||
|
|
||||||
const REGEX_MENTION = /(^|\s)(@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})(\s|$)/ig;
|
const REGEX_MENTION = /(^|\s)(@[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})(\s|$)/ig;
|
||||||
@ -59,7 +59,10 @@ export class CommentProcessor {
|
|||||||
|
|
||||||
public async getEventBodyForGitHubComment(comment: IssuesGetCommentResponseData,
|
public async getEventBodyForGitHubComment(comment: IssuesGetCommentResponseData,
|
||||||
repo?: ReposGetResponseData,
|
repo?: ReposGetResponseData,
|
||||||
issue?: IssuesGetResponseData): Promise<IMatrixCommentEvent> {
|
issue?: IssuesGetResponseData): Promise<IMatrixCommentEvent|undefined> {
|
||||||
|
if (!comment.body) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
let body = comment.body;
|
let body = comment.body;
|
||||||
body = this.replaceMentions(body);
|
body = this.replaceMentions(body);
|
||||||
body = await this.replaceImages(body, true);
|
body = await this.replaceImages(body, true);
|
||||||
|
@ -6,6 +6,7 @@ export function configKey(comment?: string, optional = false) {
|
|||||||
return Reflect.metadata(configKeyMetadataKey, [comment, optional]);
|
return Reflect.metadata(configKeyMetadataKey, [comment, optional]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function getConfigKeyMetadata(target: any, propertyKey: string): [string, boolean] {
|
export function getConfigKeyMetadata(target: any, propertyKey: string): [string, boolean] {
|
||||||
return Reflect.getMetadata(configKeyMetadataKey, target, propertyKey);
|
return Reflect.getMetadata(configKeyMetadataKey, target, propertyKey);
|
||||||
}
|
}
|
@ -65,11 +65,11 @@ function renderSection(doc: YAML.Document, obj: Record<string, unknown>, parentN
|
|||||||
const entries = Object.entries(obj);
|
const entries = Object.entries(obj);
|
||||||
entries.forEach(([key, value]) => {
|
entries.forEach(([key, value]) => {
|
||||||
let newNode: Node;
|
let newNode: Node;
|
||||||
if (typeof value === "object") {
|
if (typeof value === "object" && !Array.isArray(value)) {
|
||||||
newNode = doc.createNode({});
|
newNode = YAML.createNode({});
|
||||||
renderSection(doc, value as any, newNode as YAMLSeq);
|
renderSection(doc, value as Record<string, unknown>, newNode as YAMLSeq);
|
||||||
} else {
|
} else {
|
||||||
newNode = doc.createNode(value);
|
newNode = YAML.createNode(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = getConfigKeyMetadata(obj, key);
|
const metadata = getConfigKeyMetadata(obj, key);
|
||||||
@ -86,10 +86,10 @@ function renderSection(doc: YAML.Document, obj: Record<string, unknown>, parentN
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderDefaultConfig() {
|
function renderDefaultConfig() {
|
||||||
const doc = new YAML.Document({});
|
const doc = new YAML.Document();
|
||||||
|
doc.contents = YAML.createNode({});
|
||||||
doc.commentBefore = ' This is an example configuration file';
|
doc.commentBefore = ' This is an example configuration file';
|
||||||
// Needed because the entries syntax below would not work otherwise
|
// Needed because the entries syntax below would not work otherwise
|
||||||
//const typeLessDefaultConfig = DefaultConfig as any;
|
|
||||||
renderSection(doc, DefaultConfig as any);
|
renderSection(doc, DefaultConfig as any);
|
||||||
return doc.toString();
|
return doc.toString();
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,10 @@ import { Octokit } from "@octokit/rest";
|
|||||||
import { MessageSenderClient } from "../MatrixSender";
|
import { MessageSenderClient } from "../MatrixSender";
|
||||||
import { getIntentForUser } from "../IntentUtils";
|
import { getIntentForUser } from "../IntentUtils";
|
||||||
import { FormatUtil } from "../FormatUtil";
|
import { FormatUtil } from "../FormatUtil";
|
||||||
import { IGitHubWebhookEvent } from "../GithubWebhooks";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { GithubInstance } from "../Github/GithubInstance";
|
import { GithubInstance } from "../Github/GithubInstance";
|
||||||
import { IssuesGetResponseData } from "@octokit/types";
|
import { IssuesGetCommentResponseData, IssuesGetResponseData, ReposGetResponseData} from "../Github/Types";
|
||||||
|
import { IssuesEditedEvent, IssueCommentCreatedEvent } from "@octokit/webhooks-types";
|
||||||
|
|
||||||
export interface GitHubIssueConnectionState {
|
export interface GitHubIssueConnectionState {
|
||||||
org: string;
|
org: string;
|
||||||
@ -151,7 +151,20 @@ export class GitHubIssueConnection implements IConnection {
|
|||||||
return this.state.repo;
|
return this.state.repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onCommentCreated(event: IGitHubWebhookEvent, updateState = true) {
|
public async onIssueCommentCreated(event: IssueCommentCreatedEvent) {
|
||||||
|
return this.onCommentCreated({
|
||||||
|
// TODO: Fix types,
|
||||||
|
comment: event.comment as any,
|
||||||
|
action: event.action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onCommentCreated(event: {
|
||||||
|
comment: IssuesGetCommentResponseData,
|
||||||
|
action: string,
|
||||||
|
repository?: ReposGetResponseData,
|
||||||
|
issue?: IssuesGetResponseData,
|
||||||
|
}, updateState = true) {
|
||||||
const comment = event.comment;
|
const comment = event.comment;
|
||||||
if (!comment || !comment.user) {
|
if (!comment || !comment.user) {
|
||||||
throw Error('Comment undefined');
|
throw Error('Comment undefined');
|
||||||
@ -168,8 +181,10 @@ export class GitHubIssueConnection implements IConnection {
|
|||||||
avatarUrl: comment.user.avatar_url,
|
avatarUrl: comment.user.avatar_url,
|
||||||
}, this.as);
|
}, this.as);
|
||||||
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
|
||||||
await this.messageClient.sendMatrixMessage(this.roomId, matrixEvent, "m.room.message", commentIntent.userId);
|
if (matrixEvent) {
|
||||||
|
await this.messageClient.sendMatrixMessage(this.roomId, matrixEvent, "m.room.message", commentIntent.userId);
|
||||||
|
}
|
||||||
if (!updateState) {
|
if (!updateState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -182,7 +197,7 @@ export class GitHubIssueConnection implements IConnection {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async syncIssueState() {
|
public async syncIssueState() {
|
||||||
log.debug("Syncing issue state for", this.roomId);
|
log.debug("Syncing issue state for", this.roomId);
|
||||||
const issue = await this.github.octokit.issues.get({
|
const issue = await this.github.octokit.issues.get({
|
||||||
owner: this.state.org,
|
owner: this.state.org,
|
||||||
@ -193,8 +208,9 @@ export class GitHubIssueConnection implements IConnection {
|
|||||||
if (this.state.comments_processed === -1) {
|
if (this.state.comments_processed === -1) {
|
||||||
// This has a side effect of creating a profile for the user.
|
// This has a side effect of creating a profile for the user.
|
||||||
const creator = await getIntentForUser({
|
const creator = await getIntentForUser({
|
||||||
login: issue.data.user.login,
|
// TODO: Fix
|
||||||
avatarUrl: issue.data.user.avatar_url
|
login: issue.data.user?.login as string,
|
||||||
|
avatarUrl: issue.data.user?.avatar_url || undefined
|
||||||
}, this.as);
|
}, this.as);
|
||||||
// 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) {
|
||||||
@ -232,11 +248,12 @@ export class GitHubIssueConnection implements IConnection {
|
|||||||
|
|
||||||
if (this.state.state !== issue.data.state) {
|
if (this.state.state !== issue.data.state) {
|
||||||
if (issue.data.state === "closed") {
|
if (issue.data.state === "closed") {
|
||||||
const closedUserId = this.as.getUserIdForSuffix(issue.data.closed_by.login);
|
// TODO: Fix
|
||||||
|
const closedUserId = this.as.getUserIdForSuffix(issue.data.closed_by?.login as string);
|
||||||
await this.messageClient.sendMatrixMessage(this.roomId, {
|
await this.messageClient.sendMatrixMessage(this.roomId, {
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: `closed the ${issue.data.pull_request ? "pull request" : "issue"} at ${issue.data.closed_at}`,
|
body: `closed the ${issue.data.pull_request ? "pull request" : "issue"} at ${issue.data.closed_at}`,
|
||||||
external_url: issue.data.closed_by.html_url,
|
external_url: issue.data.closed_by?.html_url,
|
||||||
}, "m.room.message", closedUserId);
|
}, "m.room.message", closedUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,12 +299,13 @@ export class GitHubIssueConnection implements IConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onIssueEdited(event: IGitHubWebhookEvent) {
|
public async onIssueEdited(event: IssuesEditedEvent) {
|
||||||
if (!event.changes) {
|
if (!event.changes) {
|
||||||
log.debug("No changes given");
|
log.debug("No changes given");
|
||||||
return; // No changes made.
|
return; // No changes made.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Fix types
|
||||||
if (event.issue && event.changes.title) {
|
if (event.issue && event.changes.title) {
|
||||||
await this.as.botIntent.underlyingClient.sendStateEvent(this.roomId, "m.room.name", "", {
|
await this.as.botIntent.underlyingClient.sendStateEvent(this.roomId, "m.room.name", "", {
|
||||||
name: FormatUtil.formatIssueRoomName(event.issue),
|
name: FormatUtil.formatIssueRoomName(event.issue),
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { IConnection } from "./IConnection";
|
import { IConnection } from "./IConnection";
|
||||||
import { Appservice } from "matrix-bot-sdk";
|
import { Appservice } from "matrix-bot-sdk";
|
||||||
import LogWrapper from "../LogWrapper";
|
import LogWrapper from "../LogWrapper";
|
||||||
import { ProjectsGetResponseData } from "@octokit/types";
|
import { ProjectsGetResponseData } from "../Github/Types";
|
||||||
|
|
||||||
export interface GitHubProjectConnectionState {
|
export interface GitHubProjectConnectionState {
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
|
@ -11,9 +11,9 @@ import { MessageSenderClient } from "../MatrixSender";
|
|||||||
import { FormatUtil } from "../FormatUtil";
|
import { FormatUtil } from "../FormatUtil";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { BotCommands, handleCommand, botCommand, compileBotCommands } from "../BotCommands";
|
import { BotCommands, handleCommand, botCommand, compileBotCommands } from "../BotCommands";
|
||||||
import { IGitHubWebhookEvent } from "../GithubWebhooks";
|
import { ReposGetResponseData } from "../Github/Types";
|
||||||
import { ReposGetResponseData } from "@octokit/types";
|
import { IssuesOpenedEvent, IssuesEditedEvent } from "@octokit/webhooks-types";
|
||||||
|
import emoji from "node-emoji";
|
||||||
const log = new LogWrapper("GitHubRepoConnection");
|
const log = new LogWrapper("GitHubRepoConnection");
|
||||||
const md = new markdown();
|
const md = new markdown();
|
||||||
|
|
||||||
@ -152,11 +152,11 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get org() {
|
public get org() {
|
||||||
return this.state.org;
|
return this.state.org.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get repo() {
|
public get repo() {
|
||||||
return this.state.repo;
|
return this.state.repo.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public isInterestedInStateEvent() {
|
public isInterestedInStateEvent() {
|
||||||
@ -255,7 +255,7 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onIssueCreated(event: IGitHubWebhookEvent) {
|
public async onIssueCreated(event: IssuesOpenedEvent) {
|
||||||
log.info(`onIssueCreated ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`);
|
log.info(`onIssueCreated ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`);
|
||||||
if (!event.issue) {
|
if (!event.issue) {
|
||||||
throw Error('No issue content!');
|
throw Error('No issue content!');
|
||||||
@ -264,23 +264,27 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
throw Error('No repository content!');
|
throw Error('No repository content!');
|
||||||
}
|
}
|
||||||
const orgRepoName = event.issue.repository_url.substr("https://api.github.com/repos/".length);
|
const orgRepoName = event.issue.repository_url.substr("https://api.github.com/repos/".length);
|
||||||
const content = `New issue created [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`;
|
|
||||||
const labelsHtml = event.issue.labels.map((label: {color: string, name: string, description: string}) =>
|
const content = emoji.emojify(`${event.issue.user?.login} created new issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`);
|
||||||
`<span title="${label.description}" data-mx-color="#CCCCCC" data-mx-bg-color="#${label.color}">${label.name}</span>`
|
const labelsHtml = (event.issue.labels || []).map((label: {color?: string|null, name?: string, description?: string|null}|string) =>
|
||||||
|
typeof(label) === "string" ?
|
||||||
|
`<span>${label}</span>` :
|
||||||
|
`<span title="${label.description}" data-mx-color="#CCCCCC" data-mx-bg-color="#${label.color}">${label.name}</span>`
|
||||||
).join(" ") || "";
|
).join(" ") || "";
|
||||||
const labels = event.issue?.labels.map((label: {name: string}) =>
|
const labels = (event.issue?.labels || []).map((label: {name?: string}|string) =>
|
||||||
label.name
|
typeof(label) === "string" ? label : label.name
|
||||||
).join(", ") || "";
|
).join(", ") || "";
|
||||||
await this.as.botIntent.sendEvent(this.roomId, {
|
await this.as.botIntent.sendEvent(this.roomId, {
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: content + (labels.length > 0 ? ` with labels ${labels}`: ""),
|
body: content + (labels.length > 0 ? ` with labels ${labels}`: ""),
|
||||||
formatted_body: md.renderInline(content) + (labelsHtml.length > 0 ? ` with labels ${labelsHtml}`: ""),
|
formatted_body: md.renderInline(content) + (labelsHtml.length > 0 ? ` with labels ${labelsHtml}`: ""),
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
...FormatUtil.getPartialBodyForIssue(event.repository, event.issue),
|
// TODO: Fix types.
|
||||||
|
...FormatUtil.getPartialBodyForIssue(event.repository, event.issue as any),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onIssueStateChange(event: IGitHubWebhookEvent) {
|
public async onIssueStateChange(event: IssuesEditedEvent) {
|
||||||
log.info(`onIssueStateChange ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`);
|
log.info(`onIssueStateChange ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`);
|
||||||
if (!event.issue) {
|
if (!event.issue) {
|
||||||
throw Error('No issue content!');
|
throw Error('No issue content!');
|
||||||
@ -290,13 +294,14 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
}
|
}
|
||||||
if (event.issue.state === "closed" && event.sender) {
|
if (event.issue.state === "closed" && event.sender) {
|
||||||
const orgRepoName = event.issue.repository_url.substr("https://api.github.com/repos/".length);
|
const orgRepoName = event.issue.repository_url.substr("https://api.github.com/repos/".length);
|
||||||
const content = `**@${event.sender.login}** closed issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`;
|
const content = `**@${event.sender.login}** closed issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`;
|
||||||
await this.as.botIntent.sendEvent(this.roomId, {
|
await this.as.botIntent.sendEvent(this.roomId, {
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: content,
|
body: content,
|
||||||
formatted_body: md.renderInline(content),
|
formatted_body: md.renderInline(content),
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
...FormatUtil.getPartialBodyForIssue(event.repository, event.issue),
|
// TODO: Fix types
|
||||||
|
...FormatUtil.getPartialBodyForIssue(event.repository, event.issue as any),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { IConnection } from "./IConnection";
|
import { IConnection } from "./IConnection";
|
||||||
import { Appservice } from "matrix-bot-sdk";
|
import { Appservice } from "matrix-bot-sdk";
|
||||||
import { MatrixMessageContent, MatrixEvent } from "../MatrixEvent";
|
import { MatrixMessageContent, MatrixEvent } from "../MatrixEvent";
|
||||||
import markdown from "markdown-it";
|
|
||||||
import { UserTokenStore } from "../UserTokenStore";
|
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 { FormatUtil } from "../FormatUtil";
|
import { FormatUtil } from "../FormatUtil";
|
||||||
import { IGitHubWebhookEvent } from "../GithubWebhooks";
|
|
||||||
import { GitLabInstance } from "../Config/Config";
|
import { GitLabInstance } from "../Config/Config";
|
||||||
import { GetIssueResponse } from "../Gitlab/Types";
|
import { GetIssueResponse } from "../Gitlab/Types";
|
||||||
import { IGitLabWebhookNoteEvent } from "../Gitlab/WebhookTypes";
|
import { IGitLabWebhookNoteEvent } from "../Gitlab/WebhookTypes";
|
||||||
@ -23,9 +21,6 @@ export interface GitLabIssueConnectionState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const log = new LogWrapper("GitLabIssueConnection");
|
const log = new LogWrapper("GitLabIssueConnection");
|
||||||
const md = new markdown();
|
|
||||||
|
|
||||||
md.render("foo");
|
|
||||||
|
|
||||||
// interface IQueryRoomOpts {
|
// interface IQueryRoomOpts {
|
||||||
// as: Appservice;
|
// as: Appservice;
|
||||||
@ -180,19 +175,6 @@ export class GitLabIssueConnection implements IConnection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onIssueEdited(event: IGitHubWebhookEvent) {
|
|
||||||
if (!event.changes) {
|
|
||||||
log.debug("No changes given");
|
|
||||||
return; // No changes made.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.issue && event.changes.title) {
|
|
||||||
await this.as.botIntent.underlyingClient.sendStateEvent(this.roomId, "m.room.name", "", {
|
|
||||||
name: FormatUtil.formatIssueRoomName(event.issue),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async onMessageEvent(ev: MatrixEvent<MatrixMessageContent>) {
|
public async onMessageEvent(ev: MatrixEvent<MatrixMessageContent>) {
|
||||||
if (ev.content.body === '!sync') {
|
if (ev.content.body === '!sync') {
|
||||||
// Sync data.
|
// Sync data.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
||||||
import { IGitHubWebhookEvent } from "../GithubWebhooks";
|
import { IssuesOpenedEvent, IssuesEditedEvent } from "@octokit/webhooks-types";
|
||||||
|
|
||||||
export interface IConnection {
|
export interface IConnection {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@ -17,11 +17,11 @@ export interface IConnection {
|
|||||||
*/
|
*/
|
||||||
onMessageEvent?: (ev: MatrixEvent<MatrixMessageContent>) => Promise<void>;
|
onMessageEvent?: (ev: MatrixEvent<MatrixMessageContent>) => Promise<void>;
|
||||||
|
|
||||||
onIssueCreated?: (ev: IGitHubWebhookEvent) => Promise<void>;
|
onIssueCreated?: (ev: IssuesOpenedEvent) => Promise<void>;
|
||||||
|
|
||||||
onIssueStateChange?: (ev: IGitHubWebhookEvent) => Promise<void>;
|
onIssueStateChange?: (ev: IssuesEditedEvent) => Promise<void>;
|
||||||
|
|
||||||
onIssueEdited? :(event: IGitHubWebhookEvent) => Promise<void>;
|
onIssueEdited? :(event: IssuesEditedEvent) => Promise<void>;
|
||||||
|
|
||||||
isInterestedInStateEvent: (eventType: string, stateKey: string) => boolean;
|
isInterestedInStateEvent: (eventType: string, stateKey: string) => boolean;
|
||||||
|
|
||||||
|
@ -1,19 +1,30 @@
|
|||||||
import { IssuesGetCommentResponseData, IssuesGetResponseData, ProjectsListForOrgResponseData, ProjectsListForUserResponseData, ProjectsListForRepoResponseData } from "@octokit/types";
|
/* eslint-disable camelcase */
|
||||||
|
import { IssuesGetCommentResponseData, IssuesGetResponseData, ProjectsListResponseData } from './Github/Types';
|
||||||
|
import emoji from "node-emoji";
|
||||||
interface IMinimalRepository {
|
interface IMinimalRepository {
|
||||||
id: number;
|
id: number;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
html_url: string;
|
html_url: string;
|
||||||
|
description: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMinimalIssue {
|
||||||
|
html_url: string;
|
||||||
|
id: number;
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
repository_url: string;
|
||||||
|
pull_request?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FormatUtil {
|
export class FormatUtil {
|
||||||
public static formatIssueRoomName(issue: {number: number, title: string, repository_url: string}) {
|
public static formatIssueRoomName(issue: IMinimalIssue) {
|
||||||
const orgRepoName = issue.repository_url.substr("https://api.github.com/repos/".length);
|
const orgRepoName = issue.repository_url.substr("https://api.github.com/repos/".length);
|
||||||
return `${orgRepoName}#${issue.number}: ${issue.title}`;
|
return emoji.emojify(`${orgRepoName}#${issue.number}: ${issue.title}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static formatRepoRoomName(repo: {full_name: string, description: string}) {
|
public static formatRepoRoomName(repo: IMinimalRepository) {
|
||||||
return `${repo.full_name}: ${repo.description}`;
|
return emoji.emojify(repo.description ? `${repo.full_name}: ${repo.description}` : repo.full_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static formatRoomTopic(repo: {state: string, html_url: string}) {
|
public static formatRoomTopic(repo: {state: string, html_url: string}) {
|
||||||
@ -35,7 +46,7 @@ export class FormatUtil {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getPartialBodyForIssue(repo: IMinimalRepository, issue: IssuesGetResponseData) {
|
public static getPartialBodyForIssue(repo: IMinimalRepository, issue: IMinimalIssue) {
|
||||||
return {
|
return {
|
||||||
...FormatUtil.getPartialBodyForRepo(repo),
|
...FormatUtil.getPartialBodyForRepo(repo),
|
||||||
"external_url": issue.html_url,
|
"external_url": issue.html_url,
|
||||||
@ -49,9 +60,9 @@ export class FormatUtil {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static getPartialBodyForComment(comment: IssuesGetCommentResponseData,
|
public static getPartialBodyForComment(comment: {id: number, html_url: string},
|
||||||
repo?: IMinimalRepository,
|
repo?: IMinimalRepository,
|
||||||
issue?: IssuesGetResponseData) {
|
issue?: IMinimalIssue) {
|
||||||
return {
|
return {
|
||||||
...(issue && repo ? FormatUtil.getPartialBodyForIssue(repo, issue) : undefined),
|
...(issue && repo ? FormatUtil.getPartialBodyForIssue(repo, issue) : undefined),
|
||||||
"external_url": comment.html_url,
|
"external_url": comment.html_url,
|
||||||
@ -61,7 +72,11 @@ export class FormatUtil {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static projectListing(projectItem: ProjectsListForOrgResponseData|ProjectsListForUserResponseData|ProjectsListForRepoResponseData) {
|
public static projectListing(projects: ProjectsListResponseData): string {
|
||||||
return `${projectItem[0].name} (#${projectItem[0].number}) - Project ID: ${projectItem[0].id}`
|
let f = '';
|
||||||
|
for (const projectItem of projects) {
|
||||||
|
f += ` - ${projectItem.name} (#${projectItem.number}) - Project ID: ${projectItem.id}`;
|
||||||
|
}
|
||||||
|
return f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,19 @@
|
|||||||
import { IssuesGetResponseData, IssuesGetCommentResponseData, PullsListReviewsResponseData, ReposGetResponseData, PullsListRequestedReviewersResponseData } from "@octokit/types";
|
import { Endpoints } from "@octokit/types";
|
||||||
|
|
||||||
|
export type IssuesGetResponseData = Endpoints["GET /repos/{owner}/{repo}/issues/{issue_number}"]["response"]["data"];
|
||||||
|
export type IssuesGetCommentResponseData = Endpoints["GET /repos/{owner}/{repo}/issues/comments/{comment_id}"]["response"]["data"];
|
||||||
|
export type PullsListReviewsResponseData = Endpoints["GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews"]["response"]["data"];
|
||||||
|
export type PullsListRequestedReviewersResponseData = Endpoints["GET /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers"]["response"]["data"];
|
||||||
|
export type ReposGetResponseData = Endpoints["GET /repos/{owner}/{repo}"]["response"]["data"];
|
||||||
|
export type ProjectsGetResponseData = Endpoints["GET /projects/{project_id}"]["response"]["data"];
|
||||||
|
export type ProjectsListForTeamsResponseData = Endpoints["GET /teams/{team_id}/projects"]["response"]["data"];
|
||||||
|
export type ProjectsListForRepoResponseData = Endpoints["GET /repos/{owner}/{repo}/projects"]["response"]["data"];
|
||||||
|
export type ProjectsListForUserResponseData = Endpoints["GET /users/{username}/projects"]["response"]["data"];
|
||||||
|
export type ProjectsListResponseData = ProjectsListForTeamsResponseData|ProjectsListForRepoResponseData|ProjectsListForUserResponseData;
|
||||||
|
export type IssuesListAssigneesResponseData = Endpoints["GET /repos/{owner}/{repo}/issues"]["response"]["data"];
|
||||||
|
export type PullsGetResponseData = Endpoints["GET /repos/{owner}/{repo}/pulls"]["response"]["data"];
|
||||||
|
export type PullGetResponseData = Endpoints["GET /repos/{owner}/{repo}/pulls/{pull_number}"]["response"]["data"];
|
||||||
|
export type DiscussionDataType = Endpoints["GET /repos/{owner}/{repo}/pulls/{pull_number}"]["response"]["data"];
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
export interface GitHubUserNotification {
|
export interface GitHubUserNotification {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { Appservice, IAppserviceRegistration, RichRepliesPreprocessor, IRichReplyMetadata } from "matrix-bot-sdk";
|
import { Appservice, IAppserviceRegistration, RichRepliesPreprocessor, IRichReplyMetadata, StateEvent } from "matrix-bot-sdk";
|
||||||
import { ProjectsGetResponseData } from "@octokit/types";
|
|
||||||
import { BridgeConfig, GitLabInstance } from "./Config/Config";
|
import { BridgeConfig, GitLabInstance } from "./Config/Config";
|
||||||
import { IGitHubWebhookEvent, IOAuthRequest, IOAuthTokens, NotificationsEnableEvent,
|
import { IOAuthRequest, IOAuthTokens, NotificationsEnableEvent, NotificationsDisableEvent,} from "./GithubWebhooks";
|
||||||
NotificationsDisableEvent } from "./GithubWebhooks";
|
|
||||||
import { CommentProcessor } from "./CommentProcessor";
|
import { CommentProcessor } from "./CommentProcessor";
|
||||||
import { MessageQueue, createMessageQueue } from "./MessageQueue/MessageQueue";
|
import { MessageQueue, createMessageQueue } from "./MessageQueue/MessageQueue";
|
||||||
import { AdminRoom, BRIDGE_ROOM_TYPE, AdminAccountData } from "./AdminRoom";
|
import { AdminRoom, BRIDGE_ROOM_TYPE, AdminAccountData } from "./AdminRoom";
|
||||||
@ -29,6 +27,9 @@ import { GitLabClient } from "./Gitlab/Client";
|
|||||||
import { BridgeWidgetApi } from "./Widgets/BridgeWidgetApi";
|
import { BridgeWidgetApi } from "./Widgets/BridgeWidgetApi";
|
||||||
import { GitHubDiscussionConnection } from "./Connections/GithubDiscussion";
|
import { GitHubDiscussionConnection } from "./Connections/GithubDiscussion";
|
||||||
import { Discussion } from "./Github/Discussion";
|
import { Discussion } from "./Github/Discussion";
|
||||||
|
import { ProjectsGetResponseData } from "./Github/Types";
|
||||||
|
import { NotifFilter, NotificationFilterStateContent } from "./NotificationFilters";
|
||||||
|
import * as GitHubWebhookTypes from "@octokit/webhooks-types";
|
||||||
|
|
||||||
const log = new LogWrapper("GithubBridge");
|
const log = new LogWrapper("GithubBridge");
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ export class GithubBridge {
|
|||||||
|
|
||||||
constructor(private config: BridgeConfig, private registration: IAppserviceRegistration) { }
|
constructor(private config: BridgeConfig, private registration: IAppserviceRegistration) { }
|
||||||
|
|
||||||
private createConnectionForState(roomId: string, state: MatrixEvent<any>) {
|
private async createConnectionForState(roomId: string, state: StateEvent<any>) {
|
||||||
log.debug(`Looking to create connection for ${roomId}`);
|
log.debug(`Looking to create connection for ${roomId}`);
|
||||||
if (state.content.disabled === false) {
|
if (state.content.disabled === false) {
|
||||||
log.debug(`${roomId} has disabled state for ${state.type}`);
|
log.debug(`${roomId} has disabled state for ${state.type}`);
|
||||||
@ -66,7 +67,7 @@ export class GithubBridge {
|
|||||||
throw Error('GitHub is not configured');
|
throw Error('GitHub is not configured');
|
||||||
}
|
}
|
||||||
return new GitHubDiscussionConnection(
|
return new GitHubDiscussionConnection(
|
||||||
roomId, this.as, state.content, state.state_key || "", this.tokenStore, this.commentProcessor,
|
roomId, this.as, state.content, state.stateKey, this.tokenStore, this.commentProcessor,
|
||||||
this.messageClient,
|
this.messageClient,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -75,7 +76,9 @@ export class GithubBridge {
|
|||||||
if (!this.github) {
|
if (!this.github) {
|
||||||
throw Error('GitHub is not configured');
|
throw Error('GitHub is not configured');
|
||||||
}
|
}
|
||||||
return new GitHubIssueConnection(roomId, this.as, state.content, state.state_key || "", 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);
|
||||||
|
await issue.syncIssueState();
|
||||||
|
return issue;
|
||||||
}
|
}
|
||||||
if (GitLabRepoConnection.EventTypes.includes(state.type)) {
|
if (GitLabRepoConnection.EventTypes.includes(state.type)) {
|
||||||
if (!this.config.gitlab) {
|
if (!this.config.gitlab) {
|
||||||
@ -97,7 +100,7 @@ export class GithubBridge {
|
|||||||
roomId,
|
roomId,
|
||||||
this.as,
|
this.as,
|
||||||
state.content,
|
state.content,
|
||||||
state.state_key as string,
|
state.stateKey as string,
|
||||||
this.tokenStore,
|
this.tokenStore,
|
||||||
this.commentProcessor,
|
this.commentProcessor,
|
||||||
this.messageClient,
|
this.messageClient,
|
||||||
@ -108,7 +111,12 @@ export class GithubBridge {
|
|||||||
|
|
||||||
private async createConnectionsForRoomId(roomId: string): Promise<IConnection[]> {
|
private async createConnectionsForRoomId(roomId: string): Promise<IConnection[]> {
|
||||||
const state = await this.as.botClient.getRoomState(roomId);
|
const state = await this.as.botClient.getRoomState(roomId);
|
||||||
return state.map((event) => this.createConnectionForState(roomId, event)).filter((connection) => !!connection) as unknown as IConnection[];
|
const connections: IConnection[] = [];
|
||||||
|
for (const event of state) {
|
||||||
|
const conn = await this.createConnectionForState(roomId, new StateEvent(event));
|
||||||
|
if (conn) { connections.push(conn); }
|
||||||
|
}
|
||||||
|
return connections;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getConnectionsForGithubIssue(org: string, repo: string, issueNumber: number): (GitHubIssueConnection|GitLabRepoConnection)[] {
|
private getConnectionsForGithubIssue(org: string, repo: string, issueNumber: number): (GitHubIssueConnection|GitLabRepoConnection)[] {
|
||||||
@ -117,6 +125,8 @@ export class GithubBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getConnectionsForGithubRepo(org: string, repo: string): GitHubRepoConnection[] {
|
private getConnectionsForGithubRepo(org: string, repo: string): GitHubRepoConnection[] {
|
||||||
|
org = org.toLowerCase();
|
||||||
|
repo = repo.toLowerCase();
|
||||||
return this.connections.filter((c) => (c instanceof GitHubRepoConnection && c.org === org && c.repo === repo)) as GitHubRepoConnection[];
|
return this.connections.filter((c) => (c instanceof GitHubRepoConnection && c.org === org && c.repo === repo)) as GitHubRepoConnection[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,39 +224,41 @@ export class GithubBridge {
|
|||||||
return this.onRoomJoin(roomId, event);
|
return this.onRoomJoin(roomId, event);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queue.subscribe("comment.*");
|
|
||||||
this.queue.subscribe("issue.*");
|
|
||||||
this.queue.subscribe("response.matrix.message");
|
this.queue.subscribe("response.matrix.message");
|
||||||
this.queue.subscribe("notifications.user.events");
|
this.queue.subscribe("notifications.user.events");
|
||||||
this.queue.subscribe("merge_request.*");
|
this.queue.subscribe("github.*");
|
||||||
this.queue.subscribe("gitlab.*");
|
this.queue.subscribe("gitlab.*");
|
||||||
|
|
||||||
const validateRepoIssue = (data: IGitHubWebhookEvent) => {
|
const validateRepoIssue = (data: GitHubWebhookTypes.IssuesEvent|GitHubWebhookTypes.IssueCommentEvent) => {
|
||||||
if (!data.repository || !data.issue) {
|
if (!data.repository || !data.issue) {
|
||||||
throw Error("Malformed webhook event, missing repository or issue");
|
throw Error("Malformed webhook event, missing repository or issue");
|
||||||
}
|
}
|
||||||
|
if (!data.repository.owner?.login) {
|
||||||
|
throw Error('Cannot get connection for ownerless issue');
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
|
owner: data.repository.owner?.login,
|
||||||
repository: data.repository,
|
repository: data.repository,
|
||||||
issue: data.issue,
|
issue: data.issue,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.queue.on<IGitHubWebhookEvent>("comment.created", async ({ data }) => {
|
this.queue.on<GitHubWebhookTypes.IssueCommentCreatedEvent>("github.issue_comment.created", async ({ data }) => {
|
||||||
const { repository, issue } = validateRepoIssue(data);
|
const { repository, issue, owner } = validateRepoIssue(data);
|
||||||
const connections = this.getConnectionsForGithubIssue(repository.owner.login, repository.name, issue.number);
|
const connections = this.getConnectionsForGithubIssue(owner, repository.name, issue.number);
|
||||||
connections.map(async (c) => {
|
connections.map(async (c) => {
|
||||||
try {
|
try {
|
||||||
if (c instanceof GitHubIssueConnection)
|
if (c instanceof GitHubIssueConnection)
|
||||||
await c.onCommentCreated(data);
|
await c.onIssueCommentCreated(data);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queue.on<IGitHubWebhookEvent>("issue.opened", async ({ data }) => {
|
this.queue.on<GitHubWebhookTypes.IssuesOpenedEvent>("github.issues.opened", async ({ data }) => {
|
||||||
const { repository } = validateRepoIssue(data);
|
const { repository, owner } = validateRepoIssue(data);
|
||||||
const connections = this.getConnectionsForGithubRepo(repository.owner.login, repository.name);
|
const connections = this.getConnectionsForGithubRepo(owner, repository.name);
|
||||||
connections.map(async (c) => {
|
connections.map(async (c) => {
|
||||||
try {
|
try {
|
||||||
await c.onIssueCreated(data);
|
await c.onIssueCreated(data);
|
||||||
@ -256,12 +268,13 @@ export class GithubBridge {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queue.on<IGitHubWebhookEvent>("issue.edited", async ({ data }) => {
|
this.queue.on<GitHubWebhookTypes.IssuesEditedEvent>("github.issues.edited", async ({ data }) => {
|
||||||
const { repository, issue } = validateRepoIssue(data);
|
const { repository, issue, owner } = validateRepoIssue(data);
|
||||||
const connections = this.getConnectionsForGithubIssue(repository.owner.login, repository.name, issue.number);
|
const connections = this.getConnectionsForGithubIssue(owner, repository.name, issue.number);
|
||||||
connections.map(async (c) => {
|
connections.map(async (c) => {
|
||||||
try {
|
try {
|
||||||
if (c instanceof GitHubIssueConnection)
|
// TODO: Needs impls
|
||||||
|
if (c instanceof GitHubIssueConnection /* || c instanceof GitHubRepoConnection*/)
|
||||||
await c.onIssueEdited(data);
|
await c.onIssueEdited(data);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||||
@ -269,12 +282,12 @@ export class GithubBridge {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queue.on<IGitHubWebhookEvent>("issue.closed", async ({ data }) => {
|
this.queue.on<GitHubWebhookTypes.IssuesClosedEvent>("github.issues.closed", async ({ data }) => {
|
||||||
const { repository, issue } = validateRepoIssue(data);
|
const { repository, issue, owner } = validateRepoIssue(data);
|
||||||
const connections = this.getConnectionsForGithubIssue(repository.owner.login, repository.name, issue.number);
|
const connections = this.getConnectionsForGithubIssue(owner, repository.name, issue.number);
|
||||||
connections.map(async (c) => {
|
connections.map(async (c) => {
|
||||||
try {
|
try {
|
||||||
if (c instanceof GitHubIssueConnection)
|
if (c instanceof GitHubIssueConnection || c instanceof GitHubRepoConnection)
|
||||||
await c.onIssueStateChange();
|
await c.onIssueStateChange();
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||||
@ -282,12 +295,12 @@ export class GithubBridge {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queue.on<IGitHubWebhookEvent>("issue.reopened", async ({ data }) => {
|
this.queue.on<GitHubWebhookTypes.IssuesReopenedEvent>("github.issues.reopened", async ({ data }) => {
|
||||||
const { repository, issue } = validateRepoIssue(data);
|
const { repository, issue, owner } = validateRepoIssue(data);
|
||||||
const connections = this.getConnectionsForGithubIssue(repository.owner.login, repository.name, issue.number);
|
const connections = this.getConnectionsForGithubIssue(owner, repository.name, issue.number);
|
||||||
connections.map(async (c) => {
|
connections.map(async (c) => {
|
||||||
try {
|
try {
|
||||||
if (c instanceof GitHubIssueConnection)
|
if (c instanceof GitHubIssueConnection || c instanceof GitHubRepoConnection)
|
||||||
await c.onIssueStateChange();
|
await c.onIssueStateChange();
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||||
@ -295,7 +308,7 @@ export class GithubBridge {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
this.queue.on<IGitLabWebhookMREvent>("merge_request.open", async (msg) => {
|
this.queue.on<IGitLabWebhookMREvent>("gitlab.merge_request.open", async (msg) => {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
// const connections = this.(msg.data.project.namespace, msg.data.repository!.name, msg.data.issue!.number);
|
// const connections = this.(msg.data.project.namespace, msg.data.repository!.name, msg.data.issue!.number);
|
||||||
// connections.map(async (c) => {
|
// connections.map(async (c) => {
|
||||||
@ -371,6 +384,17 @@ export class GithubBridge {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.queue.on<IGitLabWebhookIssueStateEvent>("github.discussion_comment.created", async ({data}) => {
|
||||||
|
const connections = this.getConnectionsForGitLabIssueWebhook(data.repository.homepage, data.object_attributes.iid);
|
||||||
|
connections.map(async (c) => {
|
||||||
|
try {
|
||||||
|
await c.onIssueClosed();
|
||||||
|
} catch (ex) {
|
||||||
|
log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
// Fetch all room state
|
// Fetch all room state
|
||||||
let joinedRooms: string[]|undefined;
|
let joinedRooms: string[]|undefined;
|
||||||
while(joinedRooms === undefined) {
|
while(joinedRooms === undefined) {
|
||||||
@ -414,21 +438,35 @@ export class GithubBridge {
|
|||||||
log.error(`Unable to create connection for ${roomId}`, ex);
|
log.error(`Unable to create connection for ${roomId}`, ex);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this.connections.push(...connections);
|
if (connections.length) {
|
||||||
if (connections.length === 0) {
|
|
||||||
// TODO: Refactor this to be a connection
|
|
||||||
try {
|
|
||||||
const accountData = await this.as.botIntent.underlyingClient.getRoomAccountData(
|
|
||||||
BRIDGE_ROOM_TYPE, roomId,
|
|
||||||
);
|
|
||||||
const adminRoom = await this.setupAdminRoom(roomId, accountData);
|
|
||||||
// Call this on startup to set the state
|
|
||||||
await this.onAdminRoomSettingsChanged(adminRoom, accountData, { admin_user: accountData.admin_user });
|
|
||||||
} catch (ex) {
|
|
||||||
log.debug(`Room ${roomId} has no connections and is not an admin room`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.info(`Room ${roomId} is connected to: ${connections.join(',')}`);
|
log.info(`Room ${roomId} is connected to: ${connections.join(',')}`);
|
||||||
|
this.connections.push(...connections);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// TODO: Refactor this to be a connection
|
||||||
|
try {
|
||||||
|
const accountData = await this.as.botIntent.underlyingClient.getSafeRoomAccountData<AdminAccountData>(
|
||||||
|
BRIDGE_ROOM_TYPE, roomId,
|
||||||
|
);
|
||||||
|
if (!accountData) {
|
||||||
|
log.debug(`Room ${roomId} has no connections and is not an admin room`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let notifContent;
|
||||||
|
try {
|
||||||
|
notifContent = await this.as.botIntent.underlyingClient.getRoomStateEvent(
|
||||||
|
roomId, NotifFilter.StateType, "",
|
||||||
|
);
|
||||||
|
} catch (ex) {
|
||||||
|
// No state yet
|
||||||
|
}
|
||||||
|
const adminRoom = await this.setupAdminRoom(roomId, accountData, notifContent || NotifFilter.getDefaultContent());
|
||||||
|
// Call this on startup to set the state
|
||||||
|
await this.onAdminRoomSettingsChanged(adminRoom, accountData, { admin_user: accountData.admin_user });
|
||||||
|
log.info(`Room ${roomId} is connected to: ${adminRoom.toString()}`);
|
||||||
|
} catch (ex) {
|
||||||
|
log.error(`Failed to setup admin room ${roomId}:`, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.config.widgets) {
|
if (this.config.widgets) {
|
||||||
@ -450,9 +488,9 @@ export class GithubBridge {
|
|||||||
}
|
}
|
||||||
await retry(() => this.as.botIntent.joinRoom(roomId), 5);
|
await retry(() => this.as.botIntent.joinRoom(roomId), 5);
|
||||||
if (event.content.is_direct) {
|
if (event.content.is_direct) {
|
||||||
const room = await this.setupAdminRoom(roomId, {admin_user: event.sender});
|
const room = await this.setupAdminRoom(roomId, {admin_user: event.sender}, NotifFilter.getDefaultContent());
|
||||||
await this.as.botIntent.underlyingClient.setRoomAccountData(
|
await this.as.botIntent.underlyingClient.setRoomAccountData(
|
||||||
BRIDGE_ROOM_TYPE, roomId, room.data,
|
BRIDGE_ROOM_TYPE, roomId, room.accountData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// This is a group room, don't add the admin settings and just sit in the room.
|
// This is a group room, don't add the admin settings and just sit in the room.
|
||||||
@ -534,18 +572,14 @@ export class GithubBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async onRoomEvent(roomId: string, event: MatrixEvent<unknown>) {
|
private async onRoomEvent(roomId: string, event: MatrixEvent<unknown>) {
|
||||||
if (event.sender === this.as.botUserId) {
|
|
||||||
// It's us
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.state_key) {
|
if (event.state_key) {
|
||||||
// A state update, hurrah!
|
// A state update, hurrah!
|
||||||
const existingConnection = this.connections.find((c) => c.roomId === roomId && c.isInterestedInStateEvent(event.type, event.state_key || ""));
|
const existingConnection = this.connections.find((c) => c.roomId === roomId && c.isInterestedInStateEvent(event.type, event.state_key || ""));
|
||||||
if (existingConnection?.onStateUpdate) {
|
if (existingConnection?.onStateUpdate) {
|
||||||
existingConnection.onStateUpdate(event);
|
existingConnection.onStateUpdate(event);
|
||||||
} else {
|
} else if (!existingConnection) {
|
||||||
// Is anyone interested in this state?
|
// Is anyone interested in this state?
|
||||||
const connection = await this.createConnectionForState(roomId, event);
|
const connection = await this.createConnectionForState(roomId, new StateEvent(event));
|
||||||
if (connection) {
|
if (connection) {
|
||||||
log.info(`New connected added to ${roomId}: ${connection.toString()}`);
|
log.info(`New connected added to ${roomId}: ${connection.toString()}`);
|
||||||
this.connections.push(connection);
|
this.connections.push(connection);
|
||||||
@ -553,6 +587,10 @@ export class GithubBridge {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (event.sender === this.as.botUserId) {
|
||||||
|
// It's us
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Alas, it's just an event.
|
// Alas, it's just an event.
|
||||||
return this.connections.filter((c) => c.roomId === roomId).map((c) => c.onEvent ? c.onEvent(event) : undefined);
|
return this.connections.filter((c) => c.roomId === roomId).map((c) => c.onEvent ? c.onEvent(event) : undefined);
|
||||||
@ -674,9 +712,9 @@ export class GithubBridge {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupAdminRoom(roomId: string, accountData: AdminAccountData) {
|
private async setupAdminRoom(roomId: string, accountData: AdminAccountData, notifContent: NotificationFilterStateContent) {
|
||||||
const adminRoom = new AdminRoom(
|
const adminRoom = new AdminRoom(
|
||||||
roomId, accountData, this.as.botIntent, this.tokenStore, this.config,
|
roomId, accountData, notifContent, this.as.botIntent, this.tokenStore, this.config,
|
||||||
);
|
);
|
||||||
adminRoom.on("settings.changed", this.onAdminRoomSettingsChanged.bind(this));
|
adminRoom.on("settings.changed", this.onAdminRoomSettingsChanged.bind(this));
|
||||||
adminRoom.on("open.project", async (project: ProjectsGetResponseData) => {
|
adminRoom.on("open.project", async (project: ProjectsGetResponseData) => {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { BridgeConfig } from "./Config/Config";
|
import { BridgeConfig } from "./Config/Config";
|
||||||
import { Application, default as express, Request, Response } from "express";
|
import { Application, default as express, Request, Response } from "express";
|
||||||
import { createHmac } from "crypto";
|
import { createHmac } from "crypto";
|
||||||
import { IssuesGetResponseData, IssuesGetCommentResponseData, ReposGetResponseData } from "@octokit/types";
|
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { MessageQueue, createMessageQueue, MessageQueueMessage } from "./MessageQueue/MessageQueue";
|
import { MessageQueue, createMessageQueue, MessageQueueMessage } from "./MessageQueue/MessageQueue";
|
||||||
import LogWrapper from "./LogWrapper";
|
import LogWrapper from "./LogWrapper";
|
||||||
@ -10,31 +9,17 @@ import { Server } from "http";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { UserNotificationWatcher } from "./Notifications/UserNotificationWatcher";
|
import { UserNotificationWatcher } from "./Notifications/UserNotificationWatcher";
|
||||||
import { IGitLabWebhookEvent } from "./Gitlab/WebhookTypes";
|
import { IGitLabWebhookEvent } from "./Gitlab/WebhookTypes";
|
||||||
|
import { Webhooks as OctokitWebhooks } from "@octokit/webhooks"
|
||||||
const log = new LogWrapper("GithubWebhooks");
|
const log = new LogWrapper("GithubWebhooks");
|
||||||
|
|
||||||
export interface IGitHubWebhookEvent {
|
|
||||||
action: string;
|
|
||||||
issue?: IssuesGetResponseData;
|
|
||||||
comment?: IssuesGetCommentResponseData;
|
|
||||||
repository?: ReposGetResponseData;
|
|
||||||
sender?: {
|
|
||||||
login: string;
|
|
||||||
}
|
|
||||||
changes?: {
|
|
||||||
title?: {
|
|
||||||
from: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IOAuthRequest {
|
export interface IOAuthRequest {
|
||||||
code: string;
|
code: string;
|
||||||
state: string;
|
state: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IOAuthTokens {
|
export interface IOAuthTokens {
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
access_token: string;
|
access_token: string;
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
token_type: string;
|
token_type: string;
|
||||||
state: string;
|
state: string;
|
||||||
}
|
}
|
||||||
@ -60,9 +45,26 @@ export class GithubWebhooks extends EventEmitter {
|
|||||||
private queue: MessageQueue;
|
private queue: MessageQueue;
|
||||||
private userNotificationWatcher: UserNotificationWatcher;
|
private userNotificationWatcher: UserNotificationWatcher;
|
||||||
private server?: Server;
|
private server?: Server;
|
||||||
|
private ghWebhooks?: OctokitWebhooks;
|
||||||
constructor(private config: BridgeConfig) {
|
constructor(private config: BridgeConfig) {
|
||||||
super();
|
super();
|
||||||
this.expressApp = express();
|
this.expressApp = express();
|
||||||
|
if (this.config.github?.webhook.secret) {
|
||||||
|
this.ghWebhooks = new OctokitWebhooks({
|
||||||
|
secret: config.github?.webhook.secret as string,
|
||||||
|
});
|
||||||
|
this.ghWebhooks.onAny(({id, name, payload}) => {
|
||||||
|
log.info(`Got GitHub webhook event ${id} ${name}`);
|
||||||
|
this.queue.push({
|
||||||
|
eventName: `github.name`,
|
||||||
|
sender: "GithubWebhooks",
|
||||||
|
data: payload,
|
||||||
|
}).catch((err) => {
|
||||||
|
log.error(`Failed to emit payload: ${err}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.expressApp.use(express.json({
|
this.expressApp.use(express.json({
|
||||||
verify: this.verifyRequest.bind(this),
|
verify: this.verifyRequest.bind(this),
|
||||||
}));
|
}));
|
||||||
@ -77,8 +79,6 @@ export class GithubWebhooks extends EventEmitter {
|
|||||||
this.queue.on("notifications.user.disable", (msg: MessageQueueMessage<NotificationsDisableEvent>) => {
|
this.queue.on("notifications.user.disable", (msg: MessageQueueMessage<NotificationsDisableEvent>) => {
|
||||||
this.userNotificationWatcher.removeUser(msg.data.userId, msg.data.type, msg.data.instanceUrl);
|
this.userNotificationWatcher.removeUser(msg.data.userId, msg.data.type, msg.data.instanceUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// This also listens for notifications for users, which is long polly.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public listen() {
|
public listen() {
|
||||||
@ -99,23 +99,6 @@ export class GithubWebhooks extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onGitHubPayload(body: IGitHubWebhookEvent) {
|
|
||||||
if (body.action === "created" && body.comment) {
|
|
||||||
return "comment.created";
|
|
||||||
} else if (body.action === "edited" && body.comment) {
|
|
||||||
return "comment.edited";
|
|
||||||
} else if (body.action === "opened" && body.issue) {
|
|
||||||
return "issue.opened";
|
|
||||||
} else if (body.action === "edited" && body.issue) {
|
|
||||||
return "issue.edited";
|
|
||||||
} else if (body.action === "closed" && body.issue) {
|
|
||||||
return "issue.closed";
|
|
||||||
} else if (body.action === "reopened" && body.issue) {
|
|
||||||
return "issue.reopened";
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onGitLabPayload(body: IGitLabWebhookEvent) {
|
private onGitLabPayload(body: IGitLabWebhookEvent) {
|
||||||
log.info(`onGitLabPayload ${body.event_type}:`, body);
|
log.info(`onGitLabPayload ${body.event_type}:`, body);
|
||||||
if (body.event_type === "merge_request") {
|
if (body.event_type === "merge_request") {
|
||||||
@ -134,10 +117,25 @@ export class GithubWebhooks extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
let eventName: string|null = null;
|
let eventName: string|null = null;
|
||||||
const body = req.body;
|
const body = req.body;
|
||||||
res.sendStatus(200);
|
|
||||||
if (req.headers['x-hub-signature']) {
|
if (req.headers['x-hub-signature']) {
|
||||||
eventName = this.onGitHubPayload(body);
|
if (!this.ghWebhooks) {
|
||||||
|
log.warn(`Not configured for GitHub webhooks, but got a GitHub event`)
|
||||||
|
res.sendStatus(500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.sendStatus(200);
|
||||||
|
this.ghWebhooks.verifyAndReceive({
|
||||||
|
id: req.headers["x-github-delivery"] as string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
name: req.headers["x-github-event"] as any,
|
||||||
|
payload: req.body,
|
||||||
|
signature: req.headers["x-hub-signature-256"] as string,
|
||||||
|
}).catch((err) => {
|
||||||
|
log.error(`Failed handle GitHubEvent: ${err}`);
|
||||||
|
});
|
||||||
|
res.sendStatus(200);
|
||||||
} else if (req.headers['x-gitlab-token']) {
|
} else if (req.headers['x-gitlab-token']) {
|
||||||
|
res.sendStatus(200);
|
||||||
eventName = this.onGitLabPayload(body);
|
eventName = this.onGitLabPayload(body);
|
||||||
}
|
}
|
||||||
if (eventName) {
|
if (eventName) {
|
||||||
@ -146,7 +144,7 @@ export class GithubWebhooks extends EventEmitter {
|
|||||||
sender: "GithubWebhooks",
|
sender: "GithubWebhooks",
|
||||||
data: body,
|
data: body,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
log.info(`Failed to emit payload: ${err}`);
|
log.error(`Failed to emit payload: ${err}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.debug("Unknown event:", req.body);
|
log.debug("Unknown event:", req.body);
|
||||||
@ -181,6 +179,7 @@ export class GithubWebhooks extends EventEmitter {
|
|||||||
redirect_uri: this.config.github.oauth.redirect_uri,
|
redirect_uri: this.config.github.oauth.redirect_uri,
|
||||||
state: req.query.state as string,
|
state: req.query.state as string,
|
||||||
})}`);
|
})}`);
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
const result = qs.parse(accessTokenRes.data) as { access_token: string, token_type: string };
|
const result = qs.parse(accessTokenRes.data) as { access_token: string, token_type: string };
|
||||||
await this.queue.push<IOAuthTokens>({
|
await this.queue.push<IOAuthTokens>({
|
||||||
eventName: "oauth.tokens",
|
eventName: "oauth.tokens",
|
||||||
@ -194,21 +193,9 @@ export class GithubWebhooks extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate the X-Hub-Signature header value.
|
private verifyRequest(req: Request, res: Response) {
|
||||||
private getSignature(buf: Buffer) {
|
|
||||||
if (!this.config.github) {
|
|
||||||
throw Error("Got GitHub oauth request but github was not configured!");
|
|
||||||
}
|
|
||||||
const hmac = createHmac("sha1", this.config.github.webhook.secret);
|
|
||||||
hmac.update(buf);
|
|
||||||
return "sha1=" + hmac.digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify function compatible with body-parser to retrieve the request payload.
|
|
||||||
// Read more: https://github.com/expressjs/body-parser#verify
|
|
||||||
private verifyRequest(req: Request, res: Response, buf: Buffer) {
|
|
||||||
if (req.headers['x-gitlab-token']) {
|
if (req.headers['x-gitlab-token']) {
|
||||||
// This is a gitlab request!
|
// GitLab
|
||||||
if (!this.config.gitlab) {
|
if (!this.config.gitlab) {
|
||||||
log.error("Got a GitLab webhook, but the bridge is not set up for it.");
|
log.error("Got a GitLab webhook, but the bridge is not set up for it.");
|
||||||
res.sendStatus(400);
|
res.sendStatus(400);
|
||||||
@ -223,16 +210,9 @@ export class GithubWebhooks extends EventEmitter {
|
|||||||
throw Error("Invalid signature.");
|
throw Error("Invalid signature.");
|
||||||
}
|
}
|
||||||
} else if (req.headers["x-hub-signature"]) {
|
} else if (req.headers["x-hub-signature"]) {
|
||||||
const expected = req.headers["x-hub-signature"];
|
// GitHub
|
||||||
const calculated = this.getSignature(buf);
|
// Verified within handler.
|
||||||
if (expected !== calculated) {
|
return true;
|
||||||
log.error(`${req.url} had an invalid signature`);
|
|
||||||
res.sendStatus(403);
|
|
||||||
throw Error("Invalid signature.");
|
|
||||||
} else {
|
|
||||||
log.debug('Verified GitHub request');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
log.error(`No signature on URL. Rejecting`);
|
log.error(`No signature on URL. Rejecting`);
|
||||||
res.sendStatus(400);
|
res.sendStatus(400);
|
||||||
|
@ -2,9 +2,8 @@ import { LogService } from "matrix-bot-sdk";
|
|||||||
import util from "util";
|
import util from "util";
|
||||||
import winston from "winston";
|
import winston from "winston";
|
||||||
|
|
||||||
// Logs contain unknowns, ignore this.
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
// tslint:disable: no-any
|
type MsgType = string|Error|any|{error?: string};
|
||||||
|
|
||||||
export default class LogWrapper {
|
export default class LogWrapper {
|
||||||
|
|
||||||
public static configureLogging(level: string) {
|
public static configureLogging(level: string) {
|
||||||
@ -24,7 +23,7 @@ export default class LogWrapper {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const getMessageString = (...messageOrObject: any[]) => {
|
const getMessageString = (messageOrObject: MsgType[]) => {
|
||||||
messageOrObject = messageOrObject.flat();
|
messageOrObject = messageOrObject.flat();
|
||||||
const messageParts: string[] = [];
|
const messageParts: string[] = [];
|
||||||
messageOrObject.forEach((obj) => {
|
messageOrObject.forEach((obj) => {
|
||||||
@ -37,7 +36,7 @@ export default class LogWrapper {
|
|||||||
return messageParts.join(" ");
|
return messageParts.join(" ");
|
||||||
};
|
};
|
||||||
LogService.setLogger({
|
LogService.setLogger({
|
||||||
info: (module: string, ...messageOrObject: any[]) => {
|
info: (module: string, ...messageOrObject: MsgType[]) => {
|
||||||
// These are noisy, redirect to debug.
|
// These are noisy, redirect to debug.
|
||||||
if (module.startsWith("MatrixLiteClient")) {
|
if (module.startsWith("MatrixLiteClient")) {
|
||||||
log.debug(getMessageString(messageOrObject), { module });
|
log.debug(getMessageString(messageOrObject), { module });
|
||||||
@ -45,19 +44,28 @@ export default class LogWrapper {
|
|||||||
}
|
}
|
||||||
log.info(getMessageString(messageOrObject), { module });
|
log.info(getMessageString(messageOrObject), { module });
|
||||||
},
|
},
|
||||||
warn: (module: string, ...messageOrObject: any[]) => {
|
warn: (module: string, ...messageOrObject: MsgType[]) => {
|
||||||
|
const error = messageOrObject[0].error || messageOrObject[1].body?.error;
|
||||||
|
if (error === "Room account data not found") {
|
||||||
|
log.debug(getMessageString(messageOrObject), { module });
|
||||||
|
return; // This is just noise :|
|
||||||
|
}
|
||||||
log.warn(getMessageString(messageOrObject), { module });
|
log.warn(getMessageString(messageOrObject), { module });
|
||||||
},
|
},
|
||||||
error: (module: string, ...messageOrObject: any[]) => {
|
error: (module: string, ...messageOrObject: MsgType[]) => {
|
||||||
if (messageOrObject[0]?.error === "Room account data not found") {
|
const error = messageOrObject[0].error || messageOrObject[1]?.body?.error;
|
||||||
|
if (error === "Room account data not found") {
|
||||||
log.debug(getMessageString(messageOrObject), { module });
|
log.debug(getMessageString(messageOrObject), { module });
|
||||||
return; // This is just noise :|
|
return; // This is just noise :|
|
||||||
}
|
}
|
||||||
log.error(getMessageString(messageOrObject), { module });
|
log.error(getMessageString(messageOrObject), { module });
|
||||||
},
|
},
|
||||||
debug: (module: string, ...messageOrObject: any[]) => {
|
debug: (module: string, ...messageOrObject: MsgType[]) => {
|
||||||
log.debug(getMessageString(messageOrObject), { module });
|
log.debug(getMessageString(messageOrObject), { module });
|
||||||
},
|
},
|
||||||
|
trace: (module: string, ...messageOrObject: MsgType[]) => {
|
||||||
|
log.verbose(getMessageString(messageOrObject), { module });
|
||||||
|
},
|
||||||
});
|
});
|
||||||
LogService.info("LogWrapper", "Reconfigured logging");
|
LogService.info("LogWrapper", "Reconfigured logging");
|
||||||
}
|
}
|
||||||
@ -69,7 +77,7 @@ export default class LogWrapper {
|
|||||||
* @param {string} module The module being logged
|
* @param {string} module The module being logged
|
||||||
* @param {*[]} messageOrObject The data to log
|
* @param {*[]} messageOrObject The data to log
|
||||||
*/
|
*/
|
||||||
public debug(...messageOrObject: any[]) {
|
public debug(...messageOrObject: MsgType[]) {
|
||||||
LogService.debug(this.module, ...messageOrObject);
|
LogService.debug(this.module, ...messageOrObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +85,7 @@ export default class LogWrapper {
|
|||||||
* Logs to the ERROR channel
|
* Logs to the ERROR channel
|
||||||
* @param {*[]} messageOrObject The data to log
|
* @param {*[]} messageOrObject The data to log
|
||||||
*/
|
*/
|
||||||
public error(...messageOrObject: any[]) {
|
public error(...messageOrObject: MsgType[]) {
|
||||||
LogService.error(this.module, ...messageOrObject);
|
LogService.error(this.module, ...messageOrObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,7 +93,7 @@ export default class LogWrapper {
|
|||||||
* Logs to the INFO channel
|
* Logs to the INFO channel
|
||||||
* @param {*[]} messageOrObject The data to log
|
* @param {*[]} messageOrObject The data to log
|
||||||
*/
|
*/
|
||||||
public info(...messageOrObject: any[]) {
|
public info(...messageOrObject: MsgType[]) {
|
||||||
LogService.info(this.module, ...messageOrObject);
|
LogService.info(this.module, ...messageOrObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +101,7 @@ export default class LogWrapper {
|
|||||||
* Logs to the WARN channel
|
* Logs to the WARN channel
|
||||||
* @param {*[]} messageOrObject The data to log
|
* @param {*[]} messageOrObject The data to log
|
||||||
*/
|
*/
|
||||||
public warn(...messageOrObject: any[]) {
|
public warn(...messageOrObject: MsgType[]) {
|
||||||
LogService.warn(this.module, ...messageOrObject);
|
LogService.warn(this.module, ...messageOrObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
87
src/NotificationFilters.ts
Normal file
87
src/NotificationFilters.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
|
||||||
|
interface FilterContent {
|
||||||
|
users: string[];
|
||||||
|
repos: string[];
|
||||||
|
orgs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NotificationFilterStateContent {
|
||||||
|
filters: {
|
||||||
|
[name: string]: FilterContent;
|
||||||
|
};
|
||||||
|
forNotifications: string[];
|
||||||
|
forInvites: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A notification filter is a set of keys that define what should be sent to the user.
|
||||||
|
*/
|
||||||
|
export class NotifFilter {
|
||||||
|
static readonly StateType = "uk.half-shot.matrix-github.notif-filter"
|
||||||
|
|
||||||
|
static getDefaultContent(): NotificationFilterStateContent {
|
||||||
|
return {
|
||||||
|
filters: {},
|
||||||
|
forNotifications: [],
|
||||||
|
forInvites: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly forNotifications: Set<string>;
|
||||||
|
public readonly forInvites: Set<string>;
|
||||||
|
public filters: Record<string, FilterContent>;
|
||||||
|
constructor(stateContent: NotificationFilterStateContent) {
|
||||||
|
this.forNotifications = new Set(stateContent.forNotifications);
|
||||||
|
this.forInvites = new Set(stateContent.forInvites);
|
||||||
|
this.filters = stateContent.filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get empty() {
|
||||||
|
return Object.values(this.filters).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStateContent(): NotificationFilterStateContent {
|
||||||
|
return {
|
||||||
|
filters: this.filters,
|
||||||
|
forInvites: [...this.forInvites],
|
||||||
|
forNotifications: [...this.forNotifications],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldInviteToRoom(user: string, repo: string, org: string): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldSendNotification(user?: string, repo?: string, org?: string): boolean {
|
||||||
|
if (this.forNotifications.size === 0) {
|
||||||
|
// Default on.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (const filterName of this.forNotifications) {
|
||||||
|
const filter = this.filters[filterName];
|
||||||
|
if (!filter) {
|
||||||
|
// Filter with this name exists.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (user && filter.users.includes(user.toLowerCase())) {
|
||||||
|
// We have a user in this notif and we are filtering on users.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (repo && filter.repos.includes(repo.toLowerCase())) {
|
||||||
|
// We have a repo in this notif and we are filtering on repos.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (org && filter.orgs.includes(org.toLowerCase())) {
|
||||||
|
// We have an org in this notif and we are filtering on orgs.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// None of the filters matched, so exclude the result.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setFilter(name: string, filter: FilterContent) {
|
||||||
|
this.filters[name] = filter;
|
||||||
|
}
|
||||||
|
}
|
@ -13,8 +13,8 @@ const GH_API_THRESHOLD = 50;
|
|||||||
const GH_API_RETRY_IN = 1000 * 60;
|
const GH_API_RETRY_IN = 1000 * 60;
|
||||||
|
|
||||||
export class GitHubWatcher extends EventEmitter implements NotificationWatcherTask {
|
export class GitHubWatcher extends EventEmitter implements NotificationWatcherTask {
|
||||||
private static apiFailureCount: number;
|
private static apiFailureCount = 0;
|
||||||
private static globalRetryIn: number;
|
private static globalRetryIn = 0;
|
||||||
|
|
||||||
public static checkGitHubStatus() {
|
public static checkGitHubStatus() {
|
||||||
this.apiFailureCount = Math.min(this.apiFailureCount + 1, GH_API_THRESHOLD);
|
this.apiFailureCount = Math.min(this.apiFailureCount + 1, GH_API_THRESHOLD);
|
||||||
@ -43,7 +43,8 @@ export class GitHubWatcher extends EventEmitter implements NotificationWatcherTa
|
|||||||
}
|
}
|
||||||
|
|
||||||
public start(intervalMs: number) {
|
public start(intervalMs: number) {
|
||||||
this.interval = setTimeout(() => {
|
log.info(`Starting for ${this.userId}`);
|
||||||
|
this.interval = setInterval(() => {
|
||||||
this.getNotifications();
|
this.getNotifications();
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
this.getNotifications();
|
this.getNotifications();
|
||||||
@ -51,6 +52,7 @@ export class GitHubWatcher extends EventEmitter implements NotificationWatcherTa
|
|||||||
|
|
||||||
public stop() {
|
public stop() {
|
||||||
if (this.interval) {
|
if (this.interval) {
|
||||||
|
log.info(`Stopping for ${this.userId}`);
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -71,7 +73,7 @@ export class GitHubWatcher extends EventEmitter implements NotificationWatcherTa
|
|||||||
log.info(`Not getting notifications for ${this.userId}, API is still down.`);
|
log.info(`Not getting notifications for ${this.userId}, API is still down.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info(`Getting notifications for ${this.userId} ${this.lastReadTs}`);
|
log.debug(`Getting notifications for ${this.userId} ${this.lastReadTs}`);
|
||||||
const since = this.lastReadTs !== 0 ? `&since=${new Date(this.lastReadTs).toISOString()}`: "";
|
const since = this.lastReadTs !== 0 ? `&since=${new Date(this.lastReadTs).toISOString()}`: "";
|
||||||
let response: OctokitResponse<GitHubUserNotification[]>;
|
let response: OctokitResponse<GitHubUserNotification[]>;
|
||||||
try {
|
try {
|
||||||
@ -86,9 +88,11 @@ export class GitHubWatcher extends EventEmitter implements NotificationWatcherTa
|
|||||||
await this.handleGitHubFailure(ex);
|
await this.handleGitHubFailure(ex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info(`Got ${response.data.length} notifications`);
|
|
||||||
this.lastReadTs = Date.now();
|
this.lastReadTs = Date.now();
|
||||||
|
|
||||||
|
if (response.data.length) {
|
||||||
|
log.info(`Got ${response.data.length} notifications for ${this.userId}`);
|
||||||
|
}
|
||||||
for (const rawEvent of response.data) {
|
for (const rawEvent of response.data) {
|
||||||
try {
|
try {
|
||||||
if (rawEvent.subject.url) {
|
if (rawEvent.subject.url) {
|
||||||
@ -104,6 +108,10 @@ export class GitHubWatcher extends EventEmitter implements NotificationWatcherTa
|
|||||||
log.warn("review_requested was missing subject.url_data.number");
|
log.warn("review_requested was missing subject.url_data.number");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!rawEvent.repository.owner) {
|
||||||
|
log.warn("review_requested was missing repository.owner");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
rawEvent.subject.requested_reviewers = (await this.octoKit.pulls.listRequestedReviewers({
|
rawEvent.subject.requested_reviewers = (await this.octoKit.pulls.listRequestedReviewers({
|
||||||
pull_number: rawEvent.subject.url_data.number,
|
pull_number: rawEvent.subject.url_data.number,
|
||||||
owner: rawEvent.repository.owner.login,
|
owner: rawEvent.repository.owner.login,
|
||||||
|
@ -16,7 +16,7 @@ export class GitLabWatcher extends EventEmitter implements NotificationWatcherTa
|
|||||||
}
|
}
|
||||||
|
|
||||||
public start(intervalMs: number) {
|
public start(intervalMs: number) {
|
||||||
this.interval = setTimeout(() => {
|
this.interval = setInterval(() => {
|
||||||
this.getNotifications();
|
this.getNotifications();
|
||||||
}, intervalMs);
|
}, intervalMs);
|
||||||
}
|
}
|
||||||
|
@ -5,15 +5,18 @@ import LogWrapper from "./LogWrapper";
|
|||||||
import { AdminRoom } from "./AdminRoom";
|
import { AdminRoom } from "./AdminRoom";
|
||||||
import markdown from "markdown-it";
|
import markdown from "markdown-it";
|
||||||
import { FormatUtil } from "./FormatUtil";
|
import { FormatUtil } from "./FormatUtil";
|
||||||
import { IssuesListAssigneesResponseData, PullsGetResponseData, IssuesGetResponseData, PullsListRequestedReviewersResponseData, PullsListReviewsResponseData, IssuesGetCommentResponseData } from "@octokit/types";
|
import { PullGetResponseData, IssuesGetResponseData, PullsListRequestedReviewersResponseData, PullsListReviewsResponseData, IssuesGetCommentResponseData } from "./Github/Types";
|
||||||
import { GitHubUserNotification } from "./Github/Types";
|
import { GitHubUserNotification } from "./Github/Types";
|
||||||
|
import { components } from "@octokit/openapi-types/dist-types/generated/types";
|
||||||
|
import { NotifFilter } from "./NotificationFilters";
|
||||||
|
|
||||||
const log = new LogWrapper("GithubBridge");
|
|
||||||
|
const log = new LogWrapper("NotificationProcessor");
|
||||||
const md = new markdown();
|
const md = new markdown();
|
||||||
|
|
||||||
export interface IssueDiff {
|
export interface IssueDiff {
|
||||||
state: null|string;
|
state: null|string;
|
||||||
assignee: null|IssuesListAssigneesResponseData;
|
assignee: null|(components["schemas"]["simple-user"][]);
|
||||||
title: null|string;
|
title: null|string;
|
||||||
merged: boolean;
|
merged: boolean;
|
||||||
mergedBy: null|{
|
mergedBy: null|{
|
||||||
@ -34,7 +37,7 @@ export interface CachedReviewData {
|
|||||||
reviews: PullsListReviewsResponseData;
|
reviews: PullsListReviewsResponseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PROrIssue = IssuesGetResponseData|PullsGetResponseData;
|
type PROrIssue = IssuesGetResponseData|PullGetResponseData;
|
||||||
|
|
||||||
export class NotificationProcessor {
|
export class NotificationProcessor {
|
||||||
|
|
||||||
@ -66,12 +69,13 @@ export class NotificationProcessor {
|
|||||||
plain += `\n\n Title changed to: ${diff.title}`;
|
plain += `\n\n Title changed to: ${diff.title}`;
|
||||||
}
|
}
|
||||||
if (diff.assignee) {
|
if (diff.assignee) {
|
||||||
plain += `\n\n Assigned to: ${diff.assignee[0].login}`;
|
plain += `\n\n Assigned to: ${diff.assignee.map(l => l?.login).join(", ")}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newComment) {
|
if (newComment) {
|
||||||
const comment = notif.subject.latest_comment_url_data as IssuesGetCommentResponseData;
|
const comment = notif.subject.latest_comment_url_data as IssuesGetCommentResponseData;
|
||||||
plain += `\n\n ${NotificationProcessor.formatUser(comment.user)}:\n\n > ${comment.body}`;
|
const user = comment.user ? NotificationProcessor.formatUser(comment.user) : 'user';
|
||||||
|
plain += `\n\n ${user}:\n\n > ${comment.body}`;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
plain,
|
plain,
|
||||||
@ -107,7 +111,7 @@ export class NotificationProcessor {
|
|||||||
for (const event of msg.events) {
|
for (const event of msg.events) {
|
||||||
const isIssueOrPR = event.subject.type === "Issue" || event.subject.type === "PullRequest";
|
const isIssueOrPR = event.subject.type === "Issue" || event.subject.type === "PullRequest";
|
||||||
try {
|
try {
|
||||||
await this.handleUserNotification(msg.roomId, event);
|
await this.handleUserNotification(msg.roomId, event, adminRoom.notifFilter);
|
||||||
if (isIssueOrPR && event.subject.url_data) {
|
if (isIssueOrPR && event.subject.url_data) {
|
||||||
const issueNumber = event.subject.url_data.number.toString();
|
const issueNumber = event.subject.url_data.number.toString();
|
||||||
await this.storage.setGithubIssue(
|
await this.storage.setGithubIssue(
|
||||||
@ -187,9 +191,12 @@ export class NotificationProcessor {
|
|||||||
private diffIssueChanges(curr: PROrIssue, prev: PROrIssue): IssueDiff {
|
private diffIssueChanges(curr: PROrIssue, prev: PROrIssue): IssueDiff {
|
||||||
let merged = false;
|
let merged = false;
|
||||||
let mergedBy = null;
|
let mergedBy = null;
|
||||||
if ((curr as PullsGetResponseData).merged !== (prev as PullsGetResponseData).merged) {
|
if ((curr as PullGetResponseData).merged !== (prev as PullGetResponseData).merged) {
|
||||||
merged = true;
|
merged = true;
|
||||||
mergedBy = (curr as PullsGetResponseData).merged_by;
|
mergedBy = (curr as PullGetResponseData).merged_by;
|
||||||
|
}
|
||||||
|
if (!curr.user) {
|
||||||
|
throw Error('No user for issue');
|
||||||
}
|
}
|
||||||
const diff: IssueDiff = {
|
const diff: IssueDiff = {
|
||||||
state: curr.state === prev.state ? null : curr.state,
|
state: curr.state === prev.state ? null : curr.state,
|
||||||
@ -217,7 +224,7 @@ export class NotificationProcessor {
|
|||||||
(await this.storage.getLastNotifCommentUrl(notif.repository.full_name, issueNumber, roomId));
|
(await this.storage.getLastNotifCommentUrl(notif.repository.full_name, issueNumber, roomId));
|
||||||
|
|
||||||
const formatted = NotificationProcessor.formatNotification(notif, diff, newComment);
|
const formatted = NotificationProcessor.formatNotification(notif, diff, newComment);
|
||||||
let body: any = {
|
let body = {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
body: formatted.plain,
|
body: formatted.plain,
|
||||||
formatted_body: formatted.html,
|
formatted_body: formatted.html,
|
||||||
@ -245,8 +252,15 @@ export class NotificationProcessor {
|
|||||||
return this.matrixSender.sendMatrixMessage(roomId, body);
|
return this.matrixSender.sendMatrixMessage(roomId, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleUserNotification(roomId: string, notif: GitHubUserNotification) {
|
private async handleUserNotification(roomId: string, notif: GitHubUserNotification, filter: NotifFilter) {
|
||||||
log.info("New notification event:", notif);
|
log.info("New notification event:", notif);
|
||||||
|
if (!filter.shouldSendNotification(
|
||||||
|
notif.subject.latest_comment_url_data?.user?.login,
|
||||||
|
notif.repository.full_name,
|
||||||
|
notif.repository.owner?.login)) {
|
||||||
|
log.debug(`Dropping notification because user is filtering it out`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (notif.reason === "security_alert") {
|
if (notif.reason === "security_alert") {
|
||||||
return this.matrixSender.sendMatrixMessage(roomId, this.formatSecurityAlert(notif));
|
return this.matrixSender.sendMatrixMessage(roomId, this.formatSecurityAlert(notif));
|
||||||
} else if (notif.subject.type === "Issue" || notif.subject.type === "PullRequest") {
|
} else if (notif.subject.type === "Issue" || notif.subject.type === "PullRequest") {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { MemoryStorageProvider as MSP } from "matrix-bot-sdk";
|
import { MemoryStorageProvider as MSP } from "matrix-bot-sdk";
|
||||||
import { IStorageProvider } from "./StorageProvider";
|
import { IStorageProvider } from "./StorageProvider";
|
||||||
import { IssuesGetResponseData } from "@octokit/types";
|
import { IssuesGetResponseData } from "../Github/Types";
|
||||||
|
|
||||||
export class MemoryStorageProvider extends MSP implements IStorageProvider {
|
export class MemoryStorageProvider extends MSP implements IStorageProvider {
|
||||||
private issues: Map<string, IssuesGetResponseData> = new Map();
|
private issues: Map<string, IssuesGetResponseData> = new Map();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { IssuesGetResponseData } from "@octokit/types";
|
import { IssuesGetResponseData } from "../Github/Types";
|
||||||
import { Redis, default as redis } from "ioredis";
|
import { Redis, default as redis } from "ioredis";
|
||||||
import LogWrapper from "../LogWrapper";
|
import LogWrapper from "../LogWrapper";
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { IAppserviceStorageProvider } from "matrix-bot-sdk";
|
import { IAppserviceStorageProvider } from "matrix-bot-sdk";
|
||||||
import { IssuesGetResponseData } from "@octokit/types";
|
import { IssuesGetResponseData } from "../Github/Types";
|
||||||
|
|
||||||
export interface IStorageProvider extends IAppserviceStorageProvider {
|
export interface IStorageProvider extends IAppserviceStorageProvider {
|
||||||
setGithubIssue(repo: string, issueNumber: string, data: IssuesGetResponseData, scope?: string): Promise<void>;
|
setGithubIssue(repo: string, issueNumber: string, data: IssuesGetResponseData, scope?: string): Promise<void>;
|
||||||
|
@ -46,12 +46,14 @@ export class UserTokenStore {
|
|||||||
if (existingToken) {
|
if (existingToken) {
|
||||||
return existingToken;
|
return existingToken;
|
||||||
}
|
}
|
||||||
let obj;
|
|
||||||
try {
|
try {
|
||||||
|
let obj;
|
||||||
if (type === "github") {
|
if (type === "github") {
|
||||||
obj = await this.intent.underlyingClient.getAccountData(key);
|
obj = await this.intent.underlyingClient.getAccountData<{encrypted: string}>(key);
|
||||||
} else if (type === "gitlab") {
|
} else if (type === "gitlab") {
|
||||||
obj = await this.intent.underlyingClient.getAccountData(key);
|
obj = await this.intent.underlyingClient.getAccountData<{encrypted: string}>(key);
|
||||||
|
} else {
|
||||||
|
throw Error('Unknown type');
|
||||||
}
|
}
|
||||||
const encryptedTextB64 = obj.encrypted;
|
const encryptedTextB64 = obj.encrypted;
|
||||||
const encryptedText = Buffer.from(encryptedTextB64, "base64");
|
const encryptedText = Buffer.from(encryptedTextB64, "base64");
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { AdminRoom } from "../src/AdminRoom";
|
import { AdminRoom } from "../src/AdminRoom";
|
||||||
|
import { NotifFilter } from "../src/NotificationFilters";
|
||||||
import { UserTokenStore } from "../src/UserTokenStore";
|
import { UserTokenStore } from "../src/UserTokenStore";
|
||||||
import { IntentMock } from "./utils/IntentMock";
|
import { IntentMock } from "./utils/IntentMock";
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ function createAdminRoom(data: any = {admin_user: "@admin:bar"}): [AdminRoom, In
|
|||||||
data.admin_user = "@admin:bar";
|
data.admin_user = "@admin:bar";
|
||||||
}
|
}
|
||||||
const tokenStore = new UserTokenStore("notapath", intent);
|
const tokenStore = new UserTokenStore("notapath", intent);
|
||||||
return [new AdminRoom(ROOM_ID, data, intent, tokenStore, {} as any), intent];
|
return [new AdminRoom(ROOM_ID, data, NotifFilter.getDefaultContent(), intent, tokenStore, {} as any, ), intent];
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("AdminRoom", () => {
|
describe("AdminRoom", () => {
|
||||||
|
@ -2,7 +2,7 @@ import { h, render } from 'preact';
|
|||||||
import 'preact/devtools';
|
import 'preact/devtools';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import "./styling.css";
|
import "./styling.css";
|
||||||
import "fontsource-open-sans/files/open-sans-latin-400-normal.woff2";
|
import "@fontsource/open-sans/files/open-sans-latin-400-normal.woff2";
|
||||||
|
|
||||||
const root = document.getElementsByTagName('main')[0];
|
const root = document.getElementsByTagName('main')[0];
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
@import "fontsource-open-sans/400-normal.css";
|
@import "../node_modules/@fontsource/open-sans/400.css";
|
||||||
@import "mini.css";
|
@import "mini.css";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user