mirror of
https://git.sr.ht/~emerson/reflectionircd
synced 2025-04-13 09:59:52 +00:00
393 lines
No EOL
16 KiB
TypeScript
393 lines
No EOL
16 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
|
|
public matrixUsers: Map<string, MatrixUser>
|
|
public ircUsers: Map<string, IRCUser>
|
|
private powerLevels: Map<string, number>
|
|
private topic: Map<string, string>;
|
|
private eventIDsSeen: Set<string>;
|
|
private historyVisibility: string
|
|
private guestAccess: string
|
|
private joinRules: 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.eventIDsSeen = new Set();
|
|
this.historyVisibility = "";
|
|
this.guestAccess = "";
|
|
this.joinRules = "";
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
updateRoomName(newNameEvent: any) {
|
|
const newName: string = newNameEvent["content"]["alias"];
|
|
if (!newName || newName === this.name)
|
|
return;
|
|
const oldName = this.name;
|
|
this.name = newName;
|
|
this.ircUsers.forEach((user, username) => {
|
|
user.getClients().forEach(client => {
|
|
if (client.enabledCaps.has("draft/channel-rename")) {
|
|
client.sendMessage(this.server.name, "RENAME", [oldName, this.name, "New channel name set"], new Map());
|
|
}
|
|
else {
|
|
client.sendMessage(this.server.name, "PART", [oldName, "Renaming channel"], new Map());
|
|
this.joinNewIRCClient(client, new Map());
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
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;
|
|
client.sendMessage(client.server.name, "324", numerics["324"](client.user.nick, this.name, `+n`), 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.reaction':
|
|
this.handleMatrixReaction(event);
|
|
break;
|
|
case 'm.room.canonical_alias':
|
|
this.updateRoomName(event);
|
|
break;
|
|
case 'm.room.guest_access':
|
|
this.handleMatrixGuestAccess(event);
|
|
break;
|
|
case 'm.room.history_visibility':
|
|
this.handleMatrixHistoryVisibility(event);
|
|
break;
|
|
case 'm.room.join_rules':
|
|
this.handleMatrixJoinRule(event);
|
|
break;
|
|
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;
|
|
// Add some events we aren't going to use now (or ever)
|
|
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.tombstone':
|
|
case 'm.room.server_acl':
|
|
case 'org.matrix.room.preview_urls':
|
|
break;
|
|
default:
|
|
console.log(event);
|
|
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 targetUser = this.server.getOrCreateMatrixUser(event["state_key"]);
|
|
const sourceUser = this.server.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"];
|
|
this.ircUsers.forEach((user) => {
|
|
user.getClients().forEach(c => {
|
|
if (c.enabledCaps.has('invite-notify')) {
|
|
if (c.enabledCaps.has('draft/extended-invite')) {
|
|
c.sendMessage(sourceUser.getMask(), 'INVITE', [targetUser.nick, this.name, reason], messageTags)
|
|
} else {
|
|
c.sendMessage(sourceUser.getMask(), 'INVITE', [targetUser.nick, this.name], messageTags)
|
|
}
|
|
}
|
|
})
|
|
});
|
|
}
|
|
else if (membershipStatus === "join") {
|
|
if (!this.matrixUsers.has(targetUser.nick))
|
|
this.joinMatrixUser(targetUser, event);
|
|
}
|
|
else if (membershipStatus === "leave") {
|
|
if (!this.matrixUsers.has(targetUser.nick))
|
|
return;
|
|
if (targetUser.mxid === sourceUser.mxid) {
|
|
const reason = content["reason"] || 'User left';
|
|
this.ircUsers.forEach((user) => {
|
|
user.sendToAll(sourceUser.getMask(), 'PART', [this.name, reason], messageTags);
|
|
});
|
|
}
|
|
else {
|
|
const reason = content["reason"] || 'User was kicked';
|
|
this.ircUsers.forEach((user) => {
|
|
user.sendToAll(sourceUser.getMask(), 'KICK', [this.name, targetUser.nick, reason], messageTags);
|
|
});
|
|
}
|
|
this.matrixUsers.delete(targetUser.nick)
|
|
}
|
|
else if (membershipStatus === "ban") {
|
|
if (!this.matrixUsers.has(targetUser.nick))
|
|
return;
|
|
const reason = content["reason"] || 'User was banned';
|
|
this.ircUsers.forEach((user) => {
|
|
user.sendToAll(sourceUser.getMask(), 'KICK', [this.name, targetUser.nick, reason], messageTags);
|
|
});
|
|
this.matrixUsers.delete(targetUser.nick)
|
|
}
|
|
else {
|
|
console.log(`Got unknown m.room.member event: ${event}`);
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
|
|
handleMatrixReaction(event: any) {
|
|
const thisMatrixUser = this.server.getOrCreateMatrixUser(event["sender"]);
|
|
if (!this.matrixUsers.has(thisMatrixUser.nick)) {
|
|
this.joinMatrixUser(thisMatrixUser, event);
|
|
}
|
|
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 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);
|
|
this.ircUsers.forEach((user) => {
|
|
user.sendToAllWithCap("message-tags", thisMatrixUser.getMask(), "TAGMSG", [this.name], tags)
|
|
});
|
|
}
|
|
|
|
handleMatrixTopic(event: any) {
|
|
const topicText = event["content"]?.["topic"];
|
|
if (!topicText)
|
|
return;
|
|
const topicSetter = this.server.getOrCreateMatrixUser(event["sender"]);
|
|
const topicTS: string = event["origin_server_ts"].toString();
|
|
this.topic.set("text", topicText);
|
|
this.topic.set("timestamp", topicTS.substring(0,10))
|
|
this.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())
|
|
this.ircUsers.forEach((user) => {
|
|
user.sendToAll(topicSetter.getMask(), 'TOPIC', [this.name, topicText], messageTags);
|
|
});
|
|
}
|
|
|
|
handleMatrixJoinRule(event: any) {
|
|
const rule = event["content"]?.["join_rule"];
|
|
if (!rule) {
|
|
console.log(`Warning: join rule not found in ${event}`);
|
|
return;
|
|
}
|
|
this.joinRules = rule;
|
|
}
|
|
|
|
handleMatrixHistoryVisibility(event: any) {
|
|
const rule = event["content"]?.["history_visibility"];
|
|
if (!rule) {
|
|
console.log(`Warning: history visibility not found in ${event}`);
|
|
return;
|
|
}
|
|
this.historyVisibility = rule;
|
|
}
|
|
|
|
handleMatrixGuestAccess(event: any) {
|
|
const rule = event["content"]?.["guest_access"];
|
|
if (!rule) {
|
|
console.log(`Warning: Guest access not found in ${event}`);
|
|
return;
|
|
}
|
|
this.guestAccess = rule;
|
|
}
|
|
} |