Skip to content

Commit

Permalink
Merge pull request #288 from matrix-org/hs/jingle-file-uploads
Browse files Browse the repository at this point in the history
Support Jingle file uploads
  • Loading branch information
Half-Shot authored Nov 7, 2022
2 parents 9a611f1 + cc38107 commit 324d1f9
Show file tree
Hide file tree
Showing 39 changed files with 1,092 additions and 336 deletions.
3 changes: 1 addition & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"ecmaVersion": 9
},
"plugins": [
"eslint-plugin-import",
Expand Down Expand Up @@ -48,7 +48,6 @@
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/indent": "error",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-empty-function": ["warn", {"allow": ["constructors"]}],
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-misused-new": "error",
Expand Down
1 change: 1 addition & 0 deletions changelog.d/288.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for Jingle file uploads, presence, IM typing notifications for the XMPP backend.
7 changes: 5 additions & 2 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ purple:
domain: "matrix.localhost"
# password needed by the component.
password: "jam"
jingle:
# Automatically download files from Jingle file transfers. Unsafe for untrusted networks.
autodownload: false

# Default settings to set for new accounts, useful for node-purple. NOT used for xmpp.js
# defaultAccountSettings:
Expand Down Expand Up @@ -83,14 +86,14 @@ portals:
# List of regexes to match a alias that can be turned into a bridge.
aliases:
# This matches #_bifrost_ followed by anything
"/^_bifrost_(.+)$/":
"^_bifrost_(.+)$":
# Use the xmpp-js protocol.
protocol: "xmpp-js"
properties:
# Set room to the first regex match
room: "regex:1"
# Set the server to be conf.localhost
server: "regex:2"
server: "conf.localhost"


# Automatically register users with accounts if they join/get invited
Expand Down
30 changes: 21 additions & 9 deletions src/AutoRegistration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IConfigAutoReg, IConfigAccessControl } from "./Config";
import { Bridge, MatrixUser, Logger } from "matrix-appservice-bridge";
import { Bridge, MatrixUser, Logger, UserProfile } from "matrix-appservice-bridge";
import request from "axios";
import { Util } from "./Util";
import { IStore } from "./store/Store";
Expand All @@ -23,14 +23,17 @@ export interface IAutoRegStep {
headers: {[key: string]: string}; // key -> value
}

const ESCAPE_TEMPLATE_REGEX = /[-\/\\^$*+?.()|[\]{}]/g;

export class AutoRegistration {
private nameCache = new QuickLRU<string, {[key: string]: string}>({ maxSize: this.autoRegConfig.registrationNameCacheSize });
constructor(
private autoRegConfig: IConfigAutoReg,
private accessConfig: IConfigAccessControl,
private bridge: Bridge,
private store: IStore,
private protoInstance: IBifrostInstance) { }
private protoInstance: IBifrostInstance) {
}

