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