mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 13:17:08 +00:00
Add feature to filter notifications based upon filters
This commit is contained in:
parent
1780778035
commit
6fccf02f94
@ -17,6 +17,7 @@ import { MatrixMessageContent } from "./MatrixEvent";
|
||||
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"];
|
||||
@ -45,21 +46,24 @@ export interface AdminAccountData {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class AdminRoom extends EventEmitter {
|
||||
public static helpMessage: MatrixMessageContent;
|
||||
private widgetAccessToken = `abcdef`;
|
||||
static botCommands: BotCommands;
|
||||
|
||||
private pendingOAuthState: string|null = null;
|
||||
public readonly notifFilter: NotifFilter;
|
||||
|
||||
constructor(public readonly roomId: string,
|
||||
public readonly data: AdminAccountData,
|
||||
notifContent: NotificationFilterStateContent,
|
||||
private botIntent: Intent,
|
||||
private tokenStore: UserTokenStore,
|
||||
private config: BridgeConfig) {
|
||||
super();
|
||||
this.notifFilter = new NotifFilter(notifContent);
|
||||
// TODO: Move this
|
||||
this.backfillAccessToken();
|
||||
}
|
||||
@ -408,8 +412,7 @@ export class AdminRoom extends EventEmitter {
|
||||
}
|
||||
|
||||
@botCommand("gitlab notifications toggle", "Toggle enabling/disabling GitHub notifications in this room", ["instanceName"])
|
||||
// @ts-ignore - property is used
|
||||
private async setGitLabNotificationsStateToggle(instanceName: string) {
|
||||
public async setGitLabNotificationsStateToggle(instanceName: string) {
|
||||
if (!this.config.gitlab) {
|
||||
return this.sendNotice("The bridge is not configured with GitLab support");
|
||||
}
|
||||
@ -437,7 +440,54 @@ 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", "List your saved filters", ["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", "List your saved filters", ["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);
|
||||
return this.sendNotice(`Filter "${name}" disabled for notifications`);
|
||||
} else {
|
||||
this.notifFilter.forNotifications.add(name);
|
||||
return this.sendNotice(`Filter "${name}" enabled for notifications`);
|
||||
}
|
||||
}
|
||||
|
||||
private async saveAccountData(updateFn: (record: AdminAccountData) => AdminAccountData) {
|
||||
@ -540,6 +590,10 @@ export class AdminRoom extends EventEmitter {
|
||||
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
|
||||
|
@ -27,6 +27,7 @@ import { GetIssueResponse, GetIssueOpts } from "./Gitlab/Types"
|
||||
import { GitLabClient } from "./Gitlab/Client";
|
||||
import { BridgeWidgetApi } from "./Widgets/BridgeWidgetApi";
|
||||
import { ProjectsGetResponseData } from "./Github/Types";
|
||||
import { NotifFilter, NotificationFilterStateContent } from "./NotificationFilters";
|
||||
|
||||
const log = new LogWrapper("GithubBridge");
|
||||
|
||||
@ -413,21 +414,35 @@ export class GithubBridge {
|
||||
log.error(`Unable to create connection for ${roomId}`, ex);
|
||||
continue;
|
||||
}
|
||||
this.connections.push(...connections);
|
||||
if (connections.length === 0) {
|
||||
// TODO: Refactor this to be a connection
|
||||
try {
|
||||
const accountData = await this.as.botIntent.underlyingClient.getRoomAccountData<AdminAccountData>(
|
||||
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 {
|
||||
if (this.connections.length) {
|
||||
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) {
|
||||
@ -449,7 +464,7 @@ export class GithubBridge {
|
||||
}
|
||||
await retry(() => this.as.botIntent.joinRoom(roomId), 5);
|
||||
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(
|
||||
BRIDGE_ROOM_TYPE, roomId, room.data,
|
||||
);
|
||||
@ -673,9 +688,9 @@ export class GithubBridge {
|
||||
|
||||
}
|
||||
|
||||
private async setupAdminRoom(roomId: string, accountData: AdminAccountData) {
|
||||
private async setupAdminRoom(roomId: string, accountData: AdminAccountData, notifContent: NotificationFilterStateContent) {
|
||||
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("open.project", async (project: ProjectsGetResponseData) => {
|
||||
|
@ -45,10 +45,16 @@ export default class LogWrapper {
|
||||
log.info(getMessageString(messageOrObject), { module });
|
||||
},
|
||||
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 });
|
||||
},
|
||||
error: (module: string, ...messageOrObject: MsgType[]) => {
|
||||
if (typeof messageOrObject[0] === "object" && 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 });
|
||||
return; // This is just noise :|
|
||||
}
|
||||
|
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;
|
||||
}
|
||||
}
|
@ -8,9 +8,10 @@ import { FormatUtil } from "./FormatUtil";
|
||||
import { PullGetResponseData, IssuesGetResponseData, PullsListRequestedReviewersResponseData, PullsListReviewsResponseData, IssuesGetCommentResponseData } 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();
|
||||
|
||||
export interface IssueDiff {
|
||||
@ -110,7 +111,7 @@ export class NotificationProcessor {
|
||||
for (const event of msg.events) {
|
||||
const isIssueOrPR = event.subject.type === "Issue" || event.subject.type === "PullRequest";
|
||||
try {
|
||||
await this.handleUserNotification(msg.roomId, event);
|
||||
await this.handleUserNotification(msg.roomId, event, adminRoom.notifFilter);
|
||||
if (isIssueOrPR && event.subject.url_data) {
|
||||
const issueNumber = event.subject.url_data.number.toString();
|
||||
await this.storage.setGithubIssue(
|
||||
@ -251,8 +252,15 @@ export class NotificationProcessor {
|
||||
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);
|
||||
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") {
|
||||
return this.matrixSender.sendMatrixMessage(roomId, this.formatSecurityAlert(notif));
|
||||
} else if (notif.subject.type === "Issue" || notif.subject.type === "PullRequest") {
|
||||
|
Loading…
x
Reference in New Issue
Block a user