Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC Slack App bridging #787

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config/config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ rtm:
#
log_level: "silent"

# Use a Slack App for bridging. This is the recommended method.
#
# slack_app:
# appToken: 'xapp-foo-bar-baz'
# botToken: 'xoxb-baz-bar-foo'
# signingSecret: 'feed1337beefcafe'

# Port for incoming Slack requests from webhooks and event API messages
# Optional if using RTM API, required otherwise.
#
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"homepage": "https://github.com/matrix-org/matrix-appservice-slack#readme",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@slack/bolt": "^3.18.0",
"@slack/logger": "^3.0.0",
"@slack/rtm-api": "^6.0.0",
"@slack/web-api": "^6.7.2",
Expand Down
3 changes: 1 addition & 2 deletions src/BridgedRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,6 @@ export class BridgedRoom {
}
const body: ISlackChatMessagePayload = {
...matrixToSlackResult,
as_user: false,
username: (await user.getDisplaynameForRoom(message.room_id)) || matrixToSlackResult.username,
};
const text = body.text;
Expand Down Expand Up @@ -524,7 +523,7 @@ export class BridgedRoom {
// Webhooks don't give us any ID, so we can't store this.
return true;
}
if (puppetedClient) {
if (puppetedClient && this.main.teamIsUsingRtm(this.SlackTeamId!)) {
body.as_user = true;
delete body.username;
}
Expand Down
6 changes: 6 additions & 0 deletions src/IConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ export interface IConfig {
log_level?: string;
};

slack_app?: {
appToken: string,
botToken: string,
signingSecret: string,
};

slack_hook_port?: number;
slack_client_opts?: WebClientOptions;
slack_proxy?: string;
Expand Down
15 changes: 12 additions & 3 deletions src/Main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { UserAdminRoom } from "./rooms/UserAdminRoom";
import { TeamSyncer } from "./TeamSyncer";
import { SlackGhostStore } from "./SlackGhostStore";
import { AllowDenyList, DenyReason } from "./AllowDenyList";
import { SlackAppHandler } from "./SlackAppHandler";

const log = new Logger("Main");

Expand Down Expand Up @@ -146,6 +147,7 @@ export class Main {
public readonly allowDenyList: AllowDenyList;

public slackRtm?: SlackRTMHandler;
public slackAppHandler?: SlackAppHandler;
private slackHookHandler?: SlackHookHandler;

private provisioner: Provisioner;
Expand Down Expand Up @@ -175,9 +177,12 @@ export class Main {

this.matrixUsersById = new QuickLRU({ maxSize: config.caching.matrixUserCache });

if ((!config.rtm || !config.rtm.enable) && (!config.slack_hook_port || !config.inbound_uri_prefix)) {
throw Error("Neither rtm.enable nor slack_hook_port|inbound_uri_prefix is defined in the config." +
"The bridge must define a listener in order to run");
if (
(!config.rtm || !config.rtm.enable)
&& (!config.slack_hook_port || !config.inbound_uri_prefix)
&& !config.slack_app
) {
throw Error("No RTM, no webhooks and no Slack App configured. The bridge must define a listener in order to run");
}

if ((!config.rtm?.enable || !config.oauth2) && config.puppeting?.enabled) {
Expand Down Expand Up @@ -1264,6 +1269,10 @@ export class Main {
await this.bridgeBlocker?.checkLimits(this.bridge.opts.controller.userActivityTracker.countActiveUsers().allUsers);
}

if (this.config.slack_app) {
this.slackAppHandler = await SlackAppHandler.create(this, this.config.slack_app);
}

log.info("Bridge initialised");
this.ready = true;
return port;
Expand Down
86 changes: 86 additions & 0 deletions src/SlackAppHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { App, KnownEventFromType, ReactionAddedEvent, ReactionRemovedEvent, SlackEvent } from "@slack/bolt";
import { Main, METRIC_RECEIVED_MESSAGE } from "./Main";
import { SlackEventHandler } from "./SlackEventHandler";
import { Logger } from "matrix-appservice-bridge";
import { ISlackEvent } from "./BaseSlackHandler";

const log = new Logger("SlackAppHandler");

interface Config {
appToken: string,
signingSecret: string,
botToken: string,
}

export class SlackAppHandler extends SlackEventHandler {
private constructor(
main: Main,
private app: App,
private teamId: string
) {
super(main);
this.app.message(async ({ message }) => this.handleSlackAppEvent(message));
this.app.event(new RegExp('^reaction'), ({ event }) => this.handleSlackAppEvent(event));
}

public static async create(main: Main, config: Config) {
const app = new App({
appToken: config.appToken,
token: config.botToken,
signingSecret: config.signingSecret,
socketMode: true,
});
await app.start();
const teamId = await main.clientFactory.upsertTeamByToken(config.botToken);
log.info(`Slack App listening for events for team ${teamId}`);
return new SlackAppHandler(main, app, teamId);
}

private async handleSlackAppEvent(ev: SlackEvent) {
try {
switch (ev.type) {
case 'message': return this.onMessage(ev);
case 'reaction_added':
case 'reaction_removed':
return this.onReaction(ev);
default:
log.warn(`Ignoring event of type ${ev.type}`);
}
} catch (err) {
log.error(`Failed to handle Slack App event ${JSON.stringify(ev, undefined, 2)}: ${err}`);
}
}

private async onMessage(msg: KnownEventFromType<'message'>) {
log.debug("Received a message:", msg);

this.main.incCounter(METRIC_RECEIVED_MESSAGE, { side: "remote" });

switch (msg.subtype) {
case undefined: // regular message
case "file_share":
return this.handleEvent({
...msg,
user_id: msg.user,
}, this.teamId);
case "message_changed":
return this.handleMessageEvent({
...msg as any,
user_id: '', // SlackEventHandler requires, but ignores this...
user: (msg.previous_message as any).user, // ...and actually uses this
}, this.teamId);
case "message_deleted":
return this.handleMessageEvent({
...msg as any,
user_id: '', // SlackEventHandler requires, but ignores this...
user: (msg.previous_message as any).user, // ...and actually uses this
}, this.teamId);
default:
log.warn(`Unhandled message subtype ${msg.subtype}`);
}
}

async onReaction(ev: ReactionAddedEvent|ReactionRemovedEvent): Promise<void> {
return this.handleEvent(ev as unknown as ISlackEvent, this.teamId);
}
}
2 changes: 0 additions & 2 deletions src/SlackEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,6 @@ export class SlackEventHandler extends BaseSlackHandler {
} catch (err) {
log.error(err);
}
// If we don't have the event
throw Error("unknown_message");
} else if (msg.subtype === "message_replied") {
// Slack sends us one of these as well as a normal message event
// when using RTM, so we ignore it.
Expand Down
Loading
Loading