Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions changelog.d/265.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Send a notice when the bridge connects or disconnects a user from their account.
2 changes: 2 additions & 0 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ purple:
# For selecting a specific backend. One of "node-purple", "xmpp-js".
# -- For xmpp.js - You need an existing xmpp server for this to work.
backend: "xmpp-js"
sendConnectionNotices: false
backendOpts:
# endpoint to reach the component on. The default port is 5347
service: "xmpp://localhost:5347"
Expand Down Expand Up @@ -65,6 +66,7 @@ purple:

# OR
# backend: "node-purple"
# sendConnectionNotices: true
# backendOpts:
# # endpoint to reach the component on. The default port is 5347
# service: "xmpp://localhost:5347"
Expand Down
85 changes: 85 additions & 0 deletions src/AccountHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { IAccountErrorEvent, IAccountEvent } from "./bifrost/Events";
import { IBifrostInstance } from "./bifrost/Instance";
import { Bridge, Logging } from "matrix-appservice-bridge";
import { IStore } from "./store/Store";
import { Config } from "./Config";

const log = Logging.get("AccountHandler");
/**
* Class to manage account settings, including commands sent from Matrix users
*/
export class AccountHandler {

constructor(private instance: IBifrostInstance,
private store: IStore,
private bridge: Bridge,
private config: Config) {
instance.on("account-signed-on", (ev: IAccountEvent) => {
this.onAccountSignedOn(ev);
});
instance.on("account-connection-error", (ev: IAccountErrorEvent) => {
this.onAccountConnectionError(ev);
});
instance.on("account-signed-off", (ev: IAccountEvent) => {
this.onAccountSignedOff(ev);
});
}

private async getInstanceEventContext(ev: IAccountEvent) {
const account = this.instance.getAccount(ev.account.username, ev.account.protocol_id);
const user = await this.store.getMatrixUserForAccount(ev.account);
const protocol = this.instance.getProtocol(ev.account.protocol_id);
if (!account) {
log.warn("Account not registered with Bifrost, ignoring");
return;
}
if (!user) {
log.warn(`Account registered with Bifrost, but not assigned to a user!!`);
return;
}
const adminRoom = await this.store.getAdminRoom(user.getId());
return {
account,
user,
protocol,
adminRoom,
}
}

private async onAccountSignedOn(ev: IAccountEvent) {
log.info(`${ev.account.protocol_id}://${ev.account.username} signed on`);
const {account, adminRoom, protocol} = await this.getInstanceEventContext(ev);
account.setStatus('available', true);
if (!this.config.purple.sendConnectionNotices) {
return;
}
await this.bridge.getIntent().sendMessage(adminRoom.roomId, {
msgtype: "m.notice",
body: `🟢 ${ev.account.username} (${protocol.name}) has signed on`
});
}

private async onAccountConnectionError(ev: IAccountErrorEvent) {
log.warn(`${ev.account.protocol_id}://${ev.account.username} had a connection error`, ev);
if (!this.config.purple.sendConnectionNotices) {
return;
}
const {protocol, adminRoom} = await this.getInstanceEventContext(ev);
await this.bridge.getIntent().sendMessage(adminRoom.roomId, {
msgtype: "m.notice",
body: `⚠️ ${ev.account.username} (${protocol.name}) had a connection error: ${ev.description}`
});
}

private async onAccountSignedOff(ev: IAccountEvent) {
log.info(`${ev.account.protocol_id}://${ev.account.username} signed off.`);
const {protocol, adminRoom} = await this.getInstanceEventContext(ev);
if (!this.config.purple.sendConnectionNotices) {
return;
}
await this.bridge.getIntent().sendMessage(adminRoom.roomId, {
msgtype: "m.notice",
body: `🔴 ${ev.account.username} (${protocol.name}) has signed off `
});
}
}
2 changes: 2 additions & 0 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class Config {
public readonly purple: IConfigPurple = {
backendOpts: undefined,
backend: "node-purple",
sendConnectionNotices: false,
defaultAccountSettings: undefined,
};

Expand Down Expand Up @@ -112,6 +113,7 @@ export interface IConfigBridge {
export interface IConfigPurple {
backendOpts: IPurpleBackendOpts|IXJSBackendOpts|undefined;
backend: "node-purple"|"xmpp-js";
sendConnectionNotices?: boolean;
defaultAccountSettings?: {[key: string]: IAccountExtraConfig};
}

Expand Down
77 changes: 36 additions & 41 deletions src/Program.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import { Cli, Bridge, AppServiceRegistration, Logging, WeakEvent, TypingEvent, Request } from "matrix-appservice-bridge";
import { AccountHandler } from "./AccountHandler";
import { AutoRegistration } from "./AutoRegistration";
import { Cli, Bridge, AppServiceRegistration, Logging, TypingEvent, Request, RoomBridgeStoreEntry } from "matrix-appservice-bridge";
import { Config } from "./Config";
import { Deduplicator } from "./Deduplicator";
import { EventEmitter } from "events";
import { GatewayHandler } from "./GatewayHandler";
import { IAccountEvent } from "./bifrost/Events";
import { IBifrostInstance } from "./bifrost/Instance";
import { install as installSMS } from "source-map-support";
import { IRemoteUserAdminData, MROOM_TYPE_UADMIN } from "./store/Types";
import { IStore, initiateStore } from "./store/Store";
import { MatrixEventHandler } from "./MatrixEventHandler";
import { MatrixRoomHandler } from "./MatrixRoomHandler";
import { IBifrostInstance } from "./bifrost/Instance";
import { IAccountEvent } from "./bifrost/Events";
import { Metrics } from "./Metrics";
import { ProfileSync } from "./ProfileSync";
import { RoomSync } from "./RoomSync";
import { IStore, initiateStore } from "./store/Store";
import { Deduplicator } from "./Deduplicator";
import { Config } from "./Config";
import { Util } from "./Util";
import { XmppJsInstance } from "./xmppjs/XJSInstance";
import { Metrics } from "./Metrics";
import { AutoRegistration } from "./AutoRegistration";
import { GatewayHandler } from "./GatewayHandler";
import * as request from "request-promise-native";

const log = Logging.get("Program");
const bridgeLog = Logging.get("bridge");

import { install as installSMS } from "source-map-support";

installSMS();

Expand All @@ -36,10 +38,11 @@ class Program {
private gatewayHandler!: GatewayHandler;
private profileSync: ProfileSync|undefined;
private roomSync: RoomSync|undefined;
private purple?: IBifrostInstance;
private bifrostInstance?: IBifrostInstance;
private store!: IStore;
private cfg: Config;
private deduplicator: Deduplicator;
private accountHandler?: AccountHandler;

constructor() {
this.cli = new Cli({
Expand All @@ -61,7 +64,7 @@ class Program {
});
this.cfg = new Config();
this.deduplicator = new Deduplicator();
process.on("SIGTERM", () =>
process.on("SIGINT", () =>
this.killBridge()
)
}
Expand All @@ -72,7 +75,6 @@ class Program {

public start() {
Logging.configure({console: "debug"});

try {
this.cli.run();
} catch (ex) {
Expand Down Expand Up @@ -136,9 +138,10 @@ class Program {
}

private async killBridge() {
log.info("SIGTERM recieved, killing bridge");
log.info("SIGINT recieved, killing bridge");
await this.bridge.close();
await this.purple.close();
await this.bifrostInstance.close();
process.exit(0);
}

private async runBridge(port: number, config: any) {
Expand Down Expand Up @@ -167,7 +170,6 @@ class Program {
}
this.bridge = new Bridge({
controller: {
// onUserQuery: userQuery,
onAliasQuery: (alias, aliasLocalpart) => this.eventHandler!.onAliasQuery(alias, aliasLocalpart),
onEvent: (r) => {
if (this.eventHandler === undefined) {return; }
Expand Down Expand Up @@ -202,20 +204,20 @@ class Program {
registration: this.cli.getRegistrationFilePath(),
...storeParams,
});
log.info("Starting appservice listener on port", port);
await this.bridge.run(port, this.cfg);
await this.bridge.initalise();
if (this.cfg.purple.backend === "node-purple") {
log.info("Selecting node-purple as a backend");
// eslint-disable-next-line @typescript-eslint/no-var-requires
this.purple = new (require("./purple/PurpleInstance").PurpleInstance)(this.cfg.purple);
this.bifrostInstance = new (require("./purple/PurpleInstance").PurpleInstance)(this.cfg.purple);
} else if (this.cfg.purple.backend === "xmpp-js") {
log.info("Selecting xmpp-js as a backend");
// eslint-disable-next-line @typescript-eslint/no-var-requires
this.purple = new (require("./xmppjs/XJSInstance").XmppJsInstance)(this.cfg);
this.bifrostInstance = new (require("./xmppjs/XJSInstance").XmppJsInstance)(this.cfg);
} else {
throw new Error(`Backend ${this.cfg.purple.backend} not supported`);
}
const purple = this.purple!;
const bifrostInstance = this.bifrostInstance!;


if (this.cfg.metrics.enabled) {
log.info("Enabling metrics");
Expand All @@ -235,14 +237,14 @@ class Program {

this.profileSync = new ProfileSync(this.bridge, this.cfg, this.store);
this.roomHandler = new MatrixRoomHandler(
this.purple!, this.profileSync, this.store, this.cfg, this.deduplicator,
this.bifrostInstance!, this.profileSync, this.store, this.cfg, this.deduplicator,
);
this.gatewayHandler = new GatewayHandler(purple, this.bridge, this.cfg, this.store, this.profileSync);
this.gatewayHandler = new GatewayHandler(bifrostInstance, this.bridge, this.cfg, this.store, this.profileSync);
this.roomSync = new RoomSync(
purple, this.store, this.deduplicator, this.gatewayHandler, this.bridge.getIntent(),
bifrostInstance, this.store, this.deduplicator, this.gatewayHandler, this.bridge.getIntent(),
);
this.eventHandler = new MatrixEventHandler(
purple, this.store, this.deduplicator, this.config, this.gatewayHandler,
bifrostInstance, this.store, this.deduplicator, this.config, this.gatewayHandler,
);
let autoReg: AutoRegistration|undefined;
if (this.config.autoRegistration.enabled && this.config.autoRegistration.protocolSteps !== undefined) {
Expand All @@ -251,41 +253,34 @@ class Program {
this.config.access,
this.bridge,
this.store,
purple,
bifrostInstance,
);
}

this.eventHandler.setBridge(this.bridge, autoReg || undefined);
this.roomHandler.setBridge(this.bridge);
log.info("Bridge has started.");
try {
if (purple instanceof XmppJsInstance) {
if (bifrostInstance instanceof XmppJsInstance) {
if (!autoReg) {
throw Error('AutoRegistration not enabled in config, bridge cannot start');
}
purple.preStart(this.bridge, autoReg);
bifrostInstance.preStart(this.bridge, autoReg);
}
await purple.start();
await bifrostInstance.start();
this.accountHandler = new AccountHandler(bifrostInstance, this.store, this.bridge, this.config);
await this.roomSync.sync(this.bridge.getBot());
if (purple instanceof XmppJsInstance) {
if (bifrostInstance instanceof XmppJsInstance) {
log.debug("Signing in accounts...");
purple.signInAccounts(
await this.store.getUsernameMxidForProtocol(purple.getProtocols()[0]),
bifrostInstance.signInAccounts(
await this.store.getUsernameMxidForProtocol(bifrostInstance.getProtocols()[0]),
);
}
} catch (ex) {
log.error("Encountered an error starting the backend:", ex);
process.exit(1);
}
this.purple!.on("account-signed-on", (ev: IAccountEvent) => {
log.info(`${ev.account.protocol_id}://${ev.account.username} signed on`);
this.purple.getAccount(ev.account.username, ev.account.protocol_id, ).setStatus('available', true);
});
this.purple!.on("account-connection-error", (ev: IAccountEvent) => {
log.warn(`${ev.account.protocol_id}://${ev.account.username} had a connection error`, ev);
});
this.purple!.on("account-signed-off", (ev: IAccountEvent) => {
log.info(`${ev.account.protocol_id}://${ev.account.username} signed off.`);
bifrostInstance.on("account-signed-off", (ev: IAccountEvent) => {
this.deduplicator.removeChosenOneFromAllRooms(
Util.createRemoteId(ev.account.protocol_id, ev.account.username),
);
Expand Down
6 changes: 6 additions & 0 deletions src/bifrost/Events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export interface IAccountEvent extends IEventBody {
account: any|IAccountMinimal;
}

export interface IAccountErrorEvent extends IEventBody {
account: any|IAccountMinimal;
type: number;
description: string;
}

export interface IConversationEvent extends IAccountEvent {
conv: any | IConversationMinimal;
}
Expand Down
3 changes: 2 additions & 1 deletion src/purple/PurpleInstance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @ts-ignore - These are optional.
import { helper, plugins, messaging, Conversation } from "node-purple";
import { helper, plugins, messaging, Conversation, core } from "node-purple";
import { EventEmitter } from "events";
import { PurpleAccount } from "./PurpleAccount";
import { IBifrostInstance } from "../bifrost/Instance";
Expand Down Expand Up @@ -141,6 +141,7 @@ export class PurpleInstance extends EventEmitter implements IBifrostInstance {
if (this.interval) {
clearInterval(this.interval);
this.interval = undefined;
core.quit();
}
}

Expand Down
17 changes: 14 additions & 3 deletions src/store/NeDBStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,11 @@ export class NeDBStore implements IStore {
return null;
}

public async getIMRoom(matrixUserId: string, protocolId: string, remoteUserId: string): Promise<RoomBridgeStoreEntry|null> {
public async getIMRoom(matrixUser: string, protocolId: string, recipient: string): Promise<RoomBridgeStoreEntry|null> {
const remoteEntries = await this.roomStore.getEntriesByRemoteRoomData({
matrixUser: matrixUserId,
matrixUser,
protocol_id: protocolId,
recipient: remoteUserId,
recipient,
} as IRemoteImData as Record<string, unknown>);
const suitableEntries = remoteEntries.filter((e) => e.matrix?.get("type") === MROOM_TYPE_IM)[0];
if (!suitableEntries) {
Expand All @@ -121,6 +121,17 @@ export class NeDBStore implements IStore {
return suitableEntries;
}

public async getAdminRoom(matrixUser: string): Promise<MatrixRoom|null> {
const remoteEntries = await this.roomStore.getEntriesByRemoteRoomData({
matrixUser,
} as IRemoteImData as Record<string, unknown>);
const suitableEntries = remoteEntries.filter((e) => e.matrix?.get("type") === MROOM_TYPE_UADMIN)[0];
if (!suitableEntries || !suitableEntries.matrix) {
return null;
}
return suitableEntries.matrix;
}

public async getUsernameMxidForProtocol(protocol: BifrostProtocol): Promise<{[mxid: string]: string}> {
const set = {};
const users = (await this.userStore.getByRemoteData({protocol_id: protocol.id, type: MUSER_TYPE_ACCOUNT}))
Expand Down
4 changes: 3 additions & 1 deletion src/store/Store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MatrixUser, Bridge, RoomBridgeStoreEntry } from "matrix-appservice-bridge";
import { MatrixUser, Bridge, RoomBridgeStoreEntry, MatrixRoom } from "matrix-appservice-bridge";
import { IRemoteRoomData, IRemoteGroupData, MROOM_TYPES } from "./Types";
import { BifrostProtocol } from "../bifrost/Protocol";
import { IAccountMinimal } from "../bifrost/Events";
Expand Down Expand Up @@ -36,6 +36,8 @@ export interface IStore {

getIMRoom(matrixUserId: string, protocolId: string, remoteUserId: string): Promise<RoomBridgeStoreEntry|null>;

getAdminRoom(matrixUserId: string): Promise<MatrixRoom|null>;

getUsernameMxidForProtocol(protocol: BifrostProtocol): Promise<{[mxid: string]: string}>;

getRoomsOfType(type: MROOM_TYPES): Promise<RoomBridgeStoreEntry[]>;
Expand Down
1 change: 0 additions & 1 deletion src/store/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export interface IMatrixUserData {
}

export interface IRemoteUserAccount {
// XXX: We are mixing camel case and snake case in here.
type: MUSER_TYPES;
username: string;
protocolId: string;
Expand Down
Loading