Share code for tracking config via state events (#418)

* Share code for tracking config via state events

This should prevent forgetting to add state event handlers in new or
existing connection types.

* Address review feedback

* Mark GitLabRepoConnection as an IConnection

* Remove unused imports

* Tighten typing for JiraProjectConnectionState
This commit is contained in:
Andrew Ferrazzutti 2022-07-29 09:57:58 -04:00 committed by GitHub
parent b118a8e606
commit c719f1b926
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 47 additions and 33 deletions

1
changelog.d/418.misc Normal file
View File

@ -0,0 +1 @@
Refactor the way room state is tracked for room-specific configuration, to increase code reuse.

View File

@ -3,33 +3,40 @@ import LogWrapper from "../LogWrapper";
import { MatrixClient } from "matrix-bot-sdk";
import { MatrixMessageContent, MatrixEvent } from "../MatrixEvent";
import { BaseConnection } from "./BaseConnection";
import { PermissionCheckFn } from ".";
import { IConnectionState, PermissionCheckFn } from ".";
const log = new LogWrapper("CommandConnection");
/**
* Connection class that handles commands for a given connection. Should be used
* by connections expecting to handle user input.
*/
export abstract class CommandConnection extends BaseConnection {
export abstract class CommandConnection<StateType extends IConnectionState = IConnectionState> extends BaseConnection {
protected enabledHelpCategories?: string[];
protected includeTitlesInHelp?: boolean;
constructor(
roomId: string,
stateKey: string,
canonicalStateType: string,
protected state: StateType,
private readonly botClient: MatrixClient,
private readonly botCommands: BotCommands,
private readonly helpMessage: HelpFunction,
protected readonly stateCommandPrefix: string,
protected readonly defaultCommandPrefix: string,
protected readonly serviceName?: string,
) {
super(roomId, stateKey, canonicalStateType);
}
}
protected get commandPrefix() {
return this.stateCommandPrefix + " ";
return (this.state.commandPrefix || this.defaultCommandPrefix) + " ";
}
public async onStateUpdate(stateEv: MatrixEvent<unknown>) {
this.state = this.validateConnectionState(stateEv.content);
}
protected abstract validateConnectionState(content: unknown): StateType;
public async onMessageEvent(ev: MatrixEvent<MatrixMessageContent>, checkPermission: PermissionCheckFn) {
const commandResult = await handleCommand(
ev.sender, ev.content.body, this.botCommands, this,checkPermission,

View File

@ -36,7 +36,6 @@ interface IQueryRoomOpts {
export interface GitHubRepoConnectionOptions extends IConnectionState {
ignoreHooks?: AllowedEventsNames[],
commandPrefix?: string;
showIssueRoomLink?: boolean;
prDiff?: {
enabled: boolean;
@ -212,7 +211,7 @@ function compareEmojiStrings(e0: string, e1: string, e0Index = 0) {
* Handles rooms connected to a GitHub repo.
*/
@Connection
export class GitHubRepoConnection extends CommandConnection implements IConnection {
export class GitHubRepoConnection extends CommandConnection<GitHubRepoConnectionState> implements IConnection {
static validateState(state: Record<string, unknown>, isExistingState = false): GitHubRepoConnectionState {
const validator = new Ajv().compile(ConnectionStateSchema);
@ -368,7 +367,7 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
constructor(roomId: string,
private readonly as: Appservice,
private state: GitHubRepoConnectionState,
state: GitHubRepoConnectionState,
private readonly tokenStore: UserTokenStore,
stateKey: string,
private readonly githubInstance: GithubInstance,
@ -378,10 +377,11 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
roomId,
stateKey,
GitHubRepoConnection.CanonicalEventType,
state,
as.botClient,
GitHubRepoConnection.botCommands,
GitHubRepoConnection.helpMessage,
state.commandPrefix || "!gh",
"!gh",
"github",
);
}
@ -415,8 +415,8 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
return this.state.priority || super.priority;
}
public async onStateUpdate(stateEv: MatrixEvent<unknown>) {
this.state = stateEv.content as GitHubRepoConnectionState;
protected validateConnectionState(content: unknown) {
return content as GitHubRepoConnectionState;
}
public isInterestedInStateEvent(eventType: string, stateKey: string) {

View File

@ -3,13 +3,13 @@
import { UserTokenStore } from "../UserTokenStore";
import { Appservice, StateEvent } from "matrix-bot-sdk";
import { BotCommands, botCommand, compileBotCommands } from "../BotCommands";
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
import { MatrixMessageContent } from "../MatrixEvent";
import markdown from "markdown-it";
import LogWrapper from "../LogWrapper";
import { BridgeConfigGitLab, GitLabInstance } from "../Config/Config";
import { IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "../Gitlab/WebhookTypes";
import { CommandConnection } from "./CommandConnection";
import { Connection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
import { GetConnectionsResponseItem } from "../provisioning/api";
import { ErrCode, ApiError, ValidatorApiError } from "../api"
import { AccessLevel } from "../Gitlab/Types";
@ -19,7 +19,6 @@ export interface GitLabRepoConnectionState extends IConnectionState {
instance: string;
path: string;
ignoreHooks?: AllowedEventsNames[],
commandPrefix?: string;
pushTagsRegex?: string,
includingLabels?: string[];
excludingLabels?: string[];
@ -130,7 +129,7 @@ export interface GitLabTargetFilter {
* Handles rooms connected to a GitLab repo.
*/
@Connection
export class GitLabRepoConnection extends CommandConnection {
export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnectionState> implements IConnection {
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.gitlab.repository";
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.gitlab.repository";
@ -277,17 +276,18 @@ export class GitLabRepoConnection extends CommandConnection {
constructor(roomId: string,
stateKey: string,
private readonly as: Appservice,
private state: GitLabRepoConnectionState,
state: GitLabRepoConnectionState,
private readonly tokenStore: UserTokenStore,
private readonly instance: GitLabInstance) {
super(
roomId,
stateKey,
GitLabRepoConnection.CanonicalEventType,
state,
as.botClient,
GitLabRepoConnection.botCommands,
GitLabRepoConnection.helpMessage,
state.commandPrefix || "!gl",
"!gl",
"gitlab",
)
if (!state.path || !state.instance) {
@ -303,9 +303,8 @@ export class GitLabRepoConnection extends CommandConnection {
return this.state.priority || super.priority;
}
public async onStateUpdate(stateEv: MatrixEvent<unknown>) {
const state = stateEv.content as GitLabRepoConnectionState;
this.state = state;
protected validateConnectionState(content: unknown) {
return content as GitLabRepoConnectionState;
}
public isInterestedInStateEvent(eventType: string, stateKey: string) {

View File

@ -14,6 +14,7 @@ export type PermissionCheckFn = (service: string, level: BridgePermissionLevel)
export interface IConnectionState {
priority?: number;
commandPrefix?: string;
}
export interface IConnection {

View File

@ -7,7 +7,7 @@ import markdownit from "markdown-it";
import { generateJiraWebLinkFromIssue } from "../Jira";
import { JiraProject } from "../Jira/Types";
import { botCommand, BotCommands, compileBotCommands } from "../BotCommands";
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
import { MatrixMessageContent } from "../MatrixEvent";
import { CommandConnection } from "./CommandConnection";
import { UserTokenStore } from "../UserTokenStore";
import { CommandError, NotLoggedInError } from "../errors";
@ -19,13 +19,12 @@ const JiraAllowedEvents: JiraAllowedEventsNames[] = ["issue.created"];
export interface JiraProjectConnectionState extends IConnectionState {
// legacy field, prefer url
id?: string;
url?: string;
url: string;
events?: JiraAllowedEventsNames[],
commandPrefix?: string;
}
function validateJiraConnectionState(state: JiraProjectConnectionState) {
const {url, commandPrefix, events, priority} = state as JiraProjectConnectionState;
function validateJiraConnectionState(state: unknown): JiraProjectConnectionState {
const {url, commandPrefix, events, priority} = state as Partial<JiraProjectConnectionState>;
if (url === undefined) {
throw new ApiError("Expected a 'url' property", ErrCode.BadValue);
}
@ -50,7 +49,7 @@ const md = new markdownit();
* Handles rooms connected to a Jira project.
*/
@Connection
export class JiraProjectConnection extends CommandConnection implements IConnection {
export class JiraProjectConnection extends CommandConnection<JiraProjectConnectionState> implements IConnection {
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.jira.project";
@ -83,7 +82,7 @@ export class JiraProjectConnection extends CommandConnection implements IConnect
if (!jiraResourceClient) {
throw new ApiError("User is not authenticated with this JIRA instance", ErrCode.ForbiddenUser);
}
const connection = new JiraProjectConnection(roomId, as, data, validData.url, tokenStore);
const connection = new JiraProjectConnection(roomId, as, validData, validData.url, tokenStore);
log.debug(`projectKey for ${validData.url} is ${connection.projectKey}`);
if (!connection.projectKey) {
throw Error('Expected projectKey to be defined');
@ -151,17 +150,18 @@ export class JiraProjectConnection extends CommandConnection implements IConnect
constructor(roomId: string,
private readonly as: Appservice,
private state: JiraProjectConnectionState,
state: JiraProjectConnectionState,
stateKey: string,
private readonly tokenStore: UserTokenStore,) {
super(
roomId,
stateKey,
JiraProjectConnection.CanonicalEventType,
state,
as.botClient,
JiraProjectConnection.botCommands,
JiraProjectConnection.helpMessage,
state.commandPrefix || "!jira",
"!jira",
"jira"
);
if (state.url) {
@ -178,9 +178,8 @@ export class JiraProjectConnection extends CommandConnection implements IConnect
return JiraProjectConnection.EventTypes.includes(eventType) && this.stateKey === stateKey;
}
public async onStateUpdate(event: MatrixEvent<unknown>) {
const validatedConfig = validateJiraConnectionState(event.content as JiraProjectConnectionState);
this.state = validatedConfig;
protected validateConnectionState(content: unknown) {
return validateJiraConnectionState(content);
}
public async onJiraIssueCreated(data: JiraIssueEvent) {

View File

@ -12,7 +12,7 @@ import { URL } from "url";
import { SetupWidget } from "../Widgets/SetupWidget";
import { AdminRoom } from "../AdminRoom";
import { GitLabRepoConnection } from "./GitlabRepo";
import { ProvisionConnectionOpts } from "./IConnection";
import { IConnectionState, ProvisionConnectionOpts } from "./IConnection";
import LogWrapper from "../LogWrapper";
const md = new markdown();
const log = new LogWrapper("SetupConnection");
@ -34,6 +34,11 @@ export class SetupConnection extends CommandConnection {
return this.provisionOpts.as;
}
protected validateConnectionState(content: unknown) {
log.warn("SetupConnection has no state to be validated");
return content as IConnectionState;
}
constructor(public readonly roomId: string,
private readonly provisionOpts: ProvisionConnectionOpts,
private readonly getOrCreateAdminRoom: (userId: string) => Promise<AdminRoom>,) {
@ -41,6 +46,8 @@ export class SetupConnection extends CommandConnection {
roomId,
"",
"",
// TODO Consider storing room-specific config in state.
{},
provisionOpts.as.botClient,
SetupConnection.botCommands,
SetupConnection.helpMessage,