reflectionircd/src/Channel.ts
2021-12-07 11:12:14 -05:00

251 lines
No EOL
10 KiB
TypeScript

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<string, MatrixUser>
private ircUsers: Map<string, IRCUser>
private powerLevels: Map<string, number>
private topic: Map<string, string>;
private modes: Map<string, string>
private eventIDsSeen: Set<string>;
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<string, string>) {
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<string, string>) {
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<string, string>) {
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<string, string>) {
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<string, string>) {
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<string, string> = 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:\/\/(?<servername>[^\/]+)\/(?<mediaid>.+)/)
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());
});
}
}
}
}