mirror of
https://git.sr.ht/~emerson/reflectionircd
synced 2025-04-13 09:59:52 +00:00
669 lines
No EOL
29 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
})
|
|
}
|
|
} |