import axios from "axios"; import { randomUUID } from "crypto"; import { Channel } from "./Channel.js"; import { Client } from "./Client.js"; import { MatrixUser } from "./MatrixUser.js"; import { IRCMessage } from "./Message.js"; import { Server } from "./Server.js"; export class IRCUser { private clients: Set public channels: Set public nick: string private ident: string private hostname: string public accountName: string public realname: string private txnIdStore: Map public nextBatch: string private initialSync: boolean private isSyncing: boolean private currentSyncTime: number private syncIntervalID: NodeJS.Timeout; constructor(public mxid: string, private accessToken: string, public homeserver: string, private server: Server) { this.clients = new Set(); this.channels = new Set(); const mxidSplit = mxid.split(':') this.nick = mxidSplit[0].substr(1); this.ident = this.nick; this.hostname = mxidSplit[1]; this.accountName = mxid.slice(1); this.realname = this.accountName; this.txnIdStore = new Map(); this.nextBatch = ""; this.initialSync = false; this.isSyncing = false; this.currentSyncTime = 0; this.syncIntervalID = setInterval(this.doSync.bind(this), 2000); } isSynced() { return this.nextBatch !== ""; } getVerification() { return axios.get(`https://${this.homeserver}/_matrix/client/v3/account/whoami?access_token=${this.accessToken}`, { validateStatus: function (status) { return status < 500; } }); } getMask(): string { return `${this.nick}!${this.ident}@${this.hostname}`; } getClients(): Set { return this.clients; } addClient(client: Client, passedTags: Map) { this.clients.add(client); if (this.nextBatch !== "") { for (const channel of this.channels.values()) { channel.joinNewIRCClient(client, passedTags); } } } doSync(): void { if (!this.isSynced()) { console.log("not syncing, initial sync not completed"); return; } if (this.isSyncing) { if ((Date.now() - this.currentSyncTime) > 15000) console.log(`Sync is lagging, current sync has been running for ${Date.now() - this.currentSyncTime} milliseconds`); return; } this.currentSyncTime = Date.now(); this.isSyncing = true; const endpoint = `https://${this.homeserver}/_matrix/client/v3/sync?access_token=${this.accessToken}&since=${this.nextBatch}&timeout=15000`; axios.get(endpoint).then(response => { const data = response.data; this.nextBatch = data.next_batch; const rooms = data.rooms; if (rooms && rooms['join']) { for (const roomId of Object.keys(rooms.join)) { const targetChannel = this.server.getOrCreateIRCChannel(roomId); rooms.join[roomId].timeline.events.forEach((nextEvent: any) => { targetChannel.routeMatrixEvent(nextEvent) }); } } this.isSyncing = false; }).catch((error) => { console.log(error); if (error.response) { console.log(`Response error: ${error.response.status}`); } else if (error.request) { console.log(error.request); console.log(error.config); } else { console.log('Error', error.message); console.log(error.config); } this.isSyncing = false; }); } inviteMatrixUser(client: Client, channel: Channel, target: MatrixUser, reason: string, passedTags: Map = new Map()) { const data = { "reason": reason, "user_id": target.mxid } axios.post(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/invite?access_token=${this.accessToken}`, data).catch(function (error) { if (error.response) { console.log(error.response.data); } else if (error.request) { console.log(error.request); } else { console.log('Error', error.message); console.log(error.config); } }) } kickMatrixUser(client: Client, channel: Channel, target: string, reason: string, passedTags: Map = new Map()) { const data = { "reason": reason, "user_id": target } axios.post(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/kick?access_token=${this.accessToken}`, data).catch(function (error) { if (error.response) { console.log(error.response.data); } else if (error.request) { console.log(error.request); } else { console.log('Error', error.message); console.log(error.config); } }) } partMatrixRoom(client: Client, channel: Channel, reason: string, passedTags: Map = new Map()) { axios.post(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/leave?access_token=${this.accessToken}`, {"reason": reason}).then(response => { if (response.status === 200) { this.clients.forEach(c => { c.sendMessage(this.getMask(), "PART", [channel.name, reason], passedTags); }) this.channels.delete(channel); } else { client.sendMessage(this.server.name, "NOTICE", [this.nick, JSON.stringify(response.data)], passedTags); } }).catch(function (error) { if (error.response) { console.log(error.response.data); } else if (error.request) { console.log(error.request); } else { console.log('Error', error.message); console.log(error.config); } }) } changeRoomTopic(client: Client, channel: Channel, topic: string, passedTags: Map = new Map()) { axios.put(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/state/m.room.topic?access_token=${this.accessToken}`, {"topic": topic}).catch(function (error) { if (error.response) { console.log(error.response.data); } else if (error.request) { console.log(error.request); } else { console.log('Error', error.message); console.log(error.config); } }) } sendMessageToMatrix(message: IRCMessage, client: Client) { const channel = this.server.ircChannels.get(message.params[0]); if (!channel) { return; } let msgtype = 'm.text'; let msgbody = message.params[1]; if (message.command === 'NOTICE') msgtype = 'm.notice'; else if (message.params[1].startsWith('\x01')) { msgtype = 'm.emote'; msgbody = msgbody.replace(/\x01(ACTION\s)?/gi, ''); } const roomId = channel.roomId; const content = { "body": msgbody, "msgtype": msgtype, "m.relates_to": {} } if (message.tags.has("+draft/reply")) { content["m.relates_to"] = { "m.in_reply_to": { "event_id": message.tags.get("+draft/reply") } } } const newTxnid = randomUUID(); this.txnIdStore.set(newTxnid, client); axios.put(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/send/m.room.message/${newTxnid}?access_token=${this.accessToken}`, content).catch(function (error) { if (error.response) { console.log(error.response.data); } else if (error.request) { console.log(error.request); } else { console.log('Error', error.message); console.log(error.config); } }); } sendTagToMatrix(message: IRCMessage, client: Client) { const channel = this.server.ircChannels.get(message.params[0]); if (!channel) { return; } console.log(message.tags) if (message.tags.has("+draft/react") && message.tags.has("+draft/reply")) { const content = { "m.relates_to": { "event_id": message.tags.get("+draft/reply"), "key": message.tags.get("+draft/react"), "rel_type": "m.annotation" } } const newTxnid = randomUUID(); this.txnIdStore.set(newTxnid, client); axios.put(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/send/m.reaction/${newTxnid}?access_token=${this.accessToken}`, content).catch(function (error) { if (error.response) { console.log(error.response.data); } else if (error.request) { console.log(error.request); } else { console.log('Error', error.message); console.log(error.config); } }); } } sendToAll(prefix: string, command: string, params: string[], tags: Map = new Map(), skipClient: Client|null = null) { this.clients.forEach(client => { if (client !== skipClient) { client.sendMessage(prefix, command, params, tags); } }) } sendToAllWithCap(cap: string, prefix: string, command: string, params: string[], tags: Map = new Map(), skipClient: Client|null = null) { this.clients.forEach(client => { if (client !== skipClient && client.enabledCaps.has(cap)) { client.sendMessage(prefix, command, params, tags); } }) } sendToAllWithoutCap(cap: string, prefix: string, command: string, params: string[], tags: Map = new Map(), skipClient: Client|null = null) { this.clients.forEach(client => { if (client !== skipClient && !client.enabledCaps.has(cap)) { client.sendMessage(prefix, command, params, tags); } }) } }