reflectionircd/src/Server.ts
2023-07-03 16:15:51 +00:00

669 lines
No EOL
29 KiB
TypeScript

import axios, { Axios } from "axios";
import { readFileSync } from "fs";
import { createServer, Server } from "tls";
import { IRCClient } from "./IRCClient.js";
import { Channel } from "./Channel.js";
import { Client } from "./Client.js";
import { MatrixUser } from "./MatrixUser.js";
export class IRCServer {
public config: any
public homeserver: string
public mxid: string
public name: string
public apiCall: Axios
public channels: Map<string, Channel>
public invitedChannels: Set<string>
public roomIdToChannel: Map<string, Channel>
public directRooms: Map<string, string[]>
private syncLocks: Set<Channel>
private directMessages: Set<string>
private matrixUsers: Map<string, MatrixUser>
public ourMatrixUser: MatrixUser;
private client: Client
public nickToMatrixUser: Map<string, MatrixUser>
public eventIdStore: Map<string, Client>
public eventIDToLabel: Map<string, string>
private nextBatch: string
private isSyncing: boolean
private initialSync: boolean
private currentSyncTime: number
private listener: Server;
constructor() {
this.config = JSON.parse(readFileSync(process.argv[2], {"encoding": "utf-8"}));
this.homeserver = this.config.homeserver;
this.mxid = this.config.mxid;
this.name = this.config.serverName;
this.apiCall = axios.create({
baseURL: `${this.homeserver}/_matrix/client/v3`,
timeout: 180000,
headers: {"Authorization": `Bearer ${this.config.accessToken}`}
})
this.channels = new Map();
this.invitedChannels = new Set();
this.roomIdToChannel = new Map();
this.directRooms = new Map();
this.syncLocks = new Set();
this.directMessages = new Set();
this.matrixUsers = new Map();
this.nickToMatrixUser = new Map();
this.client = new Client(this);
this.eventIdStore = new Map();
this.eventIDToLabel = new Map();
this.nextBatch = "";
this.isSyncing = false;
this.initialSync = false;
this.currentSyncTime = 0;
setInterval(this.doSync.bind(this), 2000);
this.listener = createServer({
cert: readFileSync(this.config["certFile"]),
key: readFileSync(this.config["keyFile"])
});
this.ourMatrixUser = this.getOrCreateMatrixUser(this.mxid);
this.apiCall.get("/account/whoami").then(r => {
this.doLog("Authentication successful, starting initial sync");
this.getDirectMessages();
this.doSync();
}).catch(e => {
this.doLog(e);
})
}
doLog(message: string) {
console.log(`[${new Date().toISOString()}] ${this.mxid}: ${message}`);
}
doSync(): void {
if (this.isSyncing) {
if ((Date.now() - this.currentSyncTime) > this.config.lagWarningLimit && this.currentSyncTime > 0)
this.doLog(`Sync is lagging, current sync has been running for ${Date.now() - this.currentSyncTime} milliseconds`);
return;
}
this.isSyncing = true;
const endpoint = (this.nextBatch === "") ? "/sync" : `/sync?since=${this.nextBatch}&timeout=${this.config.syncTimeout}`;
this.currentSyncTime = Date.now();
this.apiCall.get(endpoint).then(response => {
const data = response.data;
this.nextBatch = data.next_batch;
const rooms = data.rooms;
if (rooms) {
if (rooms['join']) {
for (const roomId of Object.keys(rooms.join)) {
const targetChannel = this.getOrCreateIRCChannel(roomId);
rooms.join[roomId].timeline.events.forEach((nextEvent: any) => {
if (targetChannel.isSynced())
this.routeMatrixEvent(nextEvent, targetChannel);
});
rooms.join[roomId].ephemeral.events.forEach((nextEvent: any) => {
if (targetChannel.isSynced())
this.routeMatrixEvent(nextEvent, targetChannel);
});
}
}
if (rooms['invite']) {
for (const roomId of Object.keys(rooms.invite)) {
this.invitedChannels.add(roomId);
}
}
}
this.isSyncing = false;
}).catch((error) => {
if (error.response) {
this.doLog(`Error: ${error.response.status} ${error.response.statusText}`)
} else {
this.doLog(`Error: ${error}`);
}
this.isSyncing = false;
});
}
getDirectMessages() {
this.apiCall.get(`/user/${this.mxid}/account_data/m.direct`).then(response => {
if (!response.data)
return;
//@ts-ignore
Object.entries(response.data).forEach(m => this.directRooms.set(m[0], m[1]));
}).catch(e => {
const errcode = e.response?.data?.errcode;
if (errcode !== "M_NOT_FOUND")
this.doLog(`Error: ${e}`);
})
}
getOrCreateIRCChannel(roomId: string): Channel {
const maybeChannel = this.roomIdToChannel.get(roomId);
if (maybeChannel)
return maybeChannel;
this.getDirectMessages();
const newChannel = new Channel(roomId, this);
this.syncLocks.add(newChannel);
this.roomIdToChannel.set(roomId, newChannel);
this.channels.set(roomId, newChannel);
return newChannel;
}
finishChannelSync(targetChannel: Channel) {
this.syncLocks.delete(targetChannel);
this.channels.delete(targetChannel.roomId);
this.invitedChannels.delete(targetChannel.roomId);
this.channels.set(targetChannel.name, targetChannel);
this.roomIdToChannel.set(targetChannel.roomId, targetChannel);
this.joinNewIRCClient(this.client, targetChannel);
if (this.initialSync === false && this.syncLocks.size === 0) {
this.initialSync = true;
this.doLog('Synced to network!');
this.listener.on('secureConnection', (c) => {
new IRCClient(c, this);
})
this.listener.listen(this.config["port"], () => {
this.doLog(`Listening on port ${this.config["port"]}`);
})
}
}
getOrCreateMatrixUser(mxid: string): MatrixUser {
let maybeMatrixUser = this.matrixUsers.get(mxid);
if (maybeMatrixUser) {
return maybeMatrixUser;
}
const localPart = mxid.split(":")[0].substring(1).replace(' ', '-20');
if (!this.nickToMatrixUser.has(localPart)) {
const newMatrixUser = new MatrixUser(mxid, localPart);
this.matrixUsers.set(mxid, newMatrixUser);
this.nickToMatrixUser.set(localPart, newMatrixUser);
return newMatrixUser;
}
const homeserver = mxid.split(":")[1];
const homeserverArray = homeserver.split('.');
const baseDomainNum = homeserverArray.length - 2;
let potentialNick = `${localPart}-${homeserverArray[baseDomainNum]}`;
if (!this.nickToMatrixUser.has(potentialNick)) {
const newMatrixUser = new MatrixUser(mxid, potentialNick);
this.matrixUsers.set(mxid, newMatrixUser);
this.nickToMatrixUser.set(potentialNick, newMatrixUser);
return newMatrixUser;
}
potentialNick = `${localPart}-${homeserver}`;
const newMatrixUser = new MatrixUser(mxid, potentialNick);
this.matrixUsers.set(mxid, newMatrixUser);
this.nickToMatrixUser.set(potentialNick, newMatrixUser);
return newMatrixUser;
}
getMask(): string {
return this.ourMatrixUser.getMask();
}
joinNewIRCClient(client: Client, targetChannel: Channel) {
if (client.enabledCaps.has('extended-join')) {
client.sendMessage(this.getMask(), "JOIN", [targetChannel.name, this.ourMatrixUser.accountName, this.mxid], new Map([['account', this.ourMatrixUser.realname]]));
} else {
client.sendMessage(this.getMask(), "JOIN", [targetChannel.name], new Map([['account', this.ourMatrixUser.realname]]));
}
client.sendNAMES(targetChannel);
client.sendTOPIC(targetChannel, new Map());
}
checkForLazyJoin(event: any, sourceUser: MatrixUser, targetChannel: Channel) {
if (!targetChannel.matrixUsers.has(sourceUser.nick)) {
targetChannel.matrixUsers.set(sourceUser.nick, sourceUser);
const prefix = sourceUser.getMask();
const joinTags = new Map([["account", sourceUser.accountName], ['time', new Date(event["origin_server_ts"]).toISOString()]])
if (this.client.enabledCaps.has('extended-join')) {
this.client.sendMessage(prefix, "JOIN", [targetChannel.name, sourceUser.accountName, sourceUser.realname], joinTags);
} else {
this.client.sendMessage(prefix, "JOIN", [targetChannel.name], joinTags);
}
}
}
addClient(client: Client) {
if (this.client.isRegistered) {
this.client.closeConnectionWithError("New client connected");
}
this.client = client;
if (this.initialSync) {
for (const channel of this.channels.values()) {
this.joinNewIRCClient(client, channel);
}
this.invitedChannels.forEach(roomId => this.client.sendMessage(this.name, "INVITE", [this.ourMatrixUser.nick, roomId], new Map()));
}
}
routeMatrixEvent(nextEvent: any, targetChannel: Channel) {
switch (nextEvent["type"]) {
case 'm.presence':
this.handleMatrixPresence(nextEvent);
break;
case 'm.reaction':
this.handleMatrixReaction(nextEvent, targetChannel);
break;
case 'm.room.canonical_alias':
this.updateRoomName(nextEvent, targetChannel);
break;
case 'm.room.encrypted':
this.handleEncryptedMessage(nextEvent, targetChannel);
break;
case 'm.room.guest_access':
this.handleMatrixGuestAccess(nextEvent, targetChannel);
break;
case 'm.room.history_visibility':
this.handleMatrixHistoryVisibility(nextEvent, targetChannel);
break;
case 'm.room.join_rules':
this.handleMatrixJoinRule(nextEvent, targetChannel);
break;
case 'm.room.member':
this.handleMatrixMember(nextEvent, targetChannel);
break;
case 'm.room.message':
this.handleMatrixMessage(nextEvent, targetChannel);
break;
case 'm.room.power_levels':
this.handleMatrixPL(nextEvent, targetChannel);
break;
case 'm.room.redaction':
this.handleMatrixRedaction(nextEvent, targetChannel);
break;
case 'm.room.tombstone':
this.handleMatrixTombstone(nextEvent,targetChannel);
break;
case 'm.room.topic':
this.handleMatrixTopic(nextEvent, targetChannel);
break;
case 'm.sticker':
this.handleMatrixSticker(nextEvent, targetChannel);
break;
case 'm.typing':
this.handleMatrixTyping(nextEvent, targetChannel);
break;
// Add some events we aren't going to use now (or ever)
case 'm.receipt':
case 'm.room.name':
case 'm.room.create':
case 'uk.half-shot.bridge':
case 'org.matrix.appservice-irc.config':
case 'org.matrix.appservice-irc.connection':
case 'im.vector.modular.widgets':
case 'm.room.avatar':
case 'm.room.third_party_invite':
case 'm.room.related_groups':
case 'm.room.bot.options':
case 'm.room.pinned_events':
case 'm.room.server_acl':
case 'org.matrix.room.preview_urls':
case 'm.space.child':
case 'm.space.parent':
case 'm.room.plumbing':
case 'm.room.bridging':
case 'org.matrix.confbot.auditorium':
case 'org.matrix.confbot.child':
case 'org.matrix.confbot.parent':
case 'org.matrix.confbot.space':
case 'org.matrix.confbot.interest_room':
case 'io.element.widgets.layout':
case 'org.matrix.msc3381.poll.response':
case 'org.matrix.msc3381.poll.start':
break;
default:
this.doLog(`${targetChannel.name}:`);
this.doLog(nextEvent);
break;
}
}
handleMatrixPresence(event: any) {
let matrixUser = this.matrixUsers.get(event["sender"]);
// Don't bother sending if the user isn't joined to any channels yet
if (!matrixUser) {
return;
}
if (matrixUser.mxid === this.ourMatrixUser.mxid) {
return;
}
const presence = event["content"]?.["presence"];
if (!presence) {
return;
}
const status = event["content"]?.["status_msg"];
if (status === undefined) {
return;
}
if (presence === 'online') {
if (this.client.enabledCaps.has('away-notify')) {
this.client.sendMessage(matrixUser.getMask(), 'AWAY', []);
}
} else {
if (this.client.enabledCaps.has('away-notify')) {
const awayMessage = (status === '') ? "user is away" : status;
this.client.sendMessage(matrixUser.getMask(), 'AWAY', [awayMessage]);
}
}
}
handleMatrixReaction(event: any, targetChannel: Channel) {
const sourceUser = this.getOrCreateMatrixUser(event["sender"]);
this.checkForLazyJoin(event, sourceUser, targetChannel);
const tags: Map<string, string> = new Map();
tags.set('msgid', event["event_id"]);
tags.set('account', sourceUser.accountName);
tags.set('time', new Date(event["origin_server_ts"]).toISOString())
const reactionData = event["content"]?.['m.relates_to'];
if (!reactionData)
return;
const targetMsgid = reactionData["event_id"];
const reaction = reactionData["key"];
tags.set('+draft/reply', targetMsgid);
tags.set('+draft/react', reaction);
if (this.eventIDToLabel.has(event["event_id"])) {
tags.set("label", this.eventIDToLabel.get(event["event_id"]) || "")
}
if (this.client.enabledCaps.has('message-tags')) {
this.client.sendMessage(sourceUser.getMask(), "TAGMSG", [targetChannel.name], tags)
}
}
updateRoomName(newNameEvent: any, targetChannel: Channel) {
let newName: string = newNameEvent["content"]["alias"];
if (newName === targetChannel.name)
return;
if (!newName || newName === "")
newName = targetChannel.roomId;
const oldName = targetChannel.name;
this.channels.delete(oldName);
targetChannel.name = newName;
this.channels.set(newName, targetChannel);
if (this.client.enabledCaps.has("draft/channel-rename")) {
this.client.sendMessage(this.name, "RENAME", [oldName, targetChannel.name, "New channel name set"], new Map());
}
else {
this.client.sendMessage(this.getMask(), "PART", [oldName, `Renaming channel to ${newName}`], new Map());
this.joinNewIRCClient(this.client, targetChannel);
}
}
handleEncryptedMessage(event: any, targetChannel: Channel) {
const sourceUser = this.getOrCreateMatrixUser(event["sender"]);
this.checkForLazyJoin(event, sourceUser, targetChannel);
const messageTags = new Map();
messageTags.set('msgid', event["event_id"]);
messageTags.set('time', new Date(event["origin_server_ts"]).toISOString());
messageTags.set('account', sourceUser.accountName);
this.client.sendMessage(sourceUser.getMask(), 'NOTICE', [targetChannel.name, "Sent an encrypted message, use another client to view"], messageTags);
}
handleMatrixGuestAccess(event: any, targetChannel: Channel) {
const rule = event["content"]?.["guest_access"];
if (!rule) {
this.doLog(`Warning: Guest access not found in ${event}`);
return;
}
targetChannel.guestAccess = rule;
}
handleMatrixHistoryVisibility(event: any, targetChannel: Channel) {
const rule = event["content"]?.["history_visibility"];
if (!rule) {
this.doLog(`Warning: history visibility not found in ${event}`);
return;
}
targetChannel.historyVisibility = rule;
}
handleMatrixJoinRule(event: any, targetChannel: Channel) {
const rule = event["content"]?.["join_rule"];
if (!rule) {
this.doLog(`Warning: join rule not found in ${event}`);
return;
}
targetChannel.joinRules = rule;
}
handleMatrixMember(event: any, targetChannel: Channel) {
const targetUser = this.getOrCreateMatrixUser(event["state_key"]);
const sourceUser = this.getOrCreateMatrixUser(event["sender"]);
const content = event["content"];
if (!content)
return;
const membershipStatus = content["membership"];
const messageTags = new Map();
messageTags.set('time', new Date(event["origin_server_ts"]).toISOString());
messageTags.set('account', sourceUser.accountName);
if (membershipStatus === "invite") {
const reason = content["reason"];
if (this.client.enabledCaps.has('invite-notify')) {
if (this.client.enabledCaps.has('reflectionircd.chat/extended-invite')) {
this.client.sendMessage(sourceUser.getMask(), 'INVITE', [targetUser.nick, targetChannel.name, reason], messageTags)
} else {
this.client.sendMessage(sourceUser.getMask(), 'INVITE', [targetUser.nick, targetChannel.name], messageTags)
}
}
}
else if (membershipStatus === "join") {
this.checkForLazyJoin(event, sourceUser, targetChannel);
}
else if (membershipStatus === "leave") {
if (!targetChannel.matrixUsers.has(targetUser.nick))
return;
if (targetUser.mxid === sourceUser.mxid) {
const reason = content["reason"] || 'User left';
this.client.sendMessage(sourceUser.getMask(), 'PART', [targetChannel.name, reason], messageTags);
}
else {
const reason = content["reason"] || 'User was kicked';
this.client.sendMessage(sourceUser.getMask(), 'KICK', [targetChannel.name, targetUser.nick, reason], messageTags);
}
targetChannel.matrixUsers.delete(targetUser.nick)
}
else if (membershipStatus === "ban") {
if (!targetChannel.matrixUsers.has(targetUser.nick))
return;
const reason = content["reason"] || 'User was banned';
this.client.sendMessage(sourceUser.getMask(), 'KICK', [targetChannel.name, targetUser.nick, reason], messageTags);
targetChannel.matrixUsers.delete(targetUser.nick)
}
else {
this.doLog(`Got unknown m.room.member event: ${event}`);
}
}
handleMatrixMessage(event: any, targetChannel: Channel) {
const sourceUser = this.getOrCreateMatrixUser(event["sender"]);
this.checkForLazyJoin(event, sourceUser, targetChannel);
const content = event["content"];
const msgtype = content["msgtype"];
let messageContent = content["body"];
const tags: Map<string, string> = new Map();
tags.set('msgid', event["event_id"]);
tags.set('account', sourceUser.accountName);
tags.set('time', new Date(event["origin_server_ts"]).toISOString())
if (this.eventIDToLabel.has(event["event_id"])) {
tags.set("label", this.eventIDToLabel.get(event["event_id"]) || "")
}
const maybeReply = content["m.relates_to"]?.["m.in_reply_to"]?.["event_id"];
if (maybeReply) {
tags.set('+draft/reply', maybeReply);
}
const ircCommand = (msgtype === 'm.notice') ? 'NOTICE' : 'PRIVMSG';
if (msgtype === 'm.emote') {
messageContent = `\x01ACTION ${messageContent}\x01`;
}
else if (['m.image', 'm.file', 'm.audio', 'm.video'].includes(msgtype)) {
let uri: string = content["url"];
if (!uri)
return;
const mxcregex = uri.match(/mxc:\/\/(?<servername>[^\/]+)\/(?<mediaid>.+)/)
if (!mxcregex || !mxcregex.groups)
this.doLog(`Failed to parse MXC URI: ${uri}`);
else
uri = `${this.homeserver}/_matrix/media/v3/download/${mxcregex.groups.servername}/${mxcregex.groups.mediaid}`;
messageContent = `\x01ACTION shared ${messageContent}: ${uri}\x01`;
}
const msgArray = (messageContent.indexOf('\n') !== -1) ? messageContent.split('\n'): [messageContent];
if (msgArray.length === 1) {
const msg = msgArray[0];
if (!msg) {
return;
}
if (this.eventIdStore.get(event["event_id"]) === this.client) {
if (this.client.enabledCaps.has('echo-message')) {
this.client.sendMessage(sourceUser.getMask(), ircCommand, [targetChannel.name, msg], tags)
}
} else {
this.client.sendMessage(sourceUser.getMask(), ircCommand, [targetChannel.name, msg], tags)
}
} else {
if (this.eventIdStore.get(event["event_id"]) === this.client && !this.client.enabledCaps.has('echo-message')) {
return;
}
if (this.client.enabledCaps.has('draft/multiline')) {
const batchLabel = Math.random().toString(36).substring(2,7);
const batchTags = new Map();
batchTags.set('batch', batchLabel);
this.client.sendMessage(sourceUser.getMask(), 'BATCH', [`+${batchLabel}`, 'draft/multiline', targetChannel.name], tags);
for (const msg of msgArray) {
if (msg) {
this.client.sendMessage(sourceUser.getMask(), ircCommand, [targetChannel.name, msg], batchTags)
}
}
this.client.sendMessage(sourceUser.getMask(), 'BATCH', [`-${batchLabel}`], new Map());
} else {
for (const msg of msgArray) {
if (msg) {
this.client.sendMessage(sourceUser.getMask(), ircCommand, [targetChannel.name, msg], tags)
}
}
}
}
}
convertPLToMode(pl: number, direction: string) {
let modeString: string = "";
if (pl > 99) modeString = `${direction}o`
else if (pl > 49) modeString = `${direction}h`
else if (pl > 0) modeString = `${direction}v`
return modeString;
}
handleMatrixPL(event: any, targetChannel: Channel) {
const allUsers = event["content"]["users"];
const sourceUser = this.getOrCreateMatrixUser(event["sender"]);
if (!allUsers) return;
for (const [mxid, pl] of Object.entries(allUsers)) {
const thisMatrixUser = this.getOrCreateMatrixUser(mxid);
targetChannel.matrixUsers.set(thisMatrixUser.nick, thisMatrixUser);
const oldPl = targetChannel.powerLevels.get(thisMatrixUser.nick);
const newPl = Number(pl);
if (oldPl === undefined) {
targetChannel.powerLevels.set(thisMatrixUser.nick, newPl);
const modeChange = this.convertPLToMode(newPl, "+");
this.client.sendMessage(sourceUser.getMask(), "MODE", [targetChannel.name, modeChange, thisMatrixUser.nick]);
}
else if (oldPl !== newPl) {
const oldModeChange = this.convertPLToMode(oldPl, "-");
this.client.sendMessage(sourceUser.getMask(), "MODE", [targetChannel.name, oldModeChange, thisMatrixUser.nick]);
if (newPl !== 0) {
const newModeChange = this.convertPLToMode(newPl, "+");
this.client.sendMessage(sourceUser.getMask(), "MODE", [targetChannel.name, newModeChange, thisMatrixUser.nick]);
} else {
targetChannel.powerLevels.delete(thisMatrixUser.nick)
}
}
}
if (targetChannel.powerLevels.size !== Object.keys(allUsers).length) {
for (const pl of targetChannel.powerLevels.keys()) {
const nextUser = this.nickToMatrixUser.get(pl);
if (!nextUser) return;
if (!(nextUser.mxid in allUsers)) {
const oldPl = targetChannel.powerLevels.get(pl);
if (!oldPl) return;
const oldMode = this.convertPLToMode(oldPl, "-");
this.client.sendMessage(sourceUser.getMask(), "MODE", [targetChannel.name, oldMode, nextUser.nick]);
}
}
}
}
handleMatrixRedaction(event: any, targetChannel: Channel) {
const sourceUser = this.getOrCreateMatrixUser(event["sender"]);
this.checkForLazyJoin(event, sourceUser, targetChannel);
const reason = event["content"]?.["reason"] || "";
const tags: Map<string, string> = new Map();
tags.set('reflectionircd.chat/delete-message', event["redacts"]);
tags.set('account', sourceUser.accountName);
tags.set('time', new Date(event["origin_server_ts"]).toISOString())
if (this.client.enabledCaps.has("reflectionircd.chat/edit-message")) {
this.client.sendMessage(sourceUser.getMask(), 'DELETEMSG', [targetChannel.name, reason], tags);
}
}
handleMatrixTombstone(event: any, targetChannel: Channel) {
const newRoom = event["content"]?.["replacement_room"];
if (!newRoom)
return
const message = `This room has been replaced by ${newRoom}, please join that room using another client`
this.client.sendMessage(this.name, 'NOTICE', [targetChannel.name, message], new Map());
}
handleMatrixTopic(event: any, targetChannel: Channel) {
const topicText = event["content"]?.["topic"];
if (!topicText)
return;
const topicSetter = this.getOrCreateMatrixUser(event["sender"]);
const topicTS: string = event["origin_server_ts"].toString();
targetChannel.topic.set("text", topicText.replace(/\r\n|\r|\n/, " "));
targetChannel.topic.set("timestamp", topicTS.substring(0,10))
targetChannel.topic.set('setter', topicSetter.nick);
const messageTags = new Map();
messageTags.set('msgid', event["event_id"]);
messageTags.set('account', topicSetter.accountName);
messageTags.set('time', new Date(event["origin_server_ts"]).toISOString());
if (this.eventIDToLabel.has(event["event_id"])) {
messageTags.set("label", this.eventIDToLabel.get(event["event_id"]) || "")
}
this.client.sendMessage(topicSetter.getMask(), 'TOPIC', [targetChannel.name, topicText], messageTags);
}
handleMatrixSticker(event: any, targetChannel: Channel) {
const sourceUser = this.getOrCreateMatrixUser(event["sender"]);
this.checkForLazyJoin(event, sourceUser, targetChannel);
const altText = event["content"]?.["body"];
const imgMxc = event["content"]?.["url"];
if (!altText || !imgMxc) {
return;
}
const mxcregex = imgMxc.match(/mxc:\/\/(?<servername>[^\/]+)\/(?<mediaid>.+)/)
let uri = imgMxc;
if (!mxcregex || !mxcregex.groups) {
this.doLog(`Failed to parse MXC URI: ${imgMxc}`);
} else {
uri = `${this.homeserver}/_matrix/media/v3/download/${mxcregex.groups.servername}/${mxcregex.groups.mediaid}`;
}
const messageContent = `\x01ACTION sent a sticker: ${altText}: ${uri}\x01`;
this.client.sendMessage(sourceUser.getMask(), 'PRIVMSG', [targetChannel.name, messageContent]);
}
handleMatrixTyping(event: any, targetChannel: Channel) {
const typingUsers = event["content"]?.["user_ids"];
if (!typingUsers) {
return;
}
typingUsers.forEach((mxid: string) => {
let matrixUser = this.matrixUsers.get(mxid);
// Don't bother sending if the user isn't joined to the channel yet
if (matrixUser !== undefined) {
const typingTags = new Map();
typingTags.set('+typing', 'active');
if (this.client.enabledCaps.has('message-tags')) {
this.client.sendMessage(matrixUser.getMask(), 'TAGMSG', [targetChannel.name], typingTags);
}
}
})
}
}