mirror of
https://git.sr.ht/~emerson/reflectionircd
synced 2025-08-05 16:59:10 +00:00
big changes, isolate channels per-user and IRC commands per-client
This commit is contained in:
parent
5a5991db50
commit
614fe8eb1e
4 changed files with 667 additions and 726 deletions
377
src/Channel.ts
377
src/Channel.ts
|
@ -1,24 +1,18 @@
|
||||||
import { Server } from "./Server.js";
|
|
||||||
import { MatrixUser } from "./MatrixUser.js";
|
import { MatrixUser } from "./MatrixUser.js";
|
||||||
import { IRCUser } from "./IRCUser.js";
|
|
||||||
import { Client } from "./Client.js";
|
|
||||||
import numerics from "./numerics.js";
|
|
||||||
import { IRCMessage } from "./Message.js";
|
|
||||||
|
|
||||||
export class Channel {
|
export class Channel {
|
||||||
public name: string
|
public name: string
|
||||||
public matrixUsers: Map<string, MatrixUser>
|
public matrixUsers: Map<string, MatrixUser>
|
||||||
public ircUsers: Map<string, IRCUser>
|
public powerLevels: Map<string, number>
|
||||||
private powerLevels: Map<string, number>
|
public topic: Map<string, string>;
|
||||||
private topic: Map<string, string>;
|
public eventIDsSeen: Set<string>;
|
||||||
private eventIDsSeen: Set<string>;
|
public historyVisibility: string
|
||||||
private historyVisibility: string
|
public guestAccess: string
|
||||||
private guestAccess: string
|
public joinRules: string
|
||||||
private joinRules: string
|
constructor(public roomId: string, initialMatrixUser: MatrixUser) {
|
||||||
constructor(public roomId: string, private server: Server) {
|
|
||||||
this.name = roomId;
|
this.name = roomId;
|
||||||
this.matrixUsers = new Map();
|
this.matrixUsers = new Map();
|
||||||
this.ircUsers = new Map();
|
this.matrixUsers.set(initialMatrixUser.nick, initialMatrixUser);
|
||||||
this.powerLevels = new Map();
|
this.powerLevels = new Map();
|
||||||
this.topic = new Map([['text', ''], ['timestamp', '0'], ['setter', 'matrix']]);
|
this.topic = new Map([['text', ''], ['timestamp', '0'], ['setter', 'matrix']]);
|
||||||
this.eventIDsSeen = new Set();
|
this.eventIDsSeen = new Set();
|
||||||
|
@ -41,359 +35,4 @@ export class Channel {
|
||||||
}
|
}
|
||||||
return opStatus;
|
return opStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRoomName(newNameEvent: any) {
|
|
||||||
const newName: string = newNameEvent["content"]["alias"];
|
|
||||||
if (!newName || newName === this.name)
|
|
||||||
return;
|
|
||||||
const oldName = this.name;
|
|
||||||
this.server.ircChannels.delete(oldName);
|
|
||||||
this.name = newName;
|
|
||||||
this.server.ircChannels.set(newName, this);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
client.sendMessage(client.server.name, '315', [client.user.nick, this.name, "End of /WHO"], passedTags);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModeChange(client: Client, message: IRCMessage) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
537
src/Client.ts
537
src/Client.ts
|
@ -1,4 +1,5 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
import { Socket } from 'net';
|
import { Socket } from 'net';
|
||||||
import { Channel } from './Channel.js';
|
import { Channel } from './Channel.js';
|
||||||
import { IRCUser } from './IRCUser.js';
|
import { IRCUser } from './IRCUser.js';
|
||||||
|
@ -58,12 +59,7 @@ export class Client {
|
||||||
|
|
||||||
getMatrixUserFromNick(targetNick: string, passedTags: Map<string, string> = new Map()) {
|
getMatrixUserFromNick(targetNick: string, passedTags: Map<string, string> = new Map()) {
|
||||||
if (!this.user) return false;
|
if (!this.user) return false;
|
||||||
const targetMxid = this.server.nickToMxid.get(targetNick);
|
const target = this.user.nickToMatrixUser.get(targetNick);
|
||||||
if (!targetMxid) {
|
|
||||||
this.sendMessage(this.server.name, "401", [this.user.nick, targetNick, "No such nick"], passedTags);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const target = this.server.matrixUsers.get(targetMxid);
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
this.sendMessage(this.server.name, "401", [this.user.nick, targetNick, "No such nick"], passedTags);
|
this.sendMessage(this.server.name, "401", [this.user.nick, targetNick, "No such nick"], passedTags);
|
||||||
return false;
|
return false;
|
||||||
|
@ -73,7 +69,7 @@ export class Client {
|
||||||
|
|
||||||
getChannel(channel: string, passedTags: Map<string, string> = new Map()) {
|
getChannel(channel: string, passedTags: Map<string, string> = new Map()) {
|
||||||
if (!this.user) return false;
|
if (!this.user) return false;
|
||||||
const targetChannel = this.server.ircChannels.get(channel);
|
const targetChannel = this.user.channels.get(channel);
|
||||||
if (!targetChannel) {
|
if (!targetChannel) {
|
||||||
this.sendMessage(this.server.name, "403", [this.user.nick, channel, "No such channel"], passedTags);
|
this.sendMessage(this.server.name, "403", [this.user.nick, channel, "No such channel"], passedTags);
|
||||||
return false;
|
return false;
|
||||||
|
@ -83,7 +79,7 @@ export class Client {
|
||||||
|
|
||||||
checkIfInChannel(channel: Channel, passedTags: Map<string, string> = new Map()) {
|
checkIfInChannel(channel: Channel, passedTags: Map<string, string> = new Map()) {
|
||||||
if (!this.user) return false;
|
if (!this.user) return false;
|
||||||
if (!channel.ircUsers.has(this.user.nick)) {
|
if (!this.user.channels.get(channel.name)) {
|
||||||
this.sendMessage(this.server.name, "442", [this.user.nick, "You're not on that channel"], passedTags);
|
this.sendMessage(this.server.name, "442", [this.user.nick, "You're not on that channel"], passedTags);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -99,142 +95,6 @@ export class Client {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
routeMessage(data: string) {
|
|
||||||
const message = parseIRCMessage(data);
|
|
||||||
switch (message.command.toUpperCase()) {
|
|
||||||
case 'AUTHENTICATE': {
|
|
||||||
this.doAUTHENTICATE(message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'CAP': {
|
|
||||||
this.doCAP(message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'INVITE': {
|
|
||||||
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 2))
|
|
||||||
return;
|
|
||||||
const targetUser = this.getMatrixUserFromNick(message.params[0], message.tags);
|
|
||||||
const targetChannel = this.getChannel(message.params[1], message.tags);
|
|
||||||
if (!this.user || !targetUser || !targetChannel) return;
|
|
||||||
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
|
||||||
if (targetChannel.matrixUsers.has(targetUser.nick)) {
|
|
||||||
this.sendMessage(this.server.name, "443", [this.user.nick, targetUser.nick, "is already on channel"], message.tags);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reason = (message.params.length === 3) ? message.params[2] : "";
|
|
||||||
this.user.inviteMatrixUser(this, targetChannel, targetUser, reason, message.tags);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'KICK': {
|
|
||||||
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 2))
|
|
||||||
return;
|
|
||||||
const targetChannel = this.getChannel(message.params[0], message.tags);
|
|
||||||
const targetUser = this.getMatrixUserFromNick(message.params[1], message.tags);
|
|
||||||
if (!this.user || !targetUser || !targetChannel) return;
|
|
||||||
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
|
||||||
if (!targetChannel.matrixUsers.has(targetUser.nick)) {
|
|
||||||
this.sendMessage(this.server.name, "441", [this.user.nick, targetUser.nick, targetChannel.name, "They aren't on that channel"], message.tags);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reason = (message.params.length === 3) ? message.params[2] : "";
|
|
||||||
this.user.kickMatrixUser(this, targetChannel, targetUser.mxid, reason, message.tags);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'MODE': {
|
|
||||||
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1) || !this.user)
|
|
||||||
return;
|
|
||||||
const targetChannel = this.server.ircChannels.get(message.params[0]);
|
|
||||||
if (!targetChannel) {
|
|
||||||
if (message.params[0] !== this.user.nick) {
|
|
||||||
this.sendMessage(this.server.name, "502", [this.user.nick, "Can't view mode for other users"], message.tags);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.sendMessage(this.server.name, "221", [this.user.nick, "+i"], message.tags);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
|
||||||
if (message.params.length === 1)
|
|
||||||
targetChannel.sendMode(this, message.tags);
|
|
||||||
else
|
|
||||||
targetChannel.handleModeChange(this, message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'NAMES': {
|
|
||||||
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1))
|
|
||||||
return;
|
|
||||||
const targetChannel = this.getChannel(message.params[0], message.tags);
|
|
||||||
if (!this.user || !targetChannel) return;
|
|
||||||
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
|
||||||
targetChannel.sendNames(this, message.tags);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'NOTICE': {
|
|
||||||
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 2))
|
|
||||||
return;
|
|
||||||
const targetChannel = this.getChannel(message.params[0], message.tags);
|
|
||||||
if (!this.user || !targetChannel) return;
|
|
||||||
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
|
||||||
this.user.sendMessageToMatrix(message, this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'PART': {
|
|
||||||
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1))
|
|
||||||
return;
|
|
||||||
const targetChannel = this.getChannel(message.params[0], message.tags);
|
|
||||||
if (!this.user || !targetChannel) return;
|
|
||||||
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
|
||||||
const reason = (message.params.length === 2) ? message.params[1] : "";
|
|
||||||
this.user.partMatrixRoom(this, targetChannel, reason, message.tags);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'PING': {
|
|
||||||
this.sendMessage(this.server.name, "PONG", message.params, message.tags);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'PRIVMSG': {
|
|
||||||
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 2))
|
|
||||||
return;
|
|
||||||
const targetChannel = this.getChannel(message.params[0], message.tags);
|
|
||||||
if (!this.user || !targetChannel) return;
|
|
||||||
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
|
||||||
this.user.sendMessageToMatrix(message, this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'TAGMSG': {
|
|
||||||
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1))
|
|
||||||
return;
|
|
||||||
const targetChannel = this.getChannel(message.params[0], message.tags);
|
|
||||||
if (!this.user || !targetChannel) return;
|
|
||||||
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
|
||||||
this.user.sendTagToMatrix(message, this);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'TOPIC': {
|
|
||||||
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1))
|
|
||||||
return;
|
|
||||||
const targetChannel = this.getChannel(message.params[0], message.tags);
|
|
||||||
if (!this.user || !targetChannel) return;
|
|
||||||
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
|
||||||
if (message.params.length === 1) {
|
|
||||||
targetChannel.sendTopic(this, message.tags);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const topic = message.params[1];
|
|
||||||
this.user.changeRoomTopic(this, targetChannel, topic, message.tags);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'WHO': {
|
|
||||||
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1))
|
|
||||||
return;
|
|
||||||
const targetChannel = this.getChannel(message.params[0], message.tags);
|
|
||||||
if (!this.user || !targetChannel) return;
|
|
||||||
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
|
||||||
targetChannel.sendWho(this, message.tags);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getCapString(capVersion: string) {
|
getCapString(capVersion: string) {
|
||||||
let capArray = [];
|
let capArray = [];
|
||||||
for (const [key, value] of this.allCaps.entries()) {
|
for (const [key, value] of this.allCaps.entries()) {
|
||||||
|
@ -248,39 +108,48 @@ export class Client {
|
||||||
return capArray.join(' ');
|
return capArray.join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
doCAP(message: IRCMessage) {
|
routeMessage(data: string) {
|
||||||
switch (message.params[0]) {
|
const message = parseIRCMessage(data);
|
||||||
case 'LS': {
|
switch (message.command.toUpperCase()) {
|
||||||
if (message.params.length === 2) {
|
case 'AUTHENTICATE':
|
||||||
this.capVersion = message.params[1];
|
this.doAUTHENTICATE(message);
|
||||||
}
|
|
||||||
this.sendMessage(this.server.name, "CAP", ["*", "LS", this.getCapString(this.capVersion)], message.tags);
|
|
||||||
break;
|
break;
|
||||||
}
|
case 'CAP':
|
||||||
case 'LIST': {
|
this.doCAP(message);
|
||||||
this.sendMessage(this.server.name, "CAP", ["*", "LIST", this.getCapString(this.capVersion)], message.tags);
|
|
||||||
break;
|
break;
|
||||||
}
|
case 'INVITE':
|
||||||
case 'REQ': {
|
this.doINVITE(message);
|
||||||
const capsToChange = (message.params[1].indexOf(' ') === -1) ? [message.params[1]] : message.params[1].split(' ');
|
break;
|
||||||
const capsEnabled: string[] = [];
|
case 'KICK':
|
||||||
capsToChange.forEach(cap => {
|
this.doKICK(message);
|
||||||
if (this.allCaps.has(cap)) {
|
break;
|
||||||
this.enabledCaps.set(cap, '');
|
case 'MODE':
|
||||||
capsEnabled.push(cap);
|
this.doMODE(message);
|
||||||
}
|
break;
|
||||||
});
|
case 'NAMES':
|
||||||
this.sendMessage(this.server.name, "CAP", ["*", "ACK", capsEnabled.join(' ')], message.tags);
|
this.doNAMES(message);
|
||||||
|
break;
|
||||||
|
case 'NOTICE':
|
||||||
|
this.doMSG(message);
|
||||||
|
break;
|
||||||
|
case 'PART':
|
||||||
|
this.doPART(message);
|
||||||
|
break;
|
||||||
|
case 'PING':
|
||||||
|
this.sendMessage(this.server.name, "PONG", message.params, message.tags);
|
||||||
|
break;
|
||||||
|
case 'PRIVMSG':
|
||||||
|
this.doMSG(message);
|
||||||
|
break;
|
||||||
|
case 'TAGMSG':
|
||||||
|
this.doTAGMSG(message);
|
||||||
|
break;
|
||||||
|
case 'TOPIC':
|
||||||
|
this.doTOPIC(message);
|
||||||
|
break;
|
||||||
|
case 'WHO':
|
||||||
|
this.doWHO(message);
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case 'END': {
|
|
||||||
if (this.user !== null) {
|
|
||||||
this.doRegistration(message);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.closeConnectionWithError("You must use SASL to connect to this server");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -331,11 +200,17 @@ export class Client {
|
||||||
if (rooms['join']) {
|
if (rooms['join']) {
|
||||||
const joinedRooms: Set<Channel> = new Set();
|
const joinedRooms: Set<Channel> = new Set();
|
||||||
for (const roomId of Object.keys(rooms.join)) {
|
for (const roomId of Object.keys(rooms.join)) {
|
||||||
const targetChannel = this.server.getOrCreateIRCChannel(roomId);
|
const targetChannel = this.user.getOrCreateIRCChannel(roomId);
|
||||||
joinedRooms.add(targetChannel);
|
joinedRooms.add(targetChannel);
|
||||||
rooms.join[roomId].state.events.forEach((nextEvent: any) => targetChannel.routeMatrixEvent(nextEvent));
|
//@ts-ignore
|
||||||
|
rooms.join[roomId].state.events.forEach((nextEvent: any) => this.user.routeMatrixEvent(nextEvent, targetChannel));
|
||||||
}
|
}
|
||||||
joinedRooms.forEach(c => {if (this.user !== null) this.user.channels.add(c)});
|
joinedRooms.forEach(c => {
|
||||||
|
if (this.user !== null) {
|
||||||
|
this.user.channels.set(c.name, c);
|
||||||
|
this.user.roomIdToChannel.set(c.roomId, c);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (this.user === null)
|
if (this.user === null)
|
||||||
return;
|
return;
|
||||||
|
@ -351,6 +226,314 @@ export class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doCAP(message: IRCMessage) {
|
||||||
|
switch (message.params[0]) {
|
||||||
|
case 'LS': {
|
||||||
|
if (message.params.length === 2) {
|
||||||
|
this.capVersion = message.params[1];
|
||||||
|
}
|
||||||
|
this.sendMessage(this.server.name, "CAP", ["*", "LS", this.getCapString(this.capVersion)], message.tags);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'LIST': {
|
||||||
|
this.sendMessage(this.server.name, "CAP", ["*", "LIST", this.getCapString(this.capVersion)], message.tags);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'REQ': {
|
||||||
|
const capsToChange = (message.params[1].indexOf(' ') === -1) ? [message.params[1]] : message.params[1].split(' ');
|
||||||
|
const capsEnabled: string[] = [];
|
||||||
|
capsToChange.forEach(cap => {
|
||||||
|
if (this.allCaps.has(cap)) {
|
||||||
|
this.enabledCaps.set(cap, '');
|
||||||
|
capsEnabled.push(cap);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.sendMessage(this.server.name, "CAP", ["*", "ACK", capsEnabled.join(' ')], message.tags);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'END': {
|
||||||
|
if (this.user !== null) {
|
||||||
|
this.doRegistration(message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.closeConnectionWithError("You must use SASL to connect to this server");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doINVITE(message: IRCMessage) {
|
||||||
|
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 2))
|
||||||
|
return;
|
||||||
|
const targetUser = this.getMatrixUserFromNick(message.params[0], message.tags);
|
||||||
|
const targetChannel = this.getChannel(message.params[1], message.tags);
|
||||||
|
if (!this.user || !targetUser || !targetChannel) return;
|
||||||
|
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
||||||
|
if (targetChannel.matrixUsers.has(targetUser.nick)) {
|
||||||
|
this.sendMessage(this.server.name, "443", [this.user.nick, targetUser.nick, "is already on channel"], message.tags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reason = (message.params.length === 3) ? message.params[2] : "";
|
||||||
|
const data = {
|
||||||
|
"reason": reason,
|
||||||
|
"user_id": targetUser.mxid
|
||||||
|
}
|
||||||
|
axios.post(`https://${this.user.homeserver}/_matrix/client/v3/rooms/${targetChannel.roomId}/invite?access_token=${this.user.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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
doKICK(message: IRCMessage) {
|
||||||
|
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 2))
|
||||||
|
return;
|
||||||
|
const targetChannel = this.getChannel(message.params[0], message.tags);
|
||||||
|
const targetUser = this.getMatrixUserFromNick(message.params[1], message.tags);
|
||||||
|
if (!this.user || !targetUser || !targetChannel) return;
|
||||||
|
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
||||||
|
if (!targetChannel.matrixUsers.has(targetUser.nick)) {
|
||||||
|
this.sendMessage(this.server.name, "441", [this.user.nick, targetUser.nick, targetChannel.name, "They aren't on that channel"], message.tags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reason = (message.params.length === 3) ? message.params[2] : "";
|
||||||
|
const data = {
|
||||||
|
"reason": reason,
|
||||||
|
"user_id": targetUser.mxid
|
||||||
|
}
|
||||||
|
axios.post(`https://${this.user.homeserver}/_matrix/client/v3/rooms/${targetChannel.roomId}/kick?access_token=${this.user.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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
doMODE(message: IRCMessage) {
|
||||||
|
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1) || !this.user)
|
||||||
|
return;
|
||||||
|
const targetChannel = this.user.channels.get(message.params[0]);
|
||||||
|
if (!targetChannel) {
|
||||||
|
if (message.params[0] !== this.user.nick) {
|
||||||
|
this.sendMessage(this.server.name, "502", [this.user.nick, "Can't view mode for other users"], message.tags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sendMessage(this.server.name, "221", [this.user.nick, "+i"], message.tags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
||||||
|
if (message.params.length === 1) {
|
||||||
|
this.sendMessage(this.server.name, "324", [this.user.nick, targetChannel.name, `+n`], message.tags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doMSG(message: IRCMessage) {
|
||||||
|
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 2))
|
||||||
|
return;
|
||||||
|
const targetChannel = this.getChannel(message.params[0], message.tags);
|
||||||
|
if (!this.user || !targetChannel) return;
|
||||||
|
if (!this.checkIfInChannel(targetChannel, message.tags)) 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 = targetChannel.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.user.txnIdStore.set(newTxnid, this);
|
||||||
|
axios.put(`https://${this.user.homeserver}/_matrix/client/v3/rooms/${targetChannel.roomId}/send/m.room.message/${newTxnid}?access_token=${this.user.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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendNAMES(targetChannel: Channel, passedTags: Map<string, string> = new Map()) {
|
||||||
|
if (!this.user) return;
|
||||||
|
let namesList: string[] = [];
|
||||||
|
for (const matrixUser of targetChannel.matrixUsers.values()) {
|
||||||
|
const opStatus = targetChannel.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 {
|
||||||
|
//@ts-ignore
|
||||||
|
this.sendMessage(this.server.name, "353", numerics["353"](this.user.nick, "=", targetChannel.name, singleNamesList), passedTags);
|
||||||
|
singleNamesList = [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (singleNamesList.length !== 0) {
|
||||||
|
this.sendMessage(this.server.name, "353", numerics["353"](this.user.nick, "=", targetChannel.name, singleNamesList), passedTags);
|
||||||
|
}
|
||||||
|
this.sendMessage(this.server.name, "366", numerics["366"](this.user.nick, targetChannel.name), passedTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
doNAMES(message: IRCMessage) {
|
||||||
|
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1))
|
||||||
|
return;
|
||||||
|
const targetChannel = this.getChannel(message.params[0], message.tags);
|
||||||
|
if (!this.user || !targetChannel) return;
|
||||||
|
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
||||||
|
this.sendNAMES(targetChannel);
|
||||||
|
}
|
||||||
|
|
||||||
|
doPART(message: IRCMessage) {
|
||||||
|
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1))
|
||||||
|
return;
|
||||||
|
const targetChannel = this.getChannel(message.params[0], message.tags);
|
||||||
|
if (!this.user || !targetChannel) return;
|
||||||
|
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
||||||
|
const reason = (message.params.length === 2) ? message.params[1] : "";
|
||||||
|
axios.post(`https://${this.user.homeserver}/_matrix/client/v3/rooms/${targetChannel.roomId}/leave?access_token=${this.user.accessToken}`, {"reason": reason}).then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
//@ts-ignore
|
||||||
|
this.user.getClients().forEach(c => {
|
||||||
|
//@ts-ignore
|
||||||
|
c.sendMessage(this.user.getMask(), "PART", [targetChannel.name, reason], message.tags);
|
||||||
|
})
|
||||||
|
//@ts-ignore
|
||||||
|
this.user.channels.delete(targetChannel.name);
|
||||||
|
//@ts-ignore
|
||||||
|
this.user.roomIdToChannel.delete(targetChannel.roomId);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//@ts-ignore
|
||||||
|
this.sendMessage(this.server.name, "NOTICE", [this.user.nick, JSON.stringify(response.data)], message.tags);
|
||||||
|
}
|
||||||
|
}).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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
doTAGMSG(message: IRCMessage) {
|
||||||
|
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1))
|
||||||
|
return;
|
||||||
|
const targetChannel = this.getChannel(message.params[0], message.tags);
|
||||||
|
if (!this.user || !targetChannel) return;
|
||||||
|
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
||||||
|
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.user.txnIdStore.set(newTxnid, this);
|
||||||
|
axios.put(`https://${this.user.homeserver}/_matrix/client/v3/rooms/${targetChannel.roomId}/send/m.reaction/${newTxnid}?access_token=${this.user.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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTOPIC(targetChannel: Channel, passedTags: Map<string, string> = new Map()) {
|
||||||
|
if (!this.user) return;
|
||||||
|
const topicText = targetChannel.topic.get('text') || '';
|
||||||
|
const topicSetter = targetChannel.topic.get('setter') || 'matrix';
|
||||||
|
const topicTimestamp = targetChannel.topic.get('timestamp') || '0';
|
||||||
|
if (topicText === '') {
|
||||||
|
this.sendMessage(this.server.name, '331', [this.user.nick, targetChannel.name, 'No topic is set'], passedTags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sendMessage(this.server.name, '332', [this.user.nick, targetChannel.name, topicText], passedTags);
|
||||||
|
this.sendMessage(this.server.name, '333', [this.user.nick, targetChannel.name, topicSetter, topicTimestamp], passedTags);
|
||||||
|
}
|
||||||
|
|
||||||
|
doTOPIC(message: IRCMessage) {
|
||||||
|
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1))
|
||||||
|
return;
|
||||||
|
const targetChannel = this.getChannel(message.params[0], message.tags);
|
||||||
|
if (!this.user || !targetChannel) return;
|
||||||
|
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
||||||
|
if (message.params.length === 1) {
|
||||||
|
this.sendTOPIC(targetChannel, message.tags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const topic = message.params[1];
|
||||||
|
axios.put(`https://${this.user.homeserver}/_matrix/client/v3/rooms/${targetChannel.roomId}/state/m.room.topic?access_token=${this.user.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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
doWHO(message: IRCMessage) {
|
||||||
|
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1))
|
||||||
|
return;
|
||||||
|
const targetChannel = this.getChannel(message.params[0], message.tags);
|
||||||
|
if (!this.user || !targetChannel) return;
|
||||||
|
if (!this.checkIfInChannel(targetChannel, message.tags)) return;
|
||||||
|
for (const matrixUser of targetChannel.matrixUsers.values()) {
|
||||||
|
const opStatus = targetChannel.getNickPowerLevelMapping(matrixUser.nick);
|
||||||
|
const userParams = [
|
||||||
|
this.user.nick,
|
||||||
|
targetChannel.name,
|
||||||
|
matrixUser.ident,
|
||||||
|
matrixUser.hostname,
|
||||||
|
this.server.name,
|
||||||
|
matrixUser.nick,
|
||||||
|
`H${opStatus}`,
|
||||||
|
`0 ${matrixUser.realname}`
|
||||||
|
]
|
||||||
|
this.sendMessage(this.server.name, '352', userParams, message.tags);
|
||||||
|
}
|
||||||
|
this.sendMessage(this.server.name, '315', [this.user.nick, targetChannel.name, "End of /WHO"], message.tags);
|
||||||
|
}
|
||||||
|
|
||||||
doRegistration(message: IRCMessage) {
|
doRegistration(message: IRCMessage) {
|
||||||
if (this.user === null) {
|
if (this.user === null) {
|
||||||
this.closeConnectionWithError("You must use SASL to connect to this server");
|
this.closeConnectionWithError("You must use SASL to connect to this server");
|
||||||
|
|
427
src/IRCUser.ts
427
src/IRCUser.ts
|
@ -1,42 +1,80 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { randomUUID } from "crypto";
|
|
||||||
import { Channel } from "./Channel.js";
|
import { Channel } from "./Channel.js";
|
||||||
import { Client } from "./Client.js";
|
import { Client } from "./Client.js";
|
||||||
import { MatrixUser } from "./MatrixUser.js";
|
import { MatrixUser } from "./MatrixUser.js";
|
||||||
import { IRCMessage } from "./Message.js";
|
|
||||||
import { Server } from "./Server.js";
|
import { Server } from "./Server.js";
|
||||||
|
|
||||||
export class IRCUser {
|
export class IRCUser {
|
||||||
private clients: Set<Client>
|
private clients: Set<Client>
|
||||||
public channels: Set<Channel>
|
public channels: Map<string, Channel>
|
||||||
|
public roomIdToChannel: Map<string, Channel>
|
||||||
|
public matrixUsers: Map<string, MatrixUser>
|
||||||
|
public nickToMatrixUser: Map<string, MatrixUser>
|
||||||
public nick: string
|
public nick: string
|
||||||
private ident: string
|
private ident: string
|
||||||
private hostname: string
|
private hostname: string
|
||||||
public accountName: string
|
public accountName: string
|
||||||
public realname: string
|
public realname: string
|
||||||
private txnIdStore: Map<string, Client>
|
public ourMatrixUser: MatrixUser
|
||||||
|
public txnIdStore: Map<string, Client>
|
||||||
public nextBatch: string
|
public nextBatch: string
|
||||||
private initialSync: boolean
|
|
||||||
private isSyncing: boolean
|
private isSyncing: boolean
|
||||||
private currentSyncTime: number
|
private currentSyncTime: number
|
||||||
private syncIntervalID: NodeJS.Timeout;
|
private syncIntervalID: NodeJS.Timeout;
|
||||||
constructor(public mxid: string, private accessToken: string, public homeserver: string, private server: Server) {
|
constructor(public mxid: string, public accessToken: string, public homeserver: string, private server: Server) {
|
||||||
this.clients = new Set();
|
this.clients = new Set();
|
||||||
this.channels = new Set();
|
this.channels = new Map();
|
||||||
|
this.roomIdToChannel = new Map();
|
||||||
|
this.matrixUsers = new Map();
|
||||||
|
this.nickToMatrixUser = new Map();
|
||||||
const mxidSplit = mxid.split(':')
|
const mxidSplit = mxid.split(':')
|
||||||
this.nick = mxidSplit[0].substr(1);
|
this.nick = mxidSplit[0].substring(1);
|
||||||
this.ident = this.nick;
|
this.ident = this.nick;
|
||||||
this.hostname = mxidSplit[1];
|
this.hostname = mxidSplit[1];
|
||||||
this.accountName = mxid.slice(1);
|
this.accountName = mxid.slice(1);
|
||||||
this.realname = this.accountName;
|
this.realname = this.accountName;
|
||||||
|
this.ourMatrixUser = new MatrixUser(this.mxid, this.nick);
|
||||||
|
this.matrixUsers.set(this.mxid, this.ourMatrixUser);
|
||||||
|
this.nickToMatrixUser.set(this.nick, this.ourMatrixUser);
|
||||||
this.txnIdStore = new Map();
|
this.txnIdStore = new Map();
|
||||||
this.nextBatch = "";
|
this.nextBatch = "";
|
||||||
this.initialSync = false;
|
|
||||||
this.isSyncing = false;
|
this.isSyncing = false;
|
||||||
this.currentSyncTime = 0;
|
this.currentSyncTime = 0;
|
||||||
this.syncIntervalID = setInterval(this.doSync.bind(this), 2000);
|
this.syncIntervalID = setInterval(this.doSync.bind(this), 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOrCreateMatrixUser(mxid: string): MatrixUser {
|
||||||
|
let maybeMatrixUser = this.matrixUsers.get(mxid);
|
||||||
|
if (maybeMatrixUser) {
|
||||||
|
return maybeMatrixUser;
|
||||||
|
}
|
||||||
|
let potentialNick = mxid.split(":")[0].substring(1);
|
||||||
|
if (!this.nickToMatrixUser.has(potentialNick)) {
|
||||||
|
const newMatrixUser = new MatrixUser(mxid, potentialNick);
|
||||||
|
this.matrixUsers.set(mxid, newMatrixUser);
|
||||||
|
this.nickToMatrixUser.set(potentialNick, newMatrixUser);
|
||||||
|
return newMatrixUser;
|
||||||
|
}
|
||||||
|
const homeserverArray = mxid.split(":")[1].split('.');
|
||||||
|
const baseDomainNum = homeserverArray.length - 2;
|
||||||
|
potentialNick = `${potentialNick}-${homeserverArray[baseDomainNum]}`;
|
||||||
|
const newMatrixUser = new MatrixUser(mxid, potentialNick);
|
||||||
|
this.matrixUsers.set(mxid, newMatrixUser);
|
||||||
|
this.nickToMatrixUser.set(potentialNick, newMatrixUser);
|
||||||
|
return newMatrixUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrCreateIRCChannel(roomId: string): Channel {
|
||||||
|
const maybeChannel = this.roomIdToChannel.get(roomId);
|
||||||
|
if (maybeChannel)
|
||||||
|
return maybeChannel;
|
||||||
|
|
||||||
|
const newChannel = new Channel(roomId, this.ourMatrixUser);
|
||||||
|
this.roomIdToChannel.set(roomId, newChannel);
|
||||||
|
this.channels.set(roomId, newChannel);
|
||||||
|
return newChannel;
|
||||||
|
}
|
||||||
|
|
||||||
isSynced() {
|
isSynced() {
|
||||||
return this.nextBatch !== "";
|
return this.nextBatch !== "";
|
||||||
}
|
}
|
||||||
|
@ -57,11 +95,21 @@ export class IRCUser {
|
||||||
return this.clients;
|
return this.clients;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
joinNewIRCClient(client: Client, targetChannel: Channel, passedTags: Map<string, string>) {
|
||||||
|
if (client.enabledCaps.has('extended-join')) {
|
||||||
|
client.sendMessage(this.getMask(), "JOIN", [targetChannel.name, this.accountName, this.mxid], new Map([['account', this.realname]]));
|
||||||
|
} else {
|
||||||
|
client.sendMessage(this.getMask(), "JOIN", [targetChannel.name], new Map([['account', this.realname]]));
|
||||||
|
}
|
||||||
|
client.sendNAMES(targetChannel, passedTags);
|
||||||
|
client.sendTOPIC(targetChannel, passedTags);
|
||||||
|
}
|
||||||
|
|
||||||
addClient(client: Client, passedTags: Map<string, string>) {
|
addClient(client: Client, passedTags: Map<string, string>) {
|
||||||
this.clients.add(client);
|
this.clients.add(client);
|
||||||
if (this.nextBatch !== "") {
|
if (this.nextBatch !== "") {
|
||||||
for (const channel of this.channels.values()) {
|
for (const channel of this.channels.values()) {
|
||||||
channel.joinNewIRCClient(client, passedTags);
|
this.joinNewIRCClient(client, channel, passedTags);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,166 +133,283 @@ export class IRCUser {
|
||||||
const rooms = data.rooms;
|
const rooms = data.rooms;
|
||||||
if (rooms && rooms['join']) {
|
if (rooms && rooms['join']) {
|
||||||
for (const roomId of Object.keys(rooms.join)) {
|
for (const roomId of Object.keys(rooms.join)) {
|
||||||
const targetChannel = this.server.getOrCreateIRCChannel(roomId);
|
const targetChannel = this.getOrCreateIRCChannel(roomId);
|
||||||
rooms.join[roomId].timeline.events.forEach((nextEvent: any) => {
|
rooms.join[roomId].timeline.events.forEach((nextEvent: any) => {
|
||||||
targetChannel.routeMatrixEvent(nextEvent)
|
this.routeMatrixEvent(nextEvent, targetChannel);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.isSyncing = false;
|
this.isSyncing = false;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.log(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;
|
this.isSyncing = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
inviteMatrixUser(client: Client, channel: Channel, target: MatrixUser, reason: string, passedTags: Map<string, string> = new Map()) {
|
routeMatrixEvent(nextEvent: any, targetChannel: Channel) {
|
||||||
const data = {
|
switch (nextEvent["type"]) {
|
||||||
"reason": reason,
|
case 'm.reaction':
|
||||||
"user_id": target.mxid
|
this.handleMatrixReaction(nextEvent, targetChannel);
|
||||||
|
break;
|
||||||
|
case 'm.room.canonical_alias':
|
||||||
|
this.updateRoomName(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;
|
||||||
}
|
}
|
||||||
axios.post(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/invite?access_token=${this.accessToken}`, data).catch(function (error) {
|
case 'm.room.power_levels':
|
||||||
if (error.response) {
|
this.handleMatrixPL(nextEvent, targetChannel);
|
||||||
console.log(error.response.data);
|
break;
|
||||||
} else if (error.request) {
|
case 'm.room.topic':
|
||||||
console.log(error.request);
|
this.handleMatrixTopic(nextEvent, targetChannel);
|
||||||
} else {
|
break;
|
||||||
console.log('Error', error.message);
|
// Add some events we aren't going to use now (or ever)
|
||||||
console.log(error.config);
|
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(nextEvent);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kickMatrixUser(client: Client, channel: Channel, target: string, reason: string, passedTags: Map<string, string> = new Map()) {
|
handleMatrixReaction(event: any, targetChannel: Channel) {
|
||||||
const data = {
|
const sourceUser = this.getOrCreateMatrixUser(event["sender"]);
|
||||||
"reason": reason,
|
if (!targetChannel.matrixUsers.has(sourceUser.nick)) {
|
||||||
"user_id": target
|
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()]])
|
||||||
|
this.sendToAllWithCap('extended-join', prefix, "JOIN", [targetChannel.name, sourceUser.accountName, sourceUser.realname], joinTags);
|
||||||
|
this.sendToAllWithoutCap('extended-join', prefix, "JOIN", [targetChannel.name], joinTags);
|
||||||
}
|
}
|
||||||
axios.post(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/kick?access_token=${this.accessToken}`, data).catch(function (error) {
|
const tags: Map<string, string> = new Map();
|
||||||
if (error.response) {
|
tags.set('msgid', event["event_id"]);
|
||||||
console.log(error.response.data);
|
tags.set('account', sourceUser.accountName);
|
||||||
} else if (error.request) {
|
tags.set('time', new Date(event["origin_server_ts"]).toISOString())
|
||||||
console.log(error.request);
|
const reactionData = event["content"]?.['m.relates_to'];
|
||||||
} else {
|
if (!reactionData)
|
||||||
console.log('Error', error.message);
|
return;
|
||||||
console.log(error.config);
|
|
||||||
}
|
const targetMsgid = reactionData["event_id"];
|
||||||
})
|
const reaction = reactionData["key"];
|
||||||
|
tags.set('+draft/reply', targetMsgid);
|
||||||
|
tags.set('+draft/react', reaction);
|
||||||
|
this.sendToAllWithCap("message-tags", sourceUser.getMask(), "TAGMSG", [targetChannel.name], tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
partMatrixRoom(client: Client, channel: Channel, reason: string, passedTags: Map<string, string> = new Map()) {
|
updateRoomName(newNameEvent: any, targetChannel: Channel) {
|
||||||
axios.post(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/leave?access_token=${this.accessToken}`, {"reason": reason}).then(response => {
|
const newName: string = newNameEvent["content"]["alias"];
|
||||||
if (response.status === 200) {
|
if (!newName || newName === targetChannel.name)
|
||||||
this.clients.forEach(c => {
|
return;
|
||||||
c.sendMessage(this.getMask(), "PART", [channel.name, reason], passedTags);
|
const oldName = targetChannel.name;
|
||||||
})
|
this.channels.delete(oldName);
|
||||||
this.channels.delete(channel);
|
targetChannel.name = newName;
|
||||||
|
this.channels.set(newName, targetChannel);
|
||||||
|
this.clients.forEach(client => {
|
||||||
|
if (client.enabledCaps.has("draft/channel-rename")) {
|
||||||
|
client.sendMessage(this.server.name, "RENAME", [oldName, targetChannel.name, "New channel name set"], new Map());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
client.sendMessage(this.server.name, "NOTICE", [this.nick, JSON.stringify(response.data)], passedTags);
|
client.sendMessage(this.server.name, "PART", [oldName, "Renaming channel"], new Map());
|
||||||
}
|
this.joinNewIRCClient(client, targetChannel, new Map());
|
||||||
}).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<string, string> = new Map()) {
|
handleMatrixGuestAccess(event: any, targetChannel: Channel) {
|
||||||
axios.put(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/state/m.room.topic?access_token=${this.accessToken}`, {"topic": topic}).catch(function (error) {
|
const rule = event["content"]?.["guest_access"];
|
||||||
if (error.response) {
|
if (!rule) {
|
||||||
console.log(error.response.data);
|
console.log(`Warning: Guest access not found in ${event}`);
|
||||||
} 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;
|
return;
|
||||||
}
|
}
|
||||||
let msgtype = 'm.text';
|
targetChannel.guestAccess = rule;
|
||||||
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 = {
|
handleMatrixHistoryVisibility(event: any, targetChannel: Channel) {
|
||||||
"body": msgbody,
|
const rule = event["content"]?.["history_visibility"];
|
||||||
"msgtype": msgtype,
|
if (!rule) {
|
||||||
"m.relates_to": {}
|
console.log(`Warning: history visibility not found in ${event}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (message.tags.has("+draft/reply")) {
|
targetChannel.historyVisibility = rule;
|
||||||
content["m.relates_to"] = {
|
|
||||||
"m.in_reply_to": {
|
|
||||||
"event_id": message.tags.get("+draft/reply")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMatrixJoinRule(event: any, targetChannel: Channel) {
|
||||||
|
const rule = event["content"]?.["join_rule"];
|
||||||
|
if (!rule) {
|
||||||
|
console.log(`Warning: join rule not found in ${event}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
targetChannel.joinRules = rule;
|
||||||
}
|
}
|
||||||
const newTxnid = randomUUID();
|
|
||||||
this.txnIdStore.set(newTxnid, client);
|
handleMatrixMember(event: any, targetChannel: Channel) {
|
||||||
axios.put(`https://${this.homeserver}/_matrix/client/v3/rooms/${channel.roomId}/send/m.room.message/${newTxnid}?access_token=${this.accessToken}`, content).catch(function (error) {
|
const targetUser = this.getOrCreateMatrixUser(event["state_key"]);
|
||||||
if (error.response) {
|
const sourceUser = this.getOrCreateMatrixUser(event["sender"]);
|
||||||
console.log(error.response.data);
|
const content = event["content"];
|
||||||
} else if (error.request) {
|
if (!content)
|
||||||
console.log(error.request);
|
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.clients.forEach(c => {
|
||||||
|
if (c.enabledCaps.has('invite-notify')) {
|
||||||
|
if (c.enabledCaps.has('draft/extended-invite')) {
|
||||||
|
c.sendMessage(sourceUser.getMask(), 'INVITE', [targetUser.nick, targetChannel.name, reason], messageTags)
|
||||||
} else {
|
} else {
|
||||||
console.log('Error', error.message);
|
c.sendMessage(sourceUser.getMask(), 'INVITE', [targetUser.nick, targetChannel.name], messageTags)
|
||||||
console.log(error.config);
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (membershipStatus === "join") {
|
||||||
|
if (!targetChannel.matrixUsers.has(targetUser.nick)) {
|
||||||
|
targetChannel.matrixUsers.set(targetUser.nick, targetUser);
|
||||||
|
const prefix = targetUser.getMask();
|
||||||
|
const joinTags = new Map([["account", targetUser.accountName], ['time', new Date(event["origin_server_ts"]).toISOString()]])
|
||||||
|
this.sendToAllWithCap('extended-join', prefix, "JOIN", [targetChannel.name, targetUser.accountName, targetUser.realname], joinTags);
|
||||||
|
this.sendToAllWithoutCap('extended-join', prefix, "JOIN", [targetChannel.name], joinTags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (membershipStatus === "leave") {
|
||||||
|
if (!targetChannel.matrixUsers.has(targetUser.nick))
|
||||||
|
return;
|
||||||
|
if (targetUser.mxid === sourceUser.mxid) {
|
||||||
|
const reason = content["reason"] || 'User left';
|
||||||
|
this.clients.forEach((client) => {
|
||||||
|
client.sendMessage(sourceUser.getMask(), 'PART', [targetChannel.name, reason], messageTags);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const reason = content["reason"] || 'User was kicked';
|
||||||
|
this.clients.forEach((client) => {
|
||||||
|
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.clients.forEach((channel) => {
|
||||||
|
channel.sendMessage(sourceUser.getMask(), 'KICK', [targetChannel.name, targetUser.nick, reason], messageTags);
|
||||||
|
});
|
||||||
|
targetChannel.matrixUsers.delete(targetUser.nick)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(`Got unknown m.room.member event: ${event}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMatrixMessage(event: any, targetChannel: Channel) {
|
||||||
|
const sourceUser = this.getOrCreateMatrixUser(event["sender"]);
|
||||||
|
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()]])
|
||||||
|
this.sendToAllWithCap('extended-join', prefix, "JOIN", [targetChannel.name, sourceUser.accountName, sourceUser.realname], joinTags);
|
||||||
|
this.sendToAllWithoutCap('extended-join', prefix, "JOIN", [targetChannel.name], joinTags);
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
const maybeReply = content["m.relates_to"]?.["m.in_reply_to"]?.["event_id"];
|
||||||
|
const maybeTxnId: string = event["unsigned"]?.["transaction_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://${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];
|
||||||
|
msgArray.forEach((msg: string) => {
|
||||||
|
if (msg) {
|
||||||
|
this.clients.forEach((client) => {
|
||||||
|
if (this.txnIdStore.get(maybeTxnId) === client) {
|
||||||
|
if (client.enabledCaps.has('echo-message')) {
|
||||||
|
client.sendMessage(sourceUser.getMask(), ircCommand, [targetChannel.name, msg], tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
client.sendMessage(sourceUser.getMask(), ircCommand, [targetChannel.name, msg], tags)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
sendTagToMatrix(message: IRCMessage, client: Client) {
|
handleMatrixPL(event: any, targetChannel: Channel) {
|
||||||
const channel = this.server.ircChannels.get(message.params[0]);
|
const allUsers = event["content"]["users"];
|
||||||
if (!channel) {
|
for (const [mxid, pl] of Object.entries(allUsers)) {
|
||||||
|
const thisMatrixUser = this.getOrCreateMatrixUser(event["sender"]);
|
||||||
|
targetChannel.matrixUsers.set(thisMatrixUser.nick, thisMatrixUser);
|
||||||
|
targetChannel.powerLevels.set(thisMatrixUser.nick, Number(pl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMatrixTopic(event: any, targetChannel: Channel) {
|
||||||
|
const topicText = event["content"]?.["topic"];
|
||||||
|
if (!topicText)
|
||||||
return;
|
return;
|
||||||
}
|
const topicSetter = this.getOrCreateMatrixUser(event["sender"]);
|
||||||
console.log(message.tags)
|
const topicTS: string = event["origin_server_ts"].toString();
|
||||||
if (message.tags.has("+draft/react") && message.tags.has("+draft/reply")) {
|
targetChannel.topic.set("text", topicText);
|
||||||
const content = {
|
targetChannel.topic.set("timestamp", topicTS.substring(0,10))
|
||||||
"m.relates_to": {
|
targetChannel.topic.set('setter', topicSetter.nick);
|
||||||
"event_id": message.tags.get("+draft/reply"),
|
|
||||||
"key": message.tags.get("+draft/react"),
|
const messageTags = new Map();
|
||||||
"rel_type": "m.annotation"
|
messageTags.set('msgid', event["event_id"]);
|
||||||
}
|
messageTags.set('account', topicSetter.accountName);
|
||||||
}
|
messageTags.set('time', new Date(event["origin_server_ts"]).toISOString())
|
||||||
const newTxnid = randomUUID();
|
this.clients.forEach((client) => {
|
||||||
this.txnIdStore.set(newTxnid, client);
|
client.sendMessage(topicSetter.getMask(), 'TOPIC', [targetChannel.name, topicText], messageTags);
|
||||||
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<string, string> = new Map(), skipClient: Client|null = null) {
|
sendToAll(prefix: string, command: string, params: string[], tags: Map<string, string> = new Map(), skipClient: Client|null = null) {
|
||||||
this.clients.forEach(client => {
|
this.clients.forEach(client => {
|
||||||
|
|
|
@ -1,25 +1,11 @@
|
||||||
import { Channel } from "./Channel.js"
|
|
||||||
import { IRCUser } from "./IRCUser.js"
|
import { IRCUser } from "./IRCUser.js"
|
||||||
import { MatrixUser } from "./MatrixUser.js"
|
|
||||||
|
|
||||||
export class Server {
|
export class Server {
|
||||||
public name: string
|
public name: string
|
||||||
// <roomId, Channel>
|
|
||||||
public matrixRooms: Map<string, Channel>
|
|
||||||
// <roomAlias (fallback to roomId), Channel>
|
|
||||||
public ircChannels: Map<string, Channel>
|
|
||||||
// <mxid, MatrixUser>
|
|
||||||
public matrixUsers: Map<string, MatrixUser>
|
|
||||||
// <mxid, IRCUser>
|
|
||||||
public ircUsers: Map<string, IRCUser>
|
public ircUsers: Map<string, IRCUser>
|
||||||
public nickToMxid: Map<string, string>
|
|
||||||
constructor(public config: any) {
|
constructor(public config: any) {
|
||||||
this.name = this.config.serverName;
|
this.name = this.config.serverName;
|
||||||
this.matrixRooms = new Map();
|
|
||||||
this.ircChannels = new Map();
|
|
||||||
this.matrixUsers = new Map();
|
|
||||||
this.ircUsers = new Map();
|
this.ircUsers = new Map();
|
||||||
this.nickToMxid = new Map();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrCreateIRCUser(mxid: string, accessToken: string, homeserver: string): IRCUser {
|
getOrCreateIRCUser(mxid: string, accessToken: string, homeserver: string): IRCUser {
|
||||||
|
@ -29,36 +15,4 @@ export class Server {
|
||||||
}
|
}
|
||||||
return new IRCUser(mxid, accessToken, homeserver, this);
|
return new IRCUser(mxid, accessToken, homeserver, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrCreateIRCChannel(roomId: string): Channel {
|
|
||||||
const maybeChannel = this.matrixRooms.get(roomId);
|
|
||||||
if (maybeChannel)
|
|
||||||
return maybeChannel;
|
|
||||||
|
|
||||||
const newChannel = new Channel(roomId, this);
|
|
||||||
this.matrixRooms.set(roomId, newChannel);
|
|
||||||
this.ircChannels.set(roomId, newChannel);
|
|
||||||
return newChannel;
|
|
||||||
}
|
|
||||||
|
|
||||||
getOrCreateMatrixUser(mxid: string): MatrixUser {
|
|
||||||
let maybeMatrixUser = this.matrixUsers.get(mxid);
|
|
||||||
if (maybeMatrixUser) {
|
|
||||||
return maybeMatrixUser;
|
|
||||||
}
|
|
||||||
let potentialNick = mxid.split(":")[0].substr(1);
|
|
||||||
if (!this.nickToMxid.has(potentialNick)) {
|
|
||||||
const newMatrixUser = new MatrixUser(mxid, potentialNick);
|
|
||||||
this.matrixUsers.set(mxid, newMatrixUser);
|
|
||||||
this.nickToMxid.set(potentialNick, mxid);
|
|
||||||
return newMatrixUser;
|
|
||||||
}
|
|
||||||
const homeserverArray = mxid.split(":")[1].split('.');
|
|
||||||
const baseDomainNum = homeserverArray.length - 2;
|
|
||||||
potentialNick = `${potentialNick}-${homeserverArray[baseDomainNum]}`;
|
|
||||||
const newMatrixUser = new MatrixUser(mxid, potentialNick);
|
|
||||||
this.matrixUsers.set(mxid, newMatrixUser);
|
|
||||||
this.nickToMxid.set(potentialNick, mxid);
|
|
||||||
return newMatrixUser;
|
|
||||||
}
|
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue