import { Server } from "./Server.js"; import { MatrixUser } from "./MatrixUser.js"; import { IRCUser } from "./IRCUser.js"; import { IRCMessage } from "./Message.js"; import { Client } from "./Client.js"; import numerics from "./numerics.js"; import axios from "axios"; export class Channel { public name: string private matrixUsers: Map private ircUsers: Map private powerLevels: Map private topic: Map; private modes: Map private eventIDsSeen: Set; constructor(public roomId: string, private server: Server) { this.name = roomId; this.matrixUsers = new Map(); this.ircUsers = new Map(); this.powerLevels = new Map(); this.topic = new Map([['text', ''], ['timestamp', '0'], ['setter', 'matrix']]); this.modes = new Map(); this.modes.set('n', ''); this.eventIDsSeen = new Set(); } getNickPowerLevelMapping(nick: string): string { let opStatus = ''; const pl = this.powerLevels.get(nick) || 0; if (pl > 99) { opStatus = '@'; } else if (pl > 49) { opStatus = '%'; } else if (pl > 0) { opStatus = '+'; } return opStatus; } joinNewIRCClient(client: Client, passedTags: Map) { if (!client.user) return; this.ircUsers.set(client.user.nick, client.user); if (client.enabledCaps.has('extended-join')) { client.sendMessage(client.user.getMask(), "JOIN", [this.name, client.user.accountName, client.user.mxid], new Map([['account', client.user.realname]])); } else { client.sendMessage(client.user.getMask(), "JOIN", [this.name], new Map([['account', client.user.realname]])); } this.sendNames(client, passedTags); this.sendTopic(client, passedTags); } sendNames(client: Client, passedTags: Map) { if (!client.user) return; let namesList: string[] = []; for (const matrixUser of this.matrixUsers.values()) { const opStatus = this.getNickPowerLevelMapping(matrixUser.nick); namesList.push(`${opStatus}${matrixUser.nick}`); } let singleNamesList: string[] = [] namesList.forEach((singleName, index) => { if (index === 0 || index % 20 !== 0) { singleNamesList.push(singleName); } else { if (!client.user) return; client.sendMessage(client.server.name, "353", numerics["353"](client.user.nick, "=", this.name, singleNamesList), passedTags); singleNamesList = []; } }) if (singleNamesList.length !== 0) { client.sendMessage(client.server.name, "353", numerics["353"](client.user.nick, "=", this.name, singleNamesList), passedTags); } client.sendMessage(client.server.name, "366", numerics["366"](client.user.nick, this.name), passedTags); } sendMode(client: Client, passedTags: Map) { if (!client.user) return; const chanModes = []; for (let m of this.modes.keys()) { chanModes.push(m); } client.sendMessage(client.server.name, "324", numerics["324"](client.user.nick, this.name, `+${chanModes.join("")}`), passedTags); } sendTopic(client: Client, passedTags: Map) { if (!client.user) return; const topicText = this.topic.get('text') || ''; const topicSetter = this.topic.get('setter') || 'matrix'; const topicTimestamp = this.topic.get('timestamp') || '0'; if (topicText === '') { client.sendMessage(client.server.name, '331', [client.user.nick, this.name, 'No topic is set'], passedTags); return; } client.sendMessage(client.server.name, '332', [client.user.nick, this.name, topicText], passedTags); client.sendMessage(client.server.name, '333', [client.user.nick, this.name, topicSetter, topicTimestamp], passedTags); } sendWho(client: Client, passedTags: Map) { if (!client.user) return; for (const matrixUser of this.matrixUsers.values()) { const opStatus = this.getNickPowerLevelMapping(matrixUser.nick); const userParams = [ client.user.nick, this.name, matrixUser.ident, matrixUser.hostname, client.server.name, matrixUser.nick, `H${opStatus}`, `0 ${matrixUser.realname}` ] client.sendMessage(client.server.name, '352', userParams, passedTags); } } routeMatrixEvent(event: any) { if (!event["type"] || !event["event_id"] || !event["origin_server_ts"]) return; switch (event["type"]) { case 'm.room.member': this.handleMatrixMember(event); break; case 'm.room.message': { if (this.eventIDsSeen.has(event["event_id"])) { console.log(`duplicate event_id: ${event["event_id"]}`); return; } this.eventIDsSeen.add(event["event_id"]); this.handleMatrixMessage(event); break; } case 'm.room.power_levels': this.handleMatrixPL(event); break; case 'm.room.topic': this.handleMatrixTopic(event); break; default: console.log(event["type"]); break; } } joinMatrixUser(matrixUser: MatrixUser, event: any) { this.matrixUsers.set(matrixUser.nick, matrixUser); const prefix = matrixUser.getMask(); if (event) { const tags = new Map([["account", matrixUser.accountName], ['time', new Date(event["origin_server_ts"]).toISOString()]]) for (const user of this.ircUsers.values()) { user.sendToAllWithCap('extended-join', prefix, "JOIN", [this.name, matrixUser.accountName, matrixUser.realname], tags); user.sendToAllWithoutCap('extended-join', prefix, "JOIN", [this.name], tags); } } } handleMatrixMember(event: any) { const thisMatrixUser = this.server.getOrCreateMatrixUser(event["sender"]); // During initial sync, all past/present members are returned, so we filter out non-joined members if (event["content"]["membership"] !== "join" && !this.matrixUsers.has(thisMatrixUser.nick)) return; this.matrixUsers.set(thisMatrixUser.nick, thisMatrixUser); } handleMatrixMessage(event: any) { const thisMatrixUser = this.server.getOrCreateMatrixUser(event["sender"]); if (!this.matrixUsers.has(thisMatrixUser.nick)) { this.joinMatrixUser(thisMatrixUser, event); } const content = event["content"]; const msgtype = content["msgtype"]; let messageContent = content["body"]; const tags: Map = new Map(); tags.set('msgid', event["event_id"]); tags.set('account', thisMatrixUser.accountName); tags.set('time', new Date(event["origin_server_ts"]).toISOString()) 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:\/\/(?[^\/]+)\/(?.+)/) if (!mxcregex || !mxcregex.groups) console.log(`Failed to parse MXC URI: ${uri}`); else uri = `https://matrix.org/_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]; msgArray.forEach((msg: string) => { if (msg) { this.ircUsers.forEach((user) => { user.sendToAll(thisMatrixUser.getMask(), ircCommand, [this.name, msg], tags) }); } }); } handleMatrixPL(event: any) { const allUsers = event["content"]["users"]; for (const [mxid, pl] of Object.entries(allUsers)) { const thisMatrixUser = this.server.getOrCreateMatrixUser(event["sender"]); this.matrixUsers.set(thisMatrixUser.nick, thisMatrixUser); this.powerLevels.set(thisMatrixUser.nick, Number(pl)); } } handleMatrixTopic(event: any) { this.topic.set("text", event["content"]["topic"]); this.topic.set("timestamp", event["origin_server_ts"].toString()) } handleMatrixJoinRule(event: any) { const rule = event["content"]?.["join_rule"]; if (!rule) { console.log(`Warning: join rule not found in ${event}`); return; } if (rule === "public") { if (this.modes.has('i')) { this.modes.delete('i'); this.ircUsers.forEach((user) => { user.sendToAll(this.server.name, 'MODE', [this.name, '-i'], new Map()); }); } } else { if (!this.modes.has('i')) { this.modes.set('i', ''); this.ircUsers.forEach((user) => { user.sendToAll(this.server.name, 'MODE', [this.name, '+i'], new Map()); }); } } } }