public isSupported(protocol: string) {
return Object.keys(this.autoRegConfig.protocolSteps!).includes(protocol);
Expand All @@ -49,7 +52,7 @@ export class AutoRegistration {
// We assume the caller has already validated this.
const proto = this.protoInstance.getProtocol(protocol)!;
const step = this.autoRegConfig.protocolSteps![protocol];
let res: {username: string, extraParams: any};
let res: {username: string, extraParams: Record<string, string>};
if (step.type === "http") {
res = await this.handleHttpRegistration(mxId, step);
} else if (step.type === "implicit") {
Expand Down Expand Up @@ -78,6 +81,7 @@ export class AutoRegistration {
log.info("Attempting to reverse register", username);
const step = this.autoRegConfig.protocolSteps![protocol.id];
const usernameFormat = step.parameters.username;
const domainParameter = step.parameters.domain;
const hasLocalpart = usernameFormat.includes("<T_LOCALPART>");
if (!usernameFormat) {
throw Error("No parameter 'username' on registration step, cannot get mxid");
Expand All @@ -97,16 +101,24 @@ export class AutoRegistration {
// Replace any ^a strings with A.
mxid = mxid.replace(/(\^([a-z]))/g, (m, p1, p2) => p2.toUpperCase());
} else if (hasLocalpart && usernameFormat.includes("<T_DOMAIN>")) {
let regexStr = usernameFormat.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
let regexStr = usernameFormat.replace(ESCAPE_TEMPLATE_REGEX, "\\$&");
regexStr = regexStr.replace("<T_LOCALPART>", "(.+)").replace("<T_DOMAIN>", "(.+)");
const match = new RegExp(regexStr).exec(username);
if (!match || match.length < 3) {
throw Error("String didn't match");
}
log.debug("Result:", match);
mxid = `@${match[1]}:${match[2]}`;
} else if (hasLocalpart) {
throw Error("We don't support localpart only, yet.");
if (!domainParameter) {
throw Error('`domain` must be specified in autoregistration parameters when only using a localpart')
}
let regexStr = usernameFormat.replace(ESCAPE_TEMPLATE_REGEX, "\\$&");
regexStr = regexStr.replace("<T_LOCALPART>", "(.+)");
const match = new RegExp(regexStr).exec(username);
if (!match || match.length < 2) {
throw Error("String didn't match");
}
mxid = `@${match[1]}:${domainParameter}`;
} else {
throw Error("No T_MXID or T_MXID_SANE on username parameter, cannot get mxid");
}
Expand Down Expand Up @@ -143,7 +155,7 @@ export class AutoRegistration {
return result;
}

public static generateParameters(parameters: {[key: string]: string}, mxId: string, profile?: {displayname: string; avatar_url: string})
public static generateParameters(parameters: {[key: string]: string}, mxId: string, profile?: UserProfile)
: {[key: string]: string} {
const body = {};
const mxUser = new MatrixUser(mxId);
Expand All @@ -153,7 +165,7 @@ export class AutoRegistration {
return body;
}

private static generateParameter(val: string, mxUser: MatrixUser, profile?: {displayname: string; avatar_url: string}) {
private static generateParameter(val: string, mxUser: MatrixUser, profile?: UserProfile) {
val = val.replace("<T_MXID>", mxUser.getId());
val = val.replace("<T_MXID_SANE>", this.getSaneMxId(mxUser.getId()));
val = val.replace("<T_LOCALPART>", mxUser.localpart);
Expand All @@ -175,7 +187,7 @@ export class AutoRegistration {
const opts = step.opts as IAutoRegHttpOpts;
log.debug("HttpReg: Fetching user profile");
const intent = this.bridge.getIntent();
let profile: any = {};
let profile: UserProfile = {};
try {
profile = await intent.getProfileInfo(mxId);
if (profile.avatar_url) {
Expand Down
5 changes: 2 additions & 3 deletions src/Config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { IAutoRegStep } from "./AutoRegistration";
import { IRoomAlias } from "./RoomAliasSet";
import { IXJSBackendOpts } from "./xmppjs/XJSBackendOpts";
import { Logger } from "matrix-appservice-bridge";
import { PgDataStoreOpts } from "./store/postgres/PgDatastore";
import { IAccountExtraConfig } from "./bifrost/Account";
import { IPurpleBackendOpts } from "./purple/PurpleInstance";

const log = new Logger("Config");
export type ConfigValue = {[key: string]: ConfigValue}|string|boolean|number|null;

export class Config {

Expand Down Expand Up @@ -89,7 +88,7 @@ export class Config {
* @param newConfig Config keys
* @param configLayer Private parameter
*/
public ApplyConfig(newConfig: {[key: string]: any}, configLayer: any = this) {
public ApplyConfig(newConfig: ConfigValue, configLayer: ConfigValue|Config = this) {
Object.keys(newConfig).forEach((key) => {
if (typeof(configLayer[key]) === "object" &&
!Array.isArray(configLayer[key])) {
Expand Down
3 changes: 1 addition & 2 deletions src/GatewayHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const log = new Logger("GatewayHandler");
* for XMPP.js).
*/
export class GatewayHandler {
private aliasCache: Map<string, IGatewayRoom> = new Map();
private roomIdCache: Map<string, Promise<IGatewayRoom>> = new Map();

constructor(
Expand Down Expand Up @@ -97,7 +96,7 @@ export class GatewayHandler {
this.purple.gateway.sendMatrixMessage(chatName, sender, body, room);
}

public async sendStateEvent(chatName: string, sender: string, ev: any , context: RoomBridgeStoreEntry) {
public async sendStateEvent(chatName: string, sender: string, ev: any, context: RoomBridgeStoreEntry) {
if (!this.purple.gateway) {
return;
}
Expand Down
68 changes: 38 additions & 30 deletions src/MatrixEventHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Bridge, MatrixUser, Request, WeakEvent, RoomBridgeStoreEntry, TypingEvent } from "matrix-appservice-bridge";
import { Bridge, MatrixUser, Request, WeakEvent, RoomBridgeStoreEntry, TypingEvent, PresenceEvent } from "matrix-appservice-bridge";
import { MatrixMembershipEvent, MatrixMessageEvent } from "./MatrixTypes";
import { MROOM_TYPE_UADMIN, MROOM_TYPE_IM, MROOM_TYPE_GROUP,
IRemoteUserAdminData,
Expand All @@ -14,7 +14,7 @@ import { Deduplicator } from "./Deduplicator";
import { AutoRegistration } from "./AutoRegistration";
import { Config } from "./Config";
import { IStore } from "./store/Store";
import { IAccountEvent, IChatJoinProperties, IChatJoined, IConversationEvent } from "./bifrost/Events";
import { IAccountEvent, IChatJoinProperties, IConversationEvent } from "./bifrost/Events";
import { ProtoHacks } from "./ProtoHacks";
import { RoomAliasSet } from "./RoomAliasSet";
import { MessageFormatter } from "./MessageFormatter";
Expand All @@ -27,32 +27,21 @@ const log = new Logger("MatrixEventHandler");
* Handles events coming into the appservice.
*/
export class MatrixEventHandler {
private bridge?: Bridge;
private autoReg: AutoRegistration | null = null;
private roomAliases: RoomAliasSet;
private pendingRoomAliases: Map<string, {protocol: BifrostProtocol, props: IChatJoinProperties}>;
private readonly roomAliases: RoomAliasSet;
private readonly pendingRoomAliases: Map<string, {protocol: BifrostProtocol, props: IChatJoinProperties}>;
constructor(
private purple: IBifrostInstance,
private store: IStore,
private deduplicator: Deduplicator,
private config: Config,
private gatewayHandler: GatewayHandler,
private readonly purple: IBifrostInstance,
private readonly store: IStore,
private readonly deduplicator: Deduplicator,
private readonly config: Config,
private readonly gatewayHandler: GatewayHandler,
private readonly bridge: Bridge,
private readonly autoReg: AutoRegistration|null = null,
) {
this.roomAliases = new RoomAliasSet(this.config.portals, this.purple);
this.pendingRoomAliases = new Map();
}

/**
* Set the bridge for us to use. This must be called after MatrixEventHandler
* has been created.
*
* @return [description]
*/
public setBridge(bridge: Bridge, autoReg?: AutoRegistration) {
this.bridge = bridge;
this.autoReg = autoReg || null;
}

public async onAliasQuery(alias: string, aliasLocalpart: string) {
const res = this.roomAliases.getOptsForAlias(aliasLocalpart);
log.info(`Got request to bridge ${aliasLocalpart}`);
Expand Down Expand Up @@ -108,7 +97,7 @@ export class MatrixEventHandler {
log.info(`onAliasQueried:`, alias, roomId);
const {protocol, props} = this.pendingRoomAliases.get(alias)!;
this.pendingRoomAliases.delete(alias);
const remoteData = {
const remoteData: IRemoteGroupData = {
protocol_id: protocol.id,
room_name: ProtoHacks.getRoomNameFromProps(protocol.id, props),
properties: Util.sanitizeProperties(props), // for joining
Expand Down Expand Up @@ -164,7 +153,7 @@ export class MatrixEventHandler {
protocol,
} = this.purple.getUsernameFromMxid(event.state_key!, this.config.bridge.userPrefix);
log.debug("Mapped username to", username);
const remoteData = {
const remoteData: IRemoteImData = {
matrixUser: event.sender,
protocol_id: protocol.id,
recipient: username,
Expand Down Expand Up @@ -282,6 +271,25 @@ export class MatrixEventHandler {
acct.sendIMTyping(recipient, isUserTyping);
}

async onPresence(req: Request<PresenceEvent>) {
const data = req.getData();
const remoteUsers = await this.store.getAllAccountsForMatrixUser(data.sender);
for (const remoteUser of remoteUsers) {
try {
log.debug(`Sending presence on behalf of Matrix user via ${remoteUser.protocolId}:${remoteUser.username}`);
const account = this.purple.getAccount(remoteUser.username, remoteUser.protocolId);
const allInterestedUsers = (
await this.store.getAllIMRoomsForAccount(data.sender, account.protocol.id)
).map((r) => r.remote.get<string>('recipient'));
if (account?.setPresence) {
account.setPresence(data.content, allInterestedUsers);
}
} catch (ex) {
log.warn(`Failed to handle presence update for ${data.sender} via account ${remoteUser.protocolId}:${remoteUser.username}`, ex);
}
}
}

/* NOTE: Command handling should really be it's own class, but I am cutting corners.*/
private async handleCommand(args: string[], event: WeakEvent) {
if (!this.bridge) {
Expand Down Expand Up @@ -448,7 +456,7 @@ export class MatrixEventHandler {
log.warn("Failed to join chat for plumbing:", ex);
throw Error("Failed to join chat");
}
const remoteData = {
const remoteData: IRemoteGroupData = {
protocol_id: acct.protocol.id,
room_name: res.conv.name,
plumbed: true,
Expand Down Expand Up @@ -621,7 +629,7 @@ Say \`help\` for more commands.
return;
}
try {
const {acct, newAcct} = await this.getAccountForMxid(event.sender, roomProtocol);
const {acct} = await this.getAccountForMxid(event.sender, roomProtocol);
log.info(`Got ${acct.name} for ${event.sender}`);
if (!acct.isInRoom(name)) {
log.debug(`${event.sender} talked in ${name}, joining them.`);
Expand All @@ -636,11 +644,11 @@ Say \`help\` for more commands.
let nick = "";
// XXX: Gnarly way of trying to determine who we are.
try {
const conv = acct.getConversation(roomName);
const conv = acct.getConversation && acct.getConversation(roomName);
if (!conv) {
throw Error();
throw Error("Could not find conversation");
}
nick = conv ? this.purple.getNickForChat(conv) || acct.name : acct.name;
nick = conv && this.purple.getNickForChat ? this.purple.getNickForChat(conv) || acct.name : acct.name;
} catch (ex) {
nick = acct.name;
}
Expand Down Expand Up @@ -854,8 +862,8 @@ E.g. \`${command} ${acct.protocol.id}\` ${required.join(" ")} ${optional.join("
}, 60000);
});
log.debug("Account signed in, joining room");
acct.setJoinPropertiesForRoom?.(name, properties);
await acct.joinChat(properties, this.purple, 5000);
acct.setJoinPropertiesForRoom(name, properties);
}

private async getAccountForMxid(sender: string, protocol: string,
Expand Down
Loading

0 comments on commit 324d1f9

Please sign in to comment.