mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 21:19:13 +00:00
Statically define connection creation and provisioing (#330)
* Statically define connection creation and provisioing * Tidy up * Drop JIRA * Convert other connections to new system * Small linting fixes * Fixes * changelog * Fix bridge * Fix JIRA instance naming * Fix JIRA config * Drop unnessacery check
This commit is contained in:
parent
2008f2cae0
commit
b23e516aa5
1
changelog.d/330.misc
Normal file
1
changelog.d/330.misc
Normal file
@ -0,0 +1 @@
|
||||
Refactor connection handling logic to improve developer experience.
|
@ -5,12 +5,11 @@ import { BridgeConfig, BridgePermissionLevel, GitLabInstance } from "./Config/Co
|
||||
import { BridgeWidgetApi } from "./Widgets/BridgeWidgetApi";
|
||||
import { CommentProcessor } from "./CommentProcessor";
|
||||
import { ConnectionManager } from "./ConnectionManager";
|
||||
import { GenericHookConnection } from "./Connections";
|
||||
import { GetIssueResponse, GetIssueOpts } from "./Gitlab/Types"
|
||||
import { GithubInstance } from "./Github/GithubInstance";
|
||||
import { IBridgeStorageProvider } from "./Stores/StorageProvider";
|
||||
import { IConnection, GitHubDiscussionSpace, GitHubDiscussionConnection, GitHubUserSpace, JiraProjectConnection, GitLabRepoConnection,
|
||||
GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitLabIssueConnection, FigmaFileConnection, FeedConnection } from "./Connections";
|
||||
GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitLabIssueConnection, FigmaFileConnection, FeedConnection, GenericHookConnection } from "./Connections";
|
||||
import { IGitLabWebhookIssueStateEvent, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "./Gitlab/WebhookTypes";
|
||||
import { JiraIssueEvent, JiraIssueUpdatedEvent } from "./Jira/WebhookTypes";
|
||||
import { JiraOAuthResult } from "./Jira/Types";
|
||||
@ -39,7 +38,6 @@ import { ListenerService } from "./ListenerService";
|
||||
import { SetupConnection } from "./Connections/SetupConnection";
|
||||
import { getAppservice } from "./appservice";
|
||||
import { JiraOAuthRequestCloud, JiraOAuthRequestOnPrem, JiraOAuthRequestResult } from "./Jira/OAuth";
|
||||
import { CLOUD_INSTANCE } from "./Jira/Client";
|
||||
import { GenericWebhookEvent, GenericWebhookEventResult } from "./generic/types";
|
||||
import { SetupWidget } from "./Widgets/SetupWidget";
|
||||
import { FeedEntry, FeedError, FeedReader } from "./feeds/FeedReader";
|
||||
@ -73,7 +71,7 @@ export class Bridge {
|
||||
}
|
||||
this.as = getAppservice(this.config, this.registration, this.storage);
|
||||
Metrics.registerMatrixSdkMetrics(this.as);
|
||||
this.queue = createMessageQueue(this.config);
|
||||
this.queue = createMessageQueue(this.config.queue);
|
||||
this.messageClient = new MessageSenderClient(this.queue);
|
||||
this.commentProcessor = new CommentProcessor(this.as, this.config.bridge.mediaUrl || this.config.bridge.url);
|
||||
this.notifProcessor = new NotificationProcessor(this.storage, this.messageClient);
|
||||
@ -509,18 +507,15 @@ export class Bridge {
|
||||
}
|
||||
try {
|
||||
let tokenInfo: JiraOAuthResult;
|
||||
let instance;
|
||||
if ("code" in msg.data) {
|
||||
tokenInfo = await this.tokenStore.jiraOAuth.exchangeRequestForToken(msg.data.code);
|
||||
instance = CLOUD_INSTANCE;
|
||||
} else {
|
||||
tokenInfo = await this.tokenStore.jiraOAuth.exchangeRequestForToken(msg.data.oauthToken, msg.data.oauthVerifier);
|
||||
instance = new URL(this.config.jira.url!).host;
|
||||
}
|
||||
await this.tokenStore.storeJiraToken(userId, {
|
||||
access_token: tokenInfo.access_token,
|
||||
refresh_token: tokenInfo.refresh_token,
|
||||
instance,
|
||||
instance: this.config.jira.instanceName,
|
||||
expires_in: tokenInfo.expires_in,
|
||||
});
|
||||
|
||||
@ -836,9 +831,17 @@ export class Bridge {
|
||||
try {
|
||||
await (
|
||||
new SetupConnection(
|
||||
roomId, this.as, this.tokenStore, this.config,
|
||||
roomId,
|
||||
{
|
||||
config: this.config,
|
||||
as: this.as,
|
||||
tokenStore: this.tokenStore,
|
||||
commentProcessor: this.commentProcessor,
|
||||
messageClient: this.messageClient,
|
||||
storage: this.storage,
|
||||
getAllConnectionsOfType: this.connectionManager.getAllConnectionsOfType.bind(this),
|
||||
},
|
||||
this.getOrCreateAdminRoom.bind(this),
|
||||
this.github,
|
||||
)
|
||||
).onMessageEvent(event, checkPermission);
|
||||
} catch (ex) {
|
||||
|
@ -108,6 +108,8 @@ export interface BridgeConfigJiraYAML {
|
||||
|
||||
}
|
||||
export class BridgeConfigJira implements BridgeConfigJiraYAML {
|
||||
static CLOUD_INSTANCE_NAME = "api.atlassian.com";
|
||||
|
||||
@configKey("Webhook settings for JIRA")
|
||||
readonly webhook: {
|
||||
secret: string;
|
||||
@ -123,13 +125,16 @@ export class BridgeConfigJira implements BridgeConfigJiraYAML {
|
||||
@hideKey()
|
||||
readonly instanceUrl?: URL;
|
||||
|
||||
@hideKey()
|
||||
readonly instanceName: string;
|
||||
|
||||
constructor(yaml: BridgeConfigJiraYAML) {
|
||||
assert.ok(yaml.webhook);
|
||||
assert.ok(yaml.webhook.secret);
|
||||
this.webhook = yaml.webhook;
|
||||
this.url = yaml.url;
|
||||
this.instanceUrl = yaml.url !== undefined ? new URL(yaml.url) : undefined;
|
||||
|
||||
this.instanceName = this.instanceUrl?.host || BridgeConfigJira.CLOUD_INSTANCE_NAME;
|
||||
if (!yaml.oauth) {
|
||||
return;
|
||||
}
|
||||
@ -314,7 +319,7 @@ interface BridgeConfigWebhook {
|
||||
bindAddress?: string;
|
||||
}
|
||||
|
||||
interface BridgeConfigQueue {
|
||||
export interface BridgeConfigQueue {
|
||||
monolithic: boolean;
|
||||
port?: number;
|
||||
host?: string;
|
||||
@ -350,11 +355,11 @@ export interface BridgeConfigRoot {
|
||||
figma?: BridgeConfigFigma;
|
||||
feeds?: BridgeConfigFeeds;
|
||||
generic?: BridgeGenericWebhooksConfigYAML;
|
||||
github?: BridgeConfigGitHub;
|
||||
github?: BridgeConfigGitHubYAML;
|
||||
gitlab?: BridgeConfigGitLabYAML;
|
||||
permissions?: BridgeConfigActorPermission[];
|
||||
provisioning?: BridgeConfigProvisioning;
|
||||
jira?: BridgeConfigJira;
|
||||
jira?: BridgeConfigJiraYAML;
|
||||
logging: BridgeConfigLogging;
|
||||
passFile: string;
|
||||
queue: BridgeConfigQueue;
|
||||
|
@ -7,8 +7,7 @@
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import { CommentProcessor } from "./CommentProcessor";
|
||||
import { BridgeConfig, BridgePermissionLevel, GitLabInstance } from "./Config/Config";
|
||||
import { GenericHookConnection, GitHubDiscussionConnection, GitHubDiscussionSpace, GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitHubUserSpace, GitLabIssueConnection, GitLabRepoConnection, IConnection, JiraProjectConnection } from "./Connections";
|
||||
import { GenericHookAccountData } from "./Connections/GenericHook";
|
||||
import { ConnectionDeclarations, GenericHookConnection, GitHubDiscussionConnection, GitHubDiscussionSpace, GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitHubUserSpace, GitLabIssueConnection, GitLabRepoConnection, IConnection, JiraProjectConnection } from "./Connections";
|
||||
import { GithubInstance } from "./Github/GithubInstance";
|
||||
import { GitLabClient } from "./Gitlab/Client";
|
||||
import { JiraProject } from "./Jira/Types";
|
||||
@ -17,7 +16,6 @@ import { MessageSenderClient } from "./MatrixSender";
|
||||
import { GetConnectionTypeResponseItem } from "./provisioning/api";
|
||||
import { ApiError, ErrCode } from "./api";
|
||||
import { UserTokenStore } from "./UserTokenStore";
|
||||
import {v4 as uuid} from "uuid";
|
||||
import { FigmaFileConnection, FeedConnection } from "./Connections";
|
||||
import { IBridgeStorageProvider } from "./Stores/StorageProvider";
|
||||
import Metrics from "./Metrics";
|
||||
@ -73,80 +71,28 @@ export class ConnectionManager extends EventEmitter {
|
||||
*/
|
||||
public async provisionConnection(roomId: string, userId: string, type: string, data: Record<string, unknown>): Promise<IConnection> {
|
||||
log.info(`Looking to provision connection for ${roomId} ${type} for ${userId} with ${data}`);
|
||||
const existingConnections = await this.getAllConnectionsForRoom(roomId);
|
||||
if (JiraProjectConnection.EventTypes.includes(type)) {
|
||||
if (!this.config.jira) {
|
||||
throw new ApiError('JIRA integration is not configured', ErrCode.DisabledFeature);
|
||||
const connectionType = ConnectionDeclarations.find(c => c.EventTypes.includes(type));
|
||||
if (connectionType?.provisionConnection) {
|
||||
if (!this.config.checkPermission(userId, connectionType.ServiceCategory, BridgePermissionLevel.manageConnections)) {
|
||||
throw new ApiError(`User is not permitted to provision connections for this type of service.`, ErrCode.ForbiddenUser);
|
||||
}
|
||||
if (existingConnections.find(c => c instanceof JiraProjectConnection)) {
|
||||
// TODO: Support this.
|
||||
throw Error("Cannot support multiple connections of the same type yet");
|
||||
}
|
||||
if (!this.config.checkPermission(userId, "jira", BridgePermissionLevel.manageConnections)) {
|
||||
throw new ApiError('User is not permitted to provision connections for Jira', ErrCode.ForbiddenUser);
|
||||
}
|
||||
const res = await JiraProjectConnection.provisionConnection(roomId, userId, data, this.as, this.tokenStore);
|
||||
await this.as.botIntent.underlyingClient.sendStateEvent(roomId, JiraProjectConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent);
|
||||
this.push(res.connection);
|
||||
return res.connection;
|
||||
}
|
||||
if (GitHubRepoConnection.EventTypes.includes(type)) {
|
||||
if (!this.config.github || !this.config.github.oauth || !this.github) {
|
||||
throw new ApiError('GitHub integration is not configured', ErrCode.DisabledFeature);
|
||||
}
|
||||
if (existingConnections.find(c => c instanceof GitHubRepoConnection)) {
|
||||
// TODO: Support this.
|
||||
throw Error("Cannot support multiple connections of the same type yet");
|
||||
}
|
||||
if (!this.config.checkPermission(userId, "github", BridgePermissionLevel.manageConnections)) {
|
||||
throw new ApiError('User is not permitted to provision connections for GitHub', ErrCode.ForbiddenUser);
|
||||
}
|
||||
const res = await GitHubRepoConnection.provisionConnection(roomId, userId, data, this.as, this.tokenStore, this.github, this.config.github);
|
||||
await this.as.botIntent.underlyingClient.sendStateEvent(roomId, GitHubRepoConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent);
|
||||
this.push(res.connection);
|
||||
return res.connection;
|
||||
}
|
||||
if (GenericHookConnection.EventTypes.includes(type)) {
|
||||
if (!this.config.generic) {
|
||||
throw new ApiError('Generic Webhook integration is not configured', ErrCode.DisabledFeature);
|
||||
}
|
||||
if (!this.config.checkPermission(userId, "webhooks", BridgePermissionLevel.manageConnections)) {
|
||||
throw new ApiError('User is not permitted to provision connections for generic webhooks', ErrCode.ForbiddenUser);
|
||||
}
|
||||
const res = await GenericHookConnection.provisionConnection(roomId, this.as, data, this.config.generic, this.messageClient);
|
||||
const existing = this.getAllConnectionsOfType(GenericHookConnection).find(c => c.stateKey === res.connection.stateKey);
|
||||
if (existing) {
|
||||
throw new ApiError("A generic webhook with this name already exists", ErrCode.ConflictingConnection, -1, {
|
||||
existingConnection: existing.getProvisionerDetails()
|
||||
const { connection } = await connectionType.provisionConnection(roomId, userId, data, {
|
||||
as: this.as,
|
||||
config: this.config,
|
||||
tokenStore: this.tokenStore,
|
||||
commentProcessor: this.commentProcessor,
|
||||
messageClient: this.messageClient,
|
||||
storage: this.storage,
|
||||
github: this.github,
|
||||
getAllConnectionsOfType: this.getAllConnectionsOfType.bind(this),
|
||||
});
|
||||
}
|
||||
await GenericHookConnection.ensureRoomAccountData(roomId, this.as, res.connection.hookId, res.connection.stateKey);
|
||||
await this.as.botIntent.underlyingClient.sendStateEvent(roomId, GenericHookConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent);
|
||||
this.push(res.connection);
|
||||
return res.connection;
|
||||
}
|
||||
if (GitLabRepoConnection.EventTypes.includes(type)) {
|
||||
if (!this.config.gitlab) {
|
||||
throw new ApiError('GitLab integration is not configured', ErrCode.DisabledFeature);
|
||||
}
|
||||
if (!this.config.checkPermission(userId, "gitlab", BridgePermissionLevel.manageConnections)) {
|
||||
throw new ApiError('User is not permitted to provision connections for GitLab', ErrCode.ForbiddenUser);
|
||||
}
|
||||
const res = await GitLabRepoConnection.provisionConnection(roomId, userId, data, this.as, this.tokenStore, data.instance as string, this.config.gitlab);
|
||||
const existing = this.getAllConnectionsOfType(GitLabRepoConnection).find(c => c.stateKey === res.connection.stateKey);
|
||||
if (existing) {
|
||||
throw new ApiError("A GitLab repo connection for this project already exists", ErrCode.ConflictingConnection, -1, {
|
||||
existingConnection: existing.getProvisionerDetails()
|
||||
});
|
||||
}
|
||||
await this.as.botIntent.underlyingClient.sendStateEvent(roomId, GitLabRepoConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent);
|
||||
this.push(res.connection);
|
||||
return res.connection;
|
||||
this.push(connection);
|
||||
return connection;
|
||||
}
|
||||
throw new ApiError(`Connection type not known`);
|
||||
}
|
||||
|
||||
private assertStateAllowed(state: StateEvent<any>, serviceType: "github"|"gitlab"|"jira"|"figma"|"webhooks"|"feed") {
|
||||
private assertStateAllowed(state: StateEvent<any>, serviceType: string) {
|
||||
if (state.sender === this.as.botUserId) {
|
||||
return;
|
||||
}
|
||||
@ -161,146 +107,21 @@ export class ConnectionManager extends EventEmitter {
|
||||
log.debug(`${roomId} has disabled state for ${state.type}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (GitHubRepoConnection.EventTypes.includes(state.type)) {
|
||||
if (!this.github || !this.config.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
this.assertStateAllowed(state, "github");
|
||||
return new GitHubRepoConnection(roomId, this.as, state.content, this.tokenStore, state.stateKey, this.github, this.config.github);
|
||||
}
|
||||
|
||||
if (GitHubDiscussionConnection.EventTypes.includes(state.type)) {
|
||||
if (!this.github || !this.config.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
this.assertStateAllowed(state, "github");
|
||||
return new GitHubDiscussionConnection(
|
||||
roomId, this.as, state.content, state.stateKey, this.tokenStore, this.commentProcessor,
|
||||
this.messageClient, this.config.github,
|
||||
);
|
||||
}
|
||||
|
||||
if (GitHubDiscussionSpace.EventTypes.includes(state.type)) {
|
||||
if (!this.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
this.assertStateAllowed(state, "github");
|
||||
|
||||
return new GitHubDiscussionSpace(
|
||||
await this.as.botClient.getSpace(roomId), state.content, state.stateKey
|
||||
);
|
||||
}
|
||||
|
||||
if (GitHubIssueConnection.EventTypes.includes(state.type)) {
|
||||
if (!this.github || !this.config.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
|
||||
this.assertStateAllowed(state, "github");
|
||||
const issue = new GitHubIssueConnection(
|
||||
roomId, this.as, state.content, state.stateKey || "", this.tokenStore,
|
||||
this.commentProcessor, this.messageClient, this.github, this.config.github,
|
||||
);
|
||||
await issue.syncIssueState();
|
||||
return issue;
|
||||
}
|
||||
|
||||
if (GitHubUserSpace.EventTypes.includes(state.type)) {
|
||||
if (!this.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
|
||||
this.assertStateAllowed(state, "github");
|
||||
return new GitHubUserSpace(
|
||||
await this.as.botClient.getSpace(roomId), state.content, state.stateKey
|
||||
);
|
||||
}
|
||||
|
||||
if (GitLabRepoConnection.EventTypes.includes(state.type)) {
|
||||
if (!this.config.gitlab) {
|
||||
throw Error('GitLab is not configured');
|
||||
}
|
||||
|
||||
this.assertStateAllowed(state, "gitlab");
|
||||
const instance = this.config.gitlab.instances[state.content.instance];
|
||||
if (!instance) {
|
||||
throw Error('Instance name not recognised');
|
||||
}
|
||||
return new GitLabRepoConnection(roomId, state.stateKey, this.as, state.content, this.tokenStore, instance);
|
||||
}
|
||||
|
||||
if (GitLabIssueConnection.EventTypes.includes(state.type)) {
|
||||
if (!this.github || !this.config.gitlab) {
|
||||
throw Error('GitLab is not configured');
|
||||
}
|
||||
this.assertStateAllowed(state, "gitlab");
|
||||
const instance = this.config.gitlab.instances[state.content.instance];
|
||||
return new GitLabIssueConnection(
|
||||
roomId,
|
||||
this.as,
|
||||
state.content,
|
||||
state.stateKey as string,
|
||||
this.tokenStore,
|
||||
this.commentProcessor,
|
||||
this.messageClient,
|
||||
instance,
|
||||
this.config.gitlab,
|
||||
);
|
||||
}
|
||||
|
||||
if (JiraProjectConnection.EventTypes.includes(state.type)) {
|
||||
if (!this.config.jira) {
|
||||
throw Error('JIRA is not configured');
|
||||
}
|
||||
this.assertStateAllowed(state, "jira");
|
||||
return new JiraProjectConnection(roomId, this.as, state.content, state.stateKey, this.tokenStore);
|
||||
}
|
||||
|
||||
if (FigmaFileConnection.EventTypes.includes(state.type)) {
|
||||
if (!this.config.figma) {
|
||||
throw Error('Figma is not configured');
|
||||
}
|
||||
this.assertStateAllowed(state, "figma");
|
||||
return new FigmaFileConnection(roomId, state.stateKey, state.content, this.config.figma, this.as, this.storage);
|
||||
}
|
||||
|
||||
if (FeedConnection.EventTypes.includes(state.type)) {
|
||||
if (!this.config.feeds?.enabled) {
|
||||
throw Error('RSS/Atom feeds are not configured');
|
||||
}
|
||||
this.assertStateAllowed(state, "feed");
|
||||
return new FeedConnection(roomId, state.stateKey, state.content, this.config.feeds, this.as, this.storage);
|
||||
}
|
||||
|
||||
if (GenericHookConnection.EventTypes.includes(state.type) && this.config.generic?.enabled) {
|
||||
if (!this.config.generic) {
|
||||
throw Error('Generic webhooks are not configured');
|
||||
}
|
||||
this.assertStateAllowed(state, "webhooks");
|
||||
// Generic hooks store the hookId in the account data
|
||||
const acctData = await this.as.botClient.getSafeRoomAccountData<GenericHookAccountData>(GenericHookConnection.CanonicalEventType, roomId, {});
|
||||
// hookId => stateKey
|
||||
let hookId = Object.entries(acctData).find(([, v]) => v === state.stateKey)?.[0];
|
||||
if (!hookId) {
|
||||
hookId = uuid();
|
||||
log.warn(`hookId for ${roomId} not set in accountData, setting to ${hookId}`);
|
||||
await GenericHookConnection.ensureRoomAccountData(roomId, this.as, hookId, state.stateKey);
|
||||
}
|
||||
|
||||
return new GenericHookConnection(
|
||||
roomId,
|
||||
state.content,
|
||||
hookId,
|
||||
state.stateKey,
|
||||
this.messageClient,
|
||||
this.config.generic,
|
||||
this.as,
|
||||
);
|
||||
}
|
||||
const connectionType = ConnectionDeclarations.find(c => c.EventTypes.includes(state.type));
|
||||
if (!connectionType) {
|
||||
return;
|
||||
}
|
||||
this.assertStateAllowed(state, connectionType.ServiceCategory);
|
||||
return connectionType.createConnectionForState(roomId, state, {
|
||||
as: this.as,
|
||||
config: this.config,
|
||||
tokenStore: this.tokenStore,
|
||||
commentProcessor: this.commentProcessor,
|
||||
messageClient: this.messageClient,
|
||||
storage: this.storage,
|
||||
github: this.github,
|
||||
});
|
||||
}
|
||||
|
||||
public async createConnectionsForRoomId(roomId: string) {
|
||||
const state = await this.as.botClient.getRoomState(roomId);
|
||||
@ -395,7 +216,6 @@ export class ConnectionManager extends EventEmitter {
|
||||
c.isInterestedInHookEvent(eventName))) as JiraProjectConnection[];
|
||||
}
|
||||
|
||||
|
||||
public getConnectionsForGenericWebhook(hookId: string): GenericHookConnection[] {
|
||||
return this.connections.filter((c) => (c instanceof GenericHookConnection && c.hookId === hookId)) as GenericHookConnection[];
|
||||
}
|
||||
@ -470,7 +290,7 @@ export class ConnectionManager extends EventEmitter {
|
||||
/**
|
||||
* Get a list of possible targets for a given connection type when provisioning
|
||||
* @param userId
|
||||
* @param arg1
|
||||
* @param type
|
||||
*/
|
||||
async getConnectionTargets(userId: string, type: string, filters: Record<string, unknown> = {}): Promise<unknown[]> {
|
||||
if (type === GitLabRepoConnection.CanonicalEventType) {
|
||||
|
@ -1,11 +1,12 @@
|
||||
import {Appservice} from "matrix-bot-sdk";
|
||||
import { IConnection, IConnectionState } from ".";
|
||||
import {Appservice, StateEvent} from "matrix-bot-sdk";
|
||||
import { IConnection, IConnectionState, InstantiateConnectionOpts } from ".";
|
||||
import { BridgeConfigFeeds } from "../Config/Config";
|
||||
import { FeedEntry, FeedError} from "../feeds/FeedReader";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { IBridgeStorageProvider } from "../Stores/StorageProvider";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
import markdown from "markdown-it";
|
||||
import { Connection } from "./IConnection";
|
||||
|
||||
const log = new LogWrapper("FeedConnection");
|
||||
const md = new markdown();
|
||||
@ -14,9 +15,20 @@ export interface FeedConnectionState extends IConnectionState {
|
||||
url: string;
|
||||
}
|
||||
|
||||
@Connection
|
||||
export class FeedConnection extends BaseConnection implements IConnection {
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.feed";
|
||||
static readonly EventTypes = [ FeedConnection.CanonicalEventType ];
|
||||
static readonly ServiceCategory = "feed";
|
||||
|
||||
public static createConnectionForState(roomId: string, event: StateEvent<any>, {config, as, storage}: InstantiateConnectionOpts) {
|
||||
if (!config.feeds?.enabled) {
|
||||
throw Error('RSS/Atom feeds are not configured');
|
||||
}
|
||||
return new FeedConnection(roomId, event.stateKey, event.content, config.feeds, as, storage);
|
||||
}
|
||||
|
||||
|
||||
private hasError = false;
|
||||
|
||||
public get feedUrl(): string {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Appservice, MatrixClient } from "matrix-bot-sdk";
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import markdownit from "markdown-it";
|
||||
import { FigmaPayload } from "../figma/types";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
@ -6,17 +6,19 @@ import { IConnection, IConnectionState } from ".";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { IBridgeStorageProvider } from "../Stores/StorageProvider";
|
||||
import { BridgeConfigFigma } from "../Config/Config";
|
||||
import { Connection, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
|
||||
|
||||
const log = new LogWrapper("FigmaFileConnection");
|
||||
|
||||
export interface FigmaFileConnectionState extends IConnectionState {
|
||||
fileId?: string;
|
||||
fileId: string;
|
||||
instanceName?: string;
|
||||
}
|
||||
|
||||
const THREAD_RELATION_TYPE = "m.thread";
|
||||
|
||||
const md = markdownit();
|
||||
@Connection
|
||||
export class FigmaFileConnection extends BaseConnection implements IConnection {
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.figma.file";
|
||||
static readonly LegacyEventType = "uk.half-shot.matrix-figma.file"; // Magically import from matrix-figma
|
||||
@ -25,11 +27,40 @@ export class FigmaFileConnection extends BaseConnection implements IConnection {
|
||||
FigmaFileConnection.CanonicalEventType,
|
||||
FigmaFileConnection.LegacyEventType,
|
||||
];
|
||||
static readonly ServiceCategory = "figma";
|
||||
|
||||
public static async createState(roomId: string, fileId: string, client: MatrixClient) {
|
||||
await client.sendStateEvent(roomId, FigmaFileConnection.CanonicalEventType, fileId, {
|
||||
fileId: fileId,
|
||||
} as FigmaFileConnectionState);
|
||||
|
||||
public static validateState(data: Record<string, unknown>): FigmaFileConnectionState {
|
||||
if (!data.fileId || typeof data.fileId !== "string") {
|
||||
throw Error('Missing or invalid fileId');
|
||||
}
|
||||
if (data.instanceName && typeof data.instanceName !== "string") {
|
||||
throw Error('Invalid instanceName');
|
||||
}
|
||||
return {
|
||||
instanceName: typeof data.instanceName === "string" ? data.instanceName : undefined,
|
||||
fileId: data.fileId,
|
||||
}
|
||||
}
|
||||
|
||||
public static createConnectionForState(roomId: string, event: StateEvent<any>, {config, as, storage}: InstantiateConnectionOpts) {
|
||||
if (!config.figma) {
|
||||
throw Error('Figma is not configured');
|
||||
}
|
||||
return new FigmaFileConnection(roomId, event.stateKey, event.content, config.figma, as, storage);
|
||||
}
|
||||
|
||||
static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown> = {}, {as, config, storage}: ProvisionConnectionOpts) {
|
||||
if (!config.figma) {
|
||||
throw Error('Figma is not configured');
|
||||
}
|
||||
const validState = this.validateState(data);
|
||||
const connection = new FigmaFileConnection(roomId, validState.fileId, validState, config.figma, as, storage);
|
||||
await as.botClient.sendStateEvent(roomId, FigmaFileConnection.CanonicalEventType, validState.fileId, validState);
|
||||
return {
|
||||
connection,
|
||||
stateEventContent: validState,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { IConnection, IConnectionState } from "./IConnection";
|
||||
import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { MessageSenderClient } from "../MatrixSender"
|
||||
import markdownit from "markdown-it";
|
||||
import { VMScript as Script, NodeVM } from "vm2";
|
||||
import { MatrixEvent } from "../MatrixEvent";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import { v4 as uuid} from "uuid";
|
||||
import { ApiError, ErrCode } from "../api";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
import { GetConnectionsResponseItem } from "../provisioning/api";
|
||||
import { BridgeConfigGenericWebhooks } from "../Config/Config";
|
||||
|
||||
export interface GenericHookConnectionState extends IConnectionState {
|
||||
/**
|
||||
* This is ONLY used for display purposes, but the account data value is used to prevent misuse.
|
||||
@ -59,13 +60,14 @@ const TRANSFORMATION_TIMEOUT_MS = 500;
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
@Connection
|
||||
export class GenericHookConnection extends BaseConnection implements IConnection {
|
||||
|
||||
static validateState(state: Record<string, unknown>, allowJsTransformationFunctions: boolean): GenericHookConnectionState {
|
||||
static validateState(state: Record<string, unknown>, allowJsTransformationFunctions?: boolean): GenericHookConnectionState {
|
||||
const {name, transformationFunction} = state;
|
||||
let transformationFunctionResult: string|undefined;
|
||||
if (transformationFunction) {
|
||||
if (!allowJsTransformationFunctions) {
|
||||
if (allowJsTransformationFunctions !== undefined && !allowJsTransformationFunctions) {
|
||||
throw new ApiError('Transformation functions are not allowed', ErrCode.DisabledFeature);
|
||||
}
|
||||
if (typeof transformationFunction !== "string") {
|
||||
@ -85,13 +87,41 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
};
|
||||
}
|
||||
|
||||
static async provisionConnection(roomId: string, as: Appservice, data: Record<string, unknown> = {}, config: BridgeConfigGenericWebhooks, messageClient: MessageSenderClient) {
|
||||
const hookId = uuid();
|
||||
const validState: GenericHookConnectionState = {
|
||||
...GenericHookConnection.validateState(data, config.allowJsTransformationFunctions || false),
|
||||
static async createConnectionForState(roomId: string, event: StateEvent<Record<string, unknown>>, {as, config, messageClient}: InstantiateConnectionOpts) {
|
||||
if (!config.generic) {
|
||||
throw Error('Generic webhooks are not configured');
|
||||
}
|
||||
// Generic hooks store the hookId in the account data
|
||||
const acctData = await as.botClient.getSafeRoomAccountData<GenericHookAccountData>(GenericHookConnection.CanonicalEventType, roomId, {});
|
||||
const state = this.validateState(event.content);
|
||||
// hookId => stateKey
|
||||
let hookId = Object.entries(acctData).find(([, v]) => v === event.stateKey)?.[0];
|
||||
if (!hookId) {
|
||||
hookId = uuid();
|
||||
log.warn(`hookId for ${roomId} not set in accountData, setting to ${hookId}`);
|
||||
await GenericHookConnection.ensureRoomAccountData(roomId, as, hookId, event.stateKey);
|
||||
}
|
||||
|
||||
return new GenericHookConnection(
|
||||
roomId,
|
||||
state,
|
||||
hookId,
|
||||
};
|
||||
const connection = new GenericHookConnection(roomId, validState, hookId, validState.name, messageClient, config, as);
|
||||
event.stateKey,
|
||||
messageClient,
|
||||
config.generic,
|
||||
as,
|
||||
);
|
||||
}
|
||||
|
||||
static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown> = {}, {as, config, messageClient}: ProvisionConnectionOpts) {
|
||||
if (!config.generic) {
|
||||
throw Error('Generic Webhooks are not configured');
|
||||
}
|
||||
const hookId = uuid();
|
||||
const validState = GenericHookConnection.validateState(data, config.generic.allowJsTransformationFunctions || false);
|
||||
await GenericHookConnection.ensureRoomAccountData(roomId, as, hookId, validState.name);
|
||||
await as.botClient.sendStateEvent(roomId, this.CanonicalEventType, validState.name, validState);
|
||||
const connection = new GenericHookConnection(roomId, validState, hookId, validState.name, messageClient, config.generic, as);
|
||||
return {
|
||||
connection,
|
||||
stateEventContent: validState,
|
||||
@ -118,6 +148,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection
|
||||
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.generic.hook";
|
||||
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.generic.hook";
|
||||
static readonly ServiceCategory = "webhooks";
|
||||
|
||||
static readonly EventTypes = [
|
||||
GenericHookConnection.CanonicalEventType,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { IConnection } from "./IConnection";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { CommentProcessor } from "../CommentProcessor";
|
||||
import { MessageSenderClient } from "../MatrixSender";
|
||||
@ -28,6 +28,7 @@ const md = new markdown();
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
@Connection
|
||||
export class GitHubDiscussionConnection extends BaseConnection implements IConnection {
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.github.discussion";
|
||||
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.discussion";
|
||||
@ -38,6 +39,18 @@ export class GitHubDiscussionConnection extends BaseConnection implements IConne
|
||||
];
|
||||
|
||||
static readonly QueryRoomRegex = /#github_disc_(.+)_(.+)_(\d+):.*/;
|
||||
static readonly ServiceCategory = "github";
|
||||
|
||||
public static createConnectionForState(roomId: string, event: StateEvent<any>, {
|
||||
github, config, as, tokenStore, commentProcessor, messageClient}: InstantiateConnectionOpts) {
|
||||
if (!github || !config.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
return new GitHubDiscussionConnection(
|
||||
roomId, as, event.content, event.stateKey, tokenStore, commentProcessor,
|
||||
messageClient, config.github,
|
||||
);
|
||||
}
|
||||
|
||||
readonly sentEvents = new Set<string>(); //TODO: Set some reasonable limits
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { IConnection } from "./IConnection";
|
||||
import { Appservice, Space } from "matrix-bot-sdk";
|
||||
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
|
||||
import { Appservice, Space, StateEvent } from "matrix-bot-sdk";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { ReposGetResponseData } from "../Github/Types";
|
||||
import axios from "axios";
|
||||
@ -17,6 +17,7 @@ export interface GitHubDiscussionSpaceConnectionState {
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
@Connection
|
||||
export class GitHubDiscussionSpace extends BaseConnection implements IConnection {
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.github.discussion.space";
|
||||
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.discussion.space";
|
||||
@ -27,6 +28,17 @@ export class GitHubDiscussionSpace extends BaseConnection implements IConnection
|
||||
];
|
||||
|
||||
static readonly QueryRoomRegex = /#github_disc_(.+)_(.+):.*/;
|
||||
static readonly ServiceCategory = "github";
|
||||
|
||||
public static async createConnectionForState(roomId: string, event: StateEvent<any>, {
|
||||
github, config, as}: InstantiateConnectionOpts) {
|
||||
if (!github || !config.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
return new GitHubDiscussionSpace(
|
||||
await as.botClient.getSpace(roomId), event.content, event.stateKey
|
||||
);
|
||||
}
|
||||
|
||||
static async onQueryRoom(result: RegExpExecArray, opts: {githubInstance: GithubInstance, as: Appservice}): Promise<Record<string, unknown>> {
|
||||
if (!result || result.length < 2) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { IConnection } from "./IConnection";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import { MatrixMessageContent, MatrixEvent } from "../MatrixEvent";
|
||||
import markdown from "markdown-it";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
@ -38,6 +38,7 @@ interface IQueryRoomOpts {
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
@Connection
|
||||
export class GitHubIssueConnection extends BaseConnection implements IConnection {
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.github.issue";
|
||||
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.bridge";
|
||||
@ -48,11 +49,25 @@ export class GitHubIssueConnection extends BaseConnection implements IConnection
|
||||
];
|
||||
|
||||
static readonly QueryRoomRegex = /#github_(.+)_(.+)_(\d+):.*/;
|
||||
static readonly ServiceCategory = "github";
|
||||
|
||||
static generateAliasLocalpart(org: string, repo: string, issueNo: string|number) {
|
||||
return `github_${org}_${repo}_${issueNo}`;
|
||||
}
|
||||
|
||||
public static async createConnectionForState(roomId: string, event: StateEvent<any>, {
|
||||
github, config, as, tokenStore, commentProcessor, messageClient}: InstantiateConnectionOpts) {
|
||||
if (!github || !config.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
const issue = new GitHubIssueConnection(
|
||||
roomId, as, event.content, event.stateKey || "", tokenStore,
|
||||
commentProcessor, messageClient, github, config.github,
|
||||
);
|
||||
await issue.syncIssueState();
|
||||
return issue;
|
||||
}
|
||||
|
||||
static async onQueryRoom(result: RegExpExecArray, opts: IQueryRoomOpts): Promise<unknown> {
|
||||
const parts = result?.slice(1);
|
||||
if (!parts) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { IConnection } from "./IConnection";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { ProjectsGetResponseData } from "../Github/Types";
|
||||
import { BaseConnection } from "./BaseConnection";
|
||||
@ -14,15 +14,23 @@ const log = new LogWrapper("GitHubProjectConnection");
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
@Connection
|
||||
export class GitHubProjectConnection extends BaseConnection implements IConnection {
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.github.project";
|
||||
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.project";
|
||||
|
||||
static readonly ServiceCategory = "github";
|
||||
static readonly EventTypes = [
|
||||
GitHubProjectConnection.CanonicalEventType,
|
||||
GitHubProjectConnection.LegacyCanonicalEventType,
|
||||
];
|
||||
|
||||
public static createConnectionForState(roomId: string, event: StateEvent<any>, {config, as}: InstantiateConnectionOpts) {
|
||||
if (!config.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
return new GitHubProjectConnection(roomId, as, event.content, event.stateKey);
|
||||
}
|
||||
|
||||
static async onOpenProject(project: ProjectsGetResponseData, as: Appservice, inviteUser: string): Promise<GitHubProjectConnection> {
|
||||
log.info(`Fetching ${project.name} ${project.id}`);
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { Appservice, IRichReplyMetadata } from "matrix-bot-sdk";
|
||||
import { Appservice, IRichReplyMetadata, StateEvent } from "matrix-bot-sdk";
|
||||
import { BotCommands, botCommand, compileBotCommands, HelpFunction } from "../BotCommands";
|
||||
import { CommentProcessor } from "../CommentProcessor";
|
||||
import { FormatUtil } from "../FormatUtil";
|
||||
import { IConnection, IConnectionState } from "./IConnection";
|
||||
import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
|
||||
import { IssuesOpenedEvent, IssuesReopenedEvent, IssuesEditedEvent, PullRequestOpenedEvent, IssuesClosedEvent, PullRequestClosedEvent, PullRequestReadyForReviewEvent, PullRequestReviewSubmittedEvent, ReleaseCreatedEvent, IssuesLabeledEvent, IssuesUnlabeledEvent } from "@octokit/webhooks-types";
|
||||
import { MatrixMessageContent, MatrixEvent, MatrixReactionContent } from "../MatrixEvent";
|
||||
import { MessageSenderClient } from "../MatrixSender";
|
||||
@ -148,9 +148,12 @@ function validateState(state: Record<string, unknown>): GitHubRepoConnectionStat
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
@Connection
|
||||
export class GitHubRepoConnection extends CommandConnection implements IConnection {
|
||||
static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown>, as: Appservice,
|
||||
tokenStore: UserTokenStore, githubInstance: GithubInstance, config: BridgeConfigGitHub) {
|
||||
static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown>, {as, tokenStore, github, config}: ProvisionConnectionOpts) {
|
||||
if (!github || !config.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
const validData = validateState(data);
|
||||
const octokit = await tokenStore.getOctokitForUser(userId);
|
||||
if (!octokit) {
|
||||
@ -168,7 +171,7 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
|
||||
if (permissionLevel !== "admin" && permissionLevel !== "write") {
|
||||
throw new ApiError("You must at least have write permissions to bridge this repository", ErrCode.ForbiddenUser);
|
||||
}
|
||||
const appOctokit = await githubInstance.getSafeOctokitForRepo(validData.org, validData.repo);
|
||||
const appOctokit = await github.getSafeOctokitForRepo(validData.org, validData.repo);
|
||||
if (!appOctokit) {
|
||||
throw new ApiError(
|
||||
"You need to add a GitHub App to this organisation / repository before you can bridge it. Open the link to add the app, and then retry this request",
|
||||
@ -176,27 +179,34 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
|
||||
-1,
|
||||
{
|
||||
// E.g. https://github.com/apps/matrix-bridge/installations/new
|
||||
installUrl: githubInstance.newInstallationUrl,
|
||||
installUrl: github.newInstallationUrl,
|
||||
}
|
||||
);
|
||||
}
|
||||
const stateEventKey = `${validData.org}/${validData.repo}`;
|
||||
await as.botClient.sendStateEvent(roomId, this.CanonicalEventType, stateEventKey, validData);
|
||||
return {
|
||||
stateEventContent: validData,
|
||||
connection: new GitHubRepoConnection(roomId, as, validData, tokenStore, stateEventKey, githubInstance, config),
|
||||
connection: new GitHubRepoConnection(roomId, as, validData, tokenStore, stateEventKey, github, config.github),
|
||||
}
|
||||
}
|
||||
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.github.repository";
|
||||
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.repository";
|
||||
|
||||
static readonly EventTypes = [
|
||||
GitHubRepoConnection.CanonicalEventType,
|
||||
GitHubRepoConnection.LegacyCanonicalEventType,
|
||||
];
|
||||
|
||||
static readonly ServiceCategory = "github";
|
||||
static readonly QueryRoomRegex = /#github_(.+)_(.+):.*/;
|
||||
|
||||
static async createConnectionForState(roomId: string, state: StateEvent<Record<string, unknown>>, {as, tokenStore, github, config}: InstantiateConnectionOpts) {
|
||||
if (!github || !config.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
return new GitHubRepoConnection(roomId, as, validateState(state.content), tokenStore, state.stateKey, github, config.github);
|
||||
}
|
||||
|
||||
static async onQueryRoom(result: RegExpExecArray, opts: IQueryRoomOpts): Promise<unknown> {
|
||||
const parts = result?.slice(1);
|
||||
if (!parts) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { IConnection } from "./IConnection";
|
||||
import { Appservice, Space } from "matrix-bot-sdk";
|
||||
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
|
||||
import { Appservice, Space, StateEvent } from "matrix-bot-sdk";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import axios from "axios";
|
||||
import { GitHubDiscussionSpace } from ".";
|
||||
@ -16,6 +16,7 @@ export interface GitHubUserSpaceConnectionState {
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
@Connection
|
||||
export class GitHubUserSpace extends BaseConnection implements IConnection {
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.github.user.space";
|
||||
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.user.space";
|
||||
@ -25,6 +26,17 @@ export class GitHubUserSpace extends BaseConnection implements IConnection {
|
||||
];
|
||||
|
||||
static readonly QueryRoomRegex = /#github_(.+):.*/;
|
||||
static readonly ServiceCategory = "github";
|
||||
|
||||
public static async createConnectionForState(roomId: string, event: StateEvent<any>, {
|
||||
github, config, as}: InstantiateConnectionOpts) {
|
||||
if (!github || !config.github) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
return new GitHubUserSpace(
|
||||
await as.botClient.getSpace(roomId), event.content, event.stateKey
|
||||
);
|
||||
}
|
||||
|
||||
static async onQueryRoom(result: RegExpExecArray, opts: {githubInstance: GithubInstance, as: Appservice}): Promise<Record<string, unknown>> {
|
||||
if (!result || result.length < 1) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { IConnection } from "./IConnection";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { Connection, IConnection, InstantiateConnectionOpts } from "./IConnection";
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import { MatrixMessageContent, MatrixEvent } from "../MatrixEvent";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
@ -33,21 +33,36 @@ const log = new LogWrapper("GitLabIssueConnection");
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
@Connection
|
||||
export class GitLabIssueConnection extends BaseConnection implements IConnection {
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.gitlab.issue";
|
||||
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.gitlab.issue";
|
||||
|
||||
static readonly EventTypes = [
|
||||
GitLabIssueConnection.CanonicalEventType,
|
||||
GitLabIssueConnection.LegacyCanonicalEventType,
|
||||
];
|
||||
|
||||
static readonly QueryRoomRegex = /#gitlab_(.+)_(.+)_(\d+):.*/;
|
||||
static readonly ServiceCategory = "gitlab";
|
||||
|
||||
static getTopicString(authorName: string, state: string) {
|
||||
`Author: ${authorName} | State: ${state === "closed" ? "closed" : "open"}`
|
||||
}
|
||||
|
||||
public static async createConnectionForState(roomId: string, event: StateEvent<any>, {
|
||||
config, as, tokenStore, commentProcessor, messageClient}: InstantiateConnectionOpts) {
|
||||
if (!config.gitlab) {
|
||||
throw Error('GitHub is not configured');
|
||||
}
|
||||
const instance = config.gitlab.instances[event.content.instance];
|
||||
if (!instance) {
|
||||
throw Error('Instance name not recognised');
|
||||
}
|
||||
return new GitLabIssueConnection(
|
||||
roomId, as, event.content, event.stateKey || "", tokenStore,
|
||||
commentProcessor, messageClient, instance, config.gitlab,
|
||||
);
|
||||
}
|
||||
|
||||
public static async createRoomForIssue(instanceName: string, instance: GitLabInstance,
|
||||
issue: GetIssueResponse, projects: string[], as: Appservice,
|
||||
tokenStore: UserTokenStore, commentProcessor: CommentProcessor,
|
||||
|
@ -1,7 +1,7 @@
|
||||
// We need to instantiate some functions which are not directly called, which confuses typescript.
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import { BotCommands, botCommand, compileBotCommands } from "../BotCommands";
|
||||
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
||||
import markdown from "markdown-it";
|
||||
@ -9,7 +9,7 @@ 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 { IConnectionState } from "./IConnection";
|
||||
import { Connection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
|
||||
import { GetConnectionsResponseItem } from "../provisioning/api";
|
||||
import { ErrCode, ApiError } from "../api"
|
||||
import { AccessLevel } from "../Gitlab/Types";
|
||||
@ -114,6 +114,7 @@ function validateState(state: Record<string, unknown>): GitLabRepoConnectionStat
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
@Connection
|
||||
export class GitLabRepoConnection extends CommandConnection {
|
||||
static readonly CanonicalEventType = "uk.half-shot.matrix-hookshot.gitlab.repository";
|
||||
static readonly LegacyCanonicalEventType = "uk.half-shot.matrix-github.gitlab.repository";
|
||||
@ -125,14 +126,29 @@ export class GitLabRepoConnection extends CommandConnection {
|
||||
|
||||
static botCommands: BotCommands;
|
||||
static helpMessage: (cmdPrefix?: string | undefined) => MatrixMessageContent;
|
||||
static ServiceCategory = "gitlab";
|
||||
|
||||
private readonly debounceMRComments = new Map<string, {comments: number, author: string, timeout: NodeJS.Timeout}>();
|
||||
|
||||
public static async provisionConnection(roomId: string, requester: string, data: Record<string, unknown>, as: Appservice, tokenStore: UserTokenStore, instanceName: string, gitlabConfig: BridgeConfigGitLab) {
|
||||
const validData = validateState(data);
|
||||
const instance = gitlabConfig.instances[instanceName];
|
||||
static async createConnectionForState(roomId: string, event: StateEvent<Record<string, unknown>>, {as, tokenStore, github, config}: InstantiateConnectionOpts) {
|
||||
if (!github || !config.gitlab) {
|
||||
throw Error('GitLab is not configured');
|
||||
}
|
||||
const state = validateState(event.content);
|
||||
const instance = config.gitlab.instances[state.instance];
|
||||
if (!instance) {
|
||||
throw Error(`provisionConnection provided an instanceName of ${instanceName} but the instance does not exist`);
|
||||
throw Error('Instance name not recognised');
|
||||
}
|
||||
return new GitLabRepoConnection(roomId, event.stateKey, as, state, tokenStore, instance);
|
||||
}
|
||||
|
||||
public static async provisionConnection(roomId: string, requester: string, data: Record<string, unknown>, { config, as, tokenStore, getAllConnectionsOfType }: ProvisionConnectionOpts) {
|
||||
if (!config.gitlab) {
|
||||
throw Error('GitLab is not configured');
|
||||
}
|
||||
const gitlabConfig = config.gitlab;
|
||||
const validData = validateState(data);
|
||||
const instance = gitlabConfig.instances[validData.instance];
|
||||
if (!instance) {
|
||||
throw Error(`provisionConnection provided an instanceName of ${validData.instance} but the instance does not exist`);
|
||||
}
|
||||
const client = await tokenStore.getGitLabForUser(requester, instance.url);
|
||||
if (!client) {
|
||||
@ -151,6 +167,17 @@ export class GitLabRepoConnection extends CommandConnection {
|
||||
throw new ApiError("You must at least have developer access to bridge this project", ErrCode.ForbiddenUser);
|
||||
}
|
||||
|
||||
const stateEventKey = `${validData.instance}/${validData.path}`;
|
||||
const connection = new GitLabRepoConnection(roomId, stateEventKey, as, validData, tokenStore, instance);
|
||||
const existingConnections = getAllConnectionsOfType(GitLabRepoConnection);
|
||||
const existing = existingConnections.find(c => c instanceof GitLabRepoConnection && c.stateKey === connection.stateKey) as undefined|GitLabRepoConnection;
|
||||
|
||||
if (existing) {
|
||||
throw new ApiError("A GitLab repo connection for this project already exists", ErrCode.ConflictingConnection, -1, {
|
||||
existingConnection: existing.getProvisionerDetails()
|
||||
});
|
||||
}
|
||||
|
||||
// Try to setup a webhook
|
||||
if (gitlabConfig.webhook.publicUrl) {
|
||||
const hooks = await client.projects.hooks.list(project.id);
|
||||
@ -173,12 +200,8 @@ export class GitLabRepoConnection extends CommandConnection {
|
||||
} else {
|
||||
log.info(`Not creating webhook, webhookUrl is not defined in config`);
|
||||
}
|
||||
|
||||
const stateEventKey = `${validData.instance}/${validData.path}`;
|
||||
return {
|
||||
stateEventContent: validData,
|
||||
connection: new GitLabRepoConnection(roomId, stateEventKey, as, validData, tokenStore, instance),
|
||||
}
|
||||
await as.botIntent.underlyingClient.sendStateEvent(roomId, this.CanonicalEventType, connection.stateKey, validData);
|
||||
return {connection};
|
||||
}
|
||||
|
||||
public static getProvisionerDetails(botUserId: string) {
|
||||
@ -222,6 +245,8 @@ export class GitLabRepoConnection extends CommandConnection {
|
||||
})) as GitLabRepoConnectionProjectTarget[];
|
||||
}
|
||||
|
||||
private readonly debounceMRComments = new Map<string, {comments: number, author: string, timeout: NodeJS.Timeout}>();
|
||||
|
||||
constructor(roomId: string,
|
||||
stateKey: string,
|
||||
private readonly as: Appservice,
|
||||
|
@ -1,8 +1,14 @@
|
||||
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
|
||||
import { IssuesOpenedEvent, IssuesEditedEvent } from "@octokit/webhooks-types";
|
||||
import { GetConnectionsResponseItem } from "../provisioning/api";
|
||||
import { IRichReplyMetadata } from "matrix-bot-sdk";
|
||||
import { BridgePermissionLevel } from "../Config/Config";
|
||||
import { Appservice, IRichReplyMetadata, StateEvent } from "matrix-bot-sdk";
|
||||
import { BridgeConfig, BridgePermissionLevel } from "../Config/Config";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { CommentProcessor } from "../CommentProcessor";
|
||||
import { MessageSenderClient } from "../MatrixSender";
|
||||
import { IBridgeStorageProvider } from "../Stores/StorageProvider";
|
||||
import { GithubInstance } from "../Github/GithubInstance";
|
||||
import "reflect-metadata";
|
||||
|
||||
export type PermissionCheckFn = (service: string, level: BridgePermissionLevel) => boolean;
|
||||
|
||||
@ -70,3 +76,42 @@ export interface IConnection {
|
||||
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export interface ConnectionDeclaration<C extends IConnection = IConnection> {
|
||||
EventTypes: string[];
|
||||
ServiceCategory: string;
|
||||
provisionConnection?: (roomId: string, userId: string, data: Record<string, unknown>, opts: ProvisionConnectionOpts) => Promise<{connection: C}>;
|
||||
createConnectionForState: (roomId: string, state: StateEvent<Record<string, unknown>>, opts: InstantiateConnectionOpts) => C|Promise<C>
|
||||
}
|
||||
|
||||
export const ConnectionDeclarations: Array<ConnectionDeclaration> = [];
|
||||
|
||||
export interface InstantiateConnectionOpts {
|
||||
as: Appservice,
|
||||
config: BridgeConfig,
|
||||
tokenStore: UserTokenStore,
|
||||
commentProcessor: CommentProcessor,
|
||||
messageClient: MessageSenderClient,
|
||||
storage: IBridgeStorageProvider,
|
||||
github?: GithubInstance,
|
||||
}
|
||||
export interface ProvisionConnectionOpts extends InstantiateConnectionOpts {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getAllConnectionsOfType<T extends IConnection>(typeT: new (...params : any[]) => T): T[],
|
||||
}
|
||||
|
||||
|
||||
export function Connection<T extends ConnectionDeclaration>(connectionType: T) {
|
||||
// Event type clashes
|
||||
if (ConnectionDeclarations.find(
|
||||
(existingConn) => !!connectionType.EventTypes.find(
|
||||
(evtType) => existingConn.EventTypes.includes(evtType))
|
||||
)
|
||||
) {
|
||||
throw Error(`Provisioning connection for ${connectionType.EventTypes[0]} has a event type clash with another connection`);
|
||||
}
|
||||
ConnectionDeclarations.push(connectionType);
|
||||
return connectionType;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { IConnection, IConnectionState } from "./IConnection";
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
|
||||
import { Appservice, StateEvent } from "matrix-bot-sdk";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { JiraIssueEvent, JiraIssueUpdatedEvent } from "../Jira/WebhookTypes";
|
||||
import { FormatUtil } from "../FormatUtil";
|
||||
@ -49,6 +49,7 @@ const md = new markdownit();
|
||||
/**
|
||||
* Handles rooms connected to a github repo.
|
||||
*/
|
||||
@Connection
|
||||
export class JiraProjectConnection extends CommandConnection implements IConnection {
|
||||
|
||||
|
||||
@ -59,10 +60,19 @@ export class JiraProjectConnection extends CommandConnection implements IConnect
|
||||
JiraProjectConnection.CanonicalEventType,
|
||||
JiraProjectConnection.LegacyCanonicalEventType,
|
||||
];
|
||||
static readonly ServiceCategory = "jira";
|
||||
static botCommands: BotCommands;
|
||||
static helpMessage: (cmdPrefix?: string) => MatrixMessageContent;
|
||||
|
||||
static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown>, as: Appservice, tokenStore: UserTokenStore) {
|
||||
static async provisionConnection(roomId: string, userId: string, data: Record<string, unknown>, {getAllConnectionsOfType, as, tokenStore, config}: ProvisionConnectionOpts) {
|
||||
if (!config.jira) {
|
||||
throw new ApiError('JIRA integration is not configured', ErrCode.DisabledFeature);
|
||||
}
|
||||
const existingConnections = getAllConnectionsOfType(JiraProjectConnection);
|
||||
if (existingConnections.find(c => c instanceof JiraProjectConnection)) {
|
||||
// TODO: Support this.
|
||||
throw Error("Cannot support multiple connections of the same type yet");
|
||||
}
|
||||
const validData = validateJiraConnectionState(data);
|
||||
log.info(`Attempting to provisionConnection for ${roomId} ${validData.url} on behalf of ${userId}`);
|
||||
const jiraClient = await tokenStore.getJiraForUser(userId, validData.url);
|
||||
@ -84,8 +94,17 @@ export class JiraProjectConnection extends CommandConnection implements IConnect
|
||||
} catch (ex) {
|
||||
throw new ApiError("Requested project was not found", ErrCode.ForbiddenUser);
|
||||
}
|
||||
await as.botIntent.underlyingClient.sendStateEvent(roomId, JiraProjectConnection.CanonicalEventType, connection.stateKey, validData);
|
||||
log.info(`Created connection via provisionConnection ${connection.toString()}`);
|
||||
return {stateEventContent: validData, connection};
|
||||
return {connection};
|
||||
}
|
||||
|
||||
static createConnectionForState(roomId: string, state: StateEvent<Record<string, unknown>>, {config, as, tokenStore}: InstantiateConnectionOpts) {
|
||||
if (!config.jira) {
|
||||
throw Error('JIRA is not configured');
|
||||
}
|
||||
const connectionConfig = validateJiraConnectionState(state.content);
|
||||
return new JiraProjectConnection(roomId, as, connectionConfig, state.stateKey, tokenStore);
|
||||
}
|
||||
|
||||
public get projectId() {
|
||||
|
@ -1,13 +1,10 @@
|
||||
// We need to instantiate some functions which are not directly called, which confuses typescript.
|
||||
import { Appservice } from "matrix-bot-sdk";
|
||||
import { BotCommands, botCommand, compileBotCommands, HelpFunction } from "../BotCommands";
|
||||
import { CommandConnection } from "./CommandConnection";
|
||||
import { GenericHookConnection, GitHubRepoConnection, JiraProjectConnection } from ".";
|
||||
import { CommandError } from "../errors";
|
||||
import { UserTokenStore } from "../UserTokenStore";
|
||||
import { GithubInstance } from "../Github/GithubInstance";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { BridgeConfig, BridgePermissionLevel } from "../Config/Config";
|
||||
import { BridgePermissionLevel } from "../Config/Config";
|
||||
import markdown from "markdown-it";
|
||||
import { FigmaFileConnection } from "./FigmaFileConnection";
|
||||
import { FeedConnection } from "./FeedConnection";
|
||||
@ -15,6 +12,7 @@ import { URL } from "url";
|
||||
import { SetupWidget } from "../Widgets/SetupWidget";
|
||||
import { AdminRoom } from "../AdminRoom";
|
||||
import { GitLabRepoConnection } from "./GitlabRepo";
|
||||
import { ProvisionConnectionOpts } from "./IConnection";
|
||||
const md = new markdown();
|
||||
|
||||
/**
|
||||
@ -26,17 +24,22 @@ export class SetupConnection extends CommandConnection {
|
||||
static botCommands: BotCommands;
|
||||
static helpMessage: HelpFunction;
|
||||
|
||||
private get config() {
|
||||
return this.provisionOpts.config;
|
||||
}
|
||||
|
||||
private get as() {
|
||||
return this.provisionOpts.as;
|
||||
}
|
||||
|
||||
constructor(public readonly roomId: string,
|
||||
private readonly as: Appservice,
|
||||
private readonly tokenStore: UserTokenStore,
|
||||
private readonly config: BridgeConfig,
|
||||
private readonly getOrCreateAdminRoom: (userId: string) => Promise<AdminRoom>,
|
||||
private readonly githubInstance?: GithubInstance,) {
|
||||
private readonly provisionOpts: ProvisionConnectionOpts,
|
||||
private readonly getOrCreateAdminRoom: (userId: string) => Promise<AdminRoom>,) {
|
||||
super(
|
||||
roomId,
|
||||
"",
|
||||
"",
|
||||
as.botClient,
|
||||
provisionOpts.as.botClient,
|
||||
SetupConnection.botCommands,
|
||||
SetupConnection.helpMessage,
|
||||
"!hookshot",
|
||||
@ -55,13 +58,12 @@ export class SetupConnection extends CommandConnection {
|
||||
|
||||
@botCommand("github repo", { help: "Create a connection for a GitHub repository. (You must be logged in with GitHub to do this.)", requiredArgs: ["url"], includeUserId: true, category: "github"})
|
||||
public async onGitHubRepo(userId: string, url: string) {
|
||||
if (!this.githubInstance || !this.config.github) {
|
||||
if (!this.provisionOpts.github || !this.config.github) {
|
||||
throw new CommandError("not-configured", "The bridge is not configured to support GitHub.");
|
||||
}
|
||||
|
||||
await this.checkUserPermissions(userId, "github", GitHubRepoConnection.CanonicalEventType);
|
||||
|
||||
const octokit = await this.tokenStore.getOctokitForUser(userId);
|
||||
const octokit = await this.provisionOpts.tokenStore.getOctokitForUser(userId);
|
||||
if (!octokit) {
|
||||
throw new CommandError("User not logged in", "You are not logged into GitHub. Start a DM with this bot and use the command `github login`.");
|
||||
}
|
||||
@ -70,9 +72,8 @@ export class SetupConnection extends CommandConnection {
|
||||
throw new CommandError("Invalid GitHub url", "The GitHub url you entered was not valid.");
|
||||
}
|
||||
const [, org, repo] = urlParts;
|
||||
const res = await GitHubRepoConnection.provisionConnection(this.roomId, userId, {org, repo}, this.as, this.tokenStore, this.githubInstance, this.config.github);
|
||||
await this.as.botClient.sendStateEvent(this.roomId, GitHubRepoConnection.CanonicalEventType, url, res.stateEventContent);
|
||||
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${org}/${repo}`);
|
||||
const {connection} = await GitHubRepoConnection.provisionConnection(this.roomId, userId, {org, repo}, this.provisionOpts);
|
||||
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${connection.org}/${connection.repo}`);
|
||||
}
|
||||
|
||||
@botCommand("gitlab project", { help: "Create a connection for a GitHub project. (You must be logged in with GitLab to do this.)", requiredArgs: ["url"], includeUserId: true, category: "gitlab"})
|
||||
@ -89,7 +90,7 @@ export class SetupConnection extends CommandConnection {
|
||||
throw new CommandError("not-configured", "No instance found that matches the provided URL.");
|
||||
}
|
||||
|
||||
const client = await this.tokenStore.getGitLabForUser(userId, instance.url);
|
||||
const client = await this.provisionOpts.tokenStore.getGitLabForUser(userId, instance.url);
|
||||
if (!client) {
|
||||
throw new CommandError("User not logged in", "You are not logged into this GitLab instance. Start a DM with this bot and use the command `gitlab personaltoken`.");
|
||||
}
|
||||
@ -97,9 +98,8 @@ export class SetupConnection extends CommandConnection {
|
||||
if (!path) {
|
||||
throw new CommandError("Invalid GitLab url", "The GitLab project url you entered was not valid.");
|
||||
}
|
||||
const res = await GitLabRepoConnection.provisionConnection(this.roomId, userId, {path, instance: name}, this.as, this.tokenStore, name, this.config.gitlab);
|
||||
await this.as.botClient.sendStateEvent(this.roomId, GitLabRepoConnection.CanonicalEventType, url, res.stateEventContent);
|
||||
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${path}`);
|
||||
const {connection} = await GitLabRepoConnection.provisionConnection(this.roomId, userId, {path, instance: name}, this.provisionOpts);
|
||||
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${connection.path}`);
|
||||
}
|
||||
|
||||
@botCommand("jira project", { help: "Create a connection for a JIRA project. (You must be logged in with JIRA to do this.)", requiredArgs: ["url"], includeUserId: true, category: "jira"})
|
||||
@ -111,7 +111,7 @@ export class SetupConnection extends CommandConnection {
|
||||
|
||||
await this.checkUserPermissions(userId, "jira", JiraProjectConnection.CanonicalEventType);
|
||||
|
||||
const jiraClient = await this.tokenStore.getJiraForUser(userId, urlStr);
|
||||
const jiraClient = await this.provisionOpts.tokenStore.getJiraForUser(userId, urlStr);
|
||||
if (!jiraClient) {
|
||||
throw new CommandError("User not logged in", "You are not logged into Jira. Start a DM with this bot and use the command `jira login`.");
|
||||
}
|
||||
@ -121,8 +121,7 @@ export class SetupConnection extends CommandConnection {
|
||||
throw new CommandError("Invalid Jira url", "The JIRA project url you entered was not valid. It should be in the format of `https://jira-instance/.../projects/PROJECTKEY/...` or `.../RapidBoard.jspa?projectKey=TEST`.");
|
||||
}
|
||||
const safeUrl = `https://${url.host}/projects/${projectKey}`;
|
||||
const res = await JiraProjectConnection.provisionConnection(this.roomId, userId, { url: safeUrl }, this.as, this.tokenStore);
|
||||
await this.as.botClient.sendStateEvent(this.roomId, JiraProjectConnection.CanonicalEventType, safeUrl, res.stateEventContent);
|
||||
const res = await JiraProjectConnection.provisionConnection(this.roomId, userId, { url: safeUrl }, this.provisionOpts);
|
||||
await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge Jira project ${res.connection.projectKey}.`);
|
||||
}
|
||||
|
||||
@ -139,8 +138,7 @@ export class SetupConnection extends CommandConnection {
|
||||
}
|
||||
const hookId = uuid();
|
||||
const url = `${this.config.generic.urlPrefix}${this.config.generic.urlPrefix.endsWith('/') ? '' : '/'}${hookId}`;
|
||||
await GenericHookConnection.ensureRoomAccountData(this.roomId, this.as, hookId, name);
|
||||
await this.as.botClient.sendStateEvent(this.roomId, GenericHookConnection.CanonicalEventType, name, {hookId, name});
|
||||
await GenericHookConnection.provisionConnection(this.roomId, userId, {name}, this.provisionOpts);
|
||||
const adminRoom = await this.getOrCreateAdminRoom(userId);
|
||||
await adminRoom.sendNotice(md.renderInline(`You have bridged a webhook. Please configure your webhook source to use \`${url}\`.`));
|
||||
return this.as.botClient.sendNotice(this.roomId, `Room configured to bridge webhooks. See admin room for secret url.`);
|
||||
@ -159,7 +157,7 @@ export class SetupConnection extends CommandConnection {
|
||||
throw new CommandError("Invalid Figma url", "The Figma file url you entered was not valid. It should be in the format of `https://figma.com/file/FILEID/...`.");
|
||||
}
|
||||
const [, fileId] = res;
|
||||
await this.as.botClient.sendStateEvent(this.roomId, FigmaFileConnection.CanonicalEventType, fileId, {fileId});
|
||||
await FigmaFileConnection.provisionConnection(this.roomId, userId, { fileId }, this.provisionOpts);
|
||||
return this.as.botClient.sendHtmlNotice(this.roomId, md.renderInline(`Room configured to bridge Figma file.`));
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ import { AdminRoomCommandHandler } from "../AdminRoomCommandHandler";
|
||||
import { botCommand } from "../BotCommands";
|
||||
import { JiraAPIAccessibleResource } from "./Types";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
import { CLOUD_INSTANCE } from "./Client";
|
||||
|
||||
const log = new LogWrapper('JiraBotCommands');
|
||||
|
||||
@ -25,7 +24,7 @@ export class JiraBotCommands extends AdminRoomCommandHandler {
|
||||
this.sendNotice(`Bot is not configured with JIRA OAuth support.`);
|
||||
return;
|
||||
}
|
||||
if (await this.tokenStore.clearUserToken("jira", this.userId, this.config.jira.url || CLOUD_INSTANCE)) {
|
||||
if (await this.tokenStore.clearUserToken("jira", this.userId, this.config.jira.instanceName)) {
|
||||
return this.sendNotice(`Your JIRA account has been unlinked from your Matrix user.`);
|
||||
}
|
||||
return this.sendNotice(`No JIRA account was linked to your Matrix user.`);
|
||||
|
@ -13,8 +13,6 @@ export interface JiraClient {
|
||||
getClientForResource(res: JiraAPIAccessibleResource): Promise<HookshotJiraApi|null>;
|
||||
}
|
||||
|
||||
export const CLOUD_INSTANCE = "api.atlassian.com";
|
||||
|
||||
export class JiraApiError extends Error {
|
||||
constructor(readonly errorMessages: string[], readonly errors: { description: string}) {
|
||||
super();
|
||||
|
@ -4,7 +4,7 @@ import QuickLRU from "@alloc/quick-lru";
|
||||
import { JiraAPIAccessibleResource, JiraIssue, JiraOAuthResult, JiraProject, JiraCloudProjectSearchResponse, JiraStoredToken } from '../Types';
|
||||
import { BridgeConfigJira, BridgeConfigJiraCloudOAuth } from '../../Config/Config';
|
||||
import LogWrapper from '../../LogWrapper';
|
||||
import { CLOUD_INSTANCE, HookshotJiraApi, JiraClient } from '../Client';
|
||||
import { HookshotJiraApi, JiraClient } from '../Client';
|
||||
import JiraApi from 'jira-client';
|
||||
|
||||
const log = new LogWrapper("JiraCloudClient");
|
||||
@ -129,7 +129,7 @@ export class JiraCloudClient implements JiraClient {
|
||||
expires_in: data.expires_in,
|
||||
refresh_token: data.refresh_token,
|
||||
access_token: data.access_token,
|
||||
instance: CLOUD_INSTANCE,
|
||||
instance: this.config.instanceName,
|
||||
};
|
||||
this.onTokenRefreshed(this.storedToken);
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ export class MatrixSender {
|
||||
private mq: MessageQueue;
|
||||
private as: Appservice;
|
||||
constructor(private config: BridgeConfig, registration: IAppserviceRegistration) {
|
||||
this.mq = createMessageQueue(this.config);
|
||||
this.mq = createMessageQueue(this.config.queue);
|
||||
this.as = getAppservice(config, registration, new MemoryStorageProvider());
|
||||
Metrics.registerMatrixSdkMetrics(this.as);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { BridgeConfig } from "../Config/Config";
|
||||
import { BridgeConfigQueue } from "../Config/Config";
|
||||
import { LocalMQ } from "./LocalMQ";
|
||||
import { RedisMQ } from "./RedisQueue";
|
||||
import { MessageQueue } from "./Types";
|
||||
@ -6,8 +6,8 @@ import { MessageQueue } from "./Types";
|
||||
const staticLocalMq = new LocalMQ();
|
||||
let staticRedisMq: RedisMQ|null = null;
|
||||
|
||||
export function createMessageQueue(config: BridgeConfig): MessageQueue {
|
||||
if (config.queue.monolithic) {
|
||||
export function createMessageQueue(config: BridgeConfigQueue): MessageQueue {
|
||||
if (config.monolithic) {
|
||||
return staticLocalMq;
|
||||
}
|
||||
if (staticRedisMq === null) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
import { MessageQueue, MessageQueueMessage, DEFAULT_RES_TIMEOUT, MessageQueueMessageOut } from "./Types";
|
||||
import { Redis, default as redis } from "ioredis";
|
||||
import { BridgeConfig } from "../Config/Config";
|
||||
import { BridgeConfig, BridgeConfigQueue } from "../Config/Config";
|
||||
import { EventEmitter } from "events";
|
||||
import LogWrapper from "../LogWrapper";
|
||||
|
||||
@ -21,11 +21,11 @@ export class RedisMQ extends EventEmitter implements MessageQueue {
|
||||
private redisPub: Redis;
|
||||
private redis: Redis;
|
||||
private myUuid: string;
|
||||
constructor(config: BridgeConfig) {
|
||||
constructor(config: BridgeConfigQueue) {
|
||||
super();
|
||||
this.redisSub = new redis(config.queue.port, config.queue.host);
|
||||
this.redisPub = new redis(config.queue.port, config.queue.host);
|
||||
this.redis = new redis(config.queue.port, config.queue.host);
|
||||
this.redisSub = new redis(config.port, config.host);
|
||||
this.redisPub = new redis(config.port, config.host);
|
||||
this.redis = new redis(config.port, config.host);
|
||||
this.myUuid = uuid();
|
||||
this.redisSub.on("pmessage", (_: string, channel: string, message: string) => {
|
||||
const msg = JSON.parse(message) as MessageQueueMessageOut<unknown>;
|
||||
|
@ -49,7 +49,7 @@ export class NotifFilter {
|
||||
};
|
||||
}
|
||||
|
||||
public shouldInviteToRoom(user: string, repo: string, org: string): boolean {
|
||||
public shouldInviteToRoom(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ export class UserNotificationWatcher {
|
||||
private queue: MessageQueue;
|
||||
|
||||
constructor(private readonly config: BridgeConfig) {
|
||||
this.queue = createMessageQueue(config);
|
||||
this.queue = createMessageQueue(config.queue);
|
||||
this.matrixMessageSender = new MessageSenderClient(this.queue);
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,9 @@ import { Intent } from "matrix-bot-sdk";
|
||||
import { promises as fs } from "fs";
|
||||
import { publicEncrypt, privateDecrypt } from "crypto";
|
||||
import LogWrapper from "./LogWrapper";
|
||||
import { CLOUD_INSTANCE, isJiraCloudInstance, JiraClient } from "./Jira/Client";
|
||||
import { isJiraCloudInstance, JiraClient } from "./Jira/Client";
|
||||
import { JiraStoredToken } from "./Jira/Types";
|
||||
import { BridgeConfig, BridgeConfigJiraOnPremOAuth, BridgePermissionLevel } from "./Config/Config";
|
||||
import { BridgeConfig, BridgeConfigJira, BridgeConfigJiraOnPremOAuth, BridgePermissionLevel } from "./Config/Config";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { GitHubOAuthToken } from "./Github/Types";
|
||||
import { ApiError, ErrCode } from "./api";
|
||||
@ -202,15 +202,15 @@ export class UserTokenStore {
|
||||
throw Error('Jira not configured');
|
||||
}
|
||||
|
||||
let instance = instanceUrl ? new URL(instanceUrl).host : CLOUD_INSTANCE;
|
||||
let instance = instanceUrl && new URL(instanceUrl).host;
|
||||
|
||||
if (isJiraCloudInstance(instance)) {
|
||||
instance = CLOUD_INSTANCE;
|
||||
if (!instance || isJiraCloudInstance(instance)) {
|
||||
instance = BridgeConfigJira.CLOUD_INSTANCE_NAME;
|
||||
}
|
||||
|
||||
let jsonData = await this.getUserToken("jira", userId, instance);
|
||||
// XXX: Legacy fallback
|
||||
if (!jsonData && instance === CLOUD_INSTANCE) {
|
||||
if (!jsonData && instance === BridgeConfigJira.CLOUD_INSTANCE_NAME) {
|
||||
jsonData = await this.getUserToken("jira", userId);
|
||||
}
|
||||
if (!jsonData) {
|
||||
@ -219,9 +219,9 @@ export class UserTokenStore {
|
||||
const storedToken = JSON.parse(jsonData) as JiraStoredToken;
|
||||
if (!storedToken.instance) {
|
||||
// Legacy stored tokens don't include the cloud instance string.
|
||||
storedToken.instance = CLOUD_INSTANCE;
|
||||
storedToken.instance = BridgeConfigJira.CLOUD_INSTANCE_NAME;
|
||||
}
|
||||
if (storedToken.instance === CLOUD_INSTANCE) {
|
||||
if (storedToken.instance === BridgeConfigJira.CLOUD_INSTANCE_NAME) {
|
||||
return new JiraCloudClient(storedToken, (data) => {
|
||||
return this.storeJiraToken(userId, data);
|
||||
}, this.config.jira, instance);
|
||||
|
@ -53,7 +53,7 @@ export class Webhooks extends EventEmitter {
|
||||
|
||||
// TODO: Move these
|
||||
this.expressRouter.get("/oauth", this.onGitHubGetOauth.bind(this));
|
||||
this.queue = createMessageQueue(config);
|
||||
this.queue = createMessageQueue(config.queue);
|
||||
if (this.config.jira) {
|
||||
this.expressRouter.use("/jira", new JiraWebhooksRouter(this.queue).getRouter());
|
||||
}
|
||||
|
@ -2,10 +2,8 @@ import { expect } from "chai";
|
||||
import { createMessageQueue } from "../src/MessageQueue/MessageQueue";
|
||||
|
||||
const mq = createMessageQueue({
|
||||
queue: {
|
||||
monolithic: true,
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
|
||||
describe("MessageQueueTest", () => {
|
||||
describe("LocalMq", () => {
|
||||
|
@ -15,10 +15,8 @@ function createGenericHook(state: GenericHookConnectionState = {
|
||||
name: "some-name"
|
||||
}, config: BridgeGenericWebhooksConfigYAML = { enabled: true, urlPrefix: "https://example.com/webhookurl"}): [GenericHookConnection, MessageQueue] {
|
||||
const mq = createMessageQueue({
|
||||
queue: {
|
||||
monolithic: true,
|
||||
},
|
||||
} as any);
|
||||
monolithic: true
|
||||
});
|
||||
mq.subscribe('*');
|
||||
const messageClient = new MessageSenderClient(mq);
|
||||
const connection = new GenericHookConnection(ROOM_ID, state, "foobar", "foobar", messageClient, new BridgeConfigGenericWebhooks(config), AppserviceMock.create())
|
||||
|
@ -8,7 +8,7 @@ export class IntentMock {
|
||||
public readonly underlyingClient = new MatrixClientMock();
|
||||
public sentEvents: {roomId: string, content: any}[] = [];
|
||||
|
||||
static create(userId?: string){
|
||||
static create(){
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return new this() as any;
|
||||
}
|
||||
|
1
web/typings/sass.d.ts
vendored
1
web/typings/sass.d.ts
vendored
@ -1,5 +1,6 @@
|
||||
// As per https://lwebapp.com/en/post/cannot-find-module-scss
|
||||
declare module'*.scss' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content: {[key: string]: any}
|
||||
export = content
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user