mirror of
https://git.sr.ht/~emerson/reflectionircd
synced 2025-04-13 09:59:52 +00:00

Controversial change, but offloading the problem of being a bouncer onto bouncers is a better idea
850 lines
No EOL
35 KiB
TypeScript
850 lines
No EOL
35 KiB
TypeScript
import { Axios } from 'axios';
|
|
import { randomUUID } from 'crypto';
|
|
import { Socket } from 'net';
|
|
import { Batch } from './Batch.js';
|
|
import { Channel } from './Channel.js';
|
|
import { MatrixUser } from './MatrixUser.js';
|
|
import { IRCMessage, parseIRCMessage } from './Message.js';
|
|
import { Server } from './Server.js';
|
|
|
|
export class Client {
|
|
capVersion: string
|
|
enabledCaps: Map<string, string>
|
|
allCaps: Map<string, string>
|
|
user: MatrixUser
|
|
isRegistered: boolean
|
|
apiCall: Axios
|
|
batchesInProgress: Map<string, Batch>
|
|
constructor(public server: Server) {
|
|
this.capVersion = '301';
|
|
this.enabledCaps = new Map();
|
|
this.allCaps = new Map([
|
|
["account-tag", ""],
|
|
["batch", ""],
|
|
["draft/channel-rename", ""],
|
|
["draft/multiline", "max-bytes=4096,max-lines=20"],
|
|
["echo-message", ""],
|
|
["reflectionircd.chat/edit-message", ""],
|
|
["reflectionircd.chat/extended-invite", ""],
|
|
["extended-join", ""],
|
|
["invite-notify", ""],
|
|
["labeled-response", ""],
|
|
["message-tags", ""],
|
|
["sasl", "PLAIN"],
|
|
["server-time", ""],
|
|
]);
|
|
this.user = this.server.ourMatrixUser;
|
|
this.isRegistered = false;
|
|
this.apiCall = this.server.apiCall;
|
|
this.batchesInProgress = new Map();
|
|
}
|
|
|
|
checkIfRegistered() {
|
|
return this.isRegistered;
|
|
}
|
|
|
|
receiveData(data: Buffer|String) {
|
|
const dataArray = data.toString().split('\r\n');
|
|
dataArray.forEach(m => {
|
|
const trimmedMsg = m.replace('\r', '').replace('\n', '');
|
|
if (trimmedMsg !== '')
|
|
this.routeMessage(trimmedMsg);
|
|
});
|
|
}
|
|
|
|
getMatrixUserFromNick(targetNick: string, message: IRCMessage) {
|
|
const target = this.server.nickToMatrixUser.get(targetNick);
|
|
if (!target) {
|
|
this.sendMessage(this.server.name, "401", [this.user.nick, targetNick, "No such nick"], message.tags);
|
|
return false;
|
|
}
|
|
return target;
|
|
}
|
|
|
|
getChannel(channel: string, message: IRCMessage) {
|
|
const targetChannel = this.server.channels.get(channel);
|
|
if (!targetChannel) {
|
|
this.sendMessage(this.server.name, "403", [this.user.nick, channel, "No such channel"], message.tags);
|
|
return false;
|
|
}
|
|
return targetChannel;
|
|
}
|
|
|
|
checkIfInChannel(channel: Channel, message: IRCMessage) {
|
|
if (!this.server.channels.get(channel.name)) {
|
|
this.sendMessage(this.server.name, "442", [this.user.nick, "You're not on that channel"], message.tags);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
checkMinParams(message: IRCMessage, neededNumber: number) {
|
|
if (message.params.length < neededNumber) {
|
|
this.sendMessage(this.server.name, "461", [this.user.nick, message.command, "Not enough parameters"], message.tags);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
getCapString(capVersion: string) {
|
|
let capArray: string[] = [];
|
|
for (const [key, value] of this.allCaps.entries()) {
|
|
if (capVersion === '301' || value.length === 0) {
|
|
capArray.push(key);
|
|
}
|
|
else {
|
|
capArray.push(`${key}=${value}`);
|
|
}
|
|
}
|
|
return capArray.join(' ');
|
|
}
|
|
|
|
routeMessage(data: string) {
|
|
const message = parseIRCMessage(data);
|
|
const maybeBatchRef = message.tags.get('batch');
|
|
if (maybeBatchRef) {
|
|
const maybeBatch = this.batchesInProgress.get(maybeBatchRef);
|
|
if (maybeBatch) {
|
|
maybeBatch.messages.add(message);
|
|
}
|
|
return;
|
|
}
|
|
switch (message.command.toUpperCase()) {
|
|
case 'AUTHENTICATE':
|
|
this.doAUTHENTICATE(message);
|
|
break;
|
|
case 'AWAY':
|
|
this.doAWAY(message);
|
|
break;
|
|
case 'BATCH':
|
|
this.doBATCH(message);
|
|
break;
|
|
case 'CAP':
|
|
this.doCAP(message);
|
|
break;
|
|
case 'DELETEMSG':
|
|
this.doDELETEMSG(message);
|
|
break;
|
|
case 'INVITE':
|
|
this.doINVITE(message);
|
|
break;
|
|
case 'KICK':
|
|
this.doKICK(message);
|
|
break;
|
|
case 'MODE':
|
|
this.doMODE(message);
|
|
break;
|
|
case 'NAMES':
|
|
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;
|
|
case 'WHOIS':
|
|
this.doWHOIS(message);
|
|
break;
|
|
case 'JOIN':
|
|
case 'NICK':
|
|
case 'USER':
|
|
// Exempting these from sending a 421, otherwise it will get annoying
|
|
break;
|
|
default:
|
|
this.sendMessage(this.server.name, "421", [message.command, 'Unknown command'], message.tags);
|
|
console.log(`unknown command ${message.command}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
doAUTHENTICATE(message: IRCMessage) {
|
|
if (message.params[0] === "PLAIN") {
|
|
this.sendMessage("", "AUTHENTICATE", ["+"]);
|
|
}
|
|
else {
|
|
const authArray = Buffer.from(message.params[0], 'base64').toString('utf-8').split('\0');
|
|
if (!authArray || authArray.length !== 3) {
|
|
this.sendMessage(this.server.name, '904', ['*', "SASL Authentication failed"], message.tags)
|
|
this.closeConnectionWithError('Invalid authentication')
|
|
}
|
|
if (authArray[2] === this.server.config.SASLPassword) {
|
|
this.sendMessage(this.server.name, '900', [this.user.nick, this.server.getMask(), this.user.accountName, `You are now logged in as ${this.user.nick}`], message.tags);
|
|
this.sendMessage(this.server.name, '903', [this.user.nick, "SASL authentication successful"], message.tags);
|
|
this.isRegistered = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
doAWAY(message: IRCMessage) {
|
|
const data = {
|
|
presence: 'online',
|
|
status_msg: ''
|
|
}
|
|
if (message.params.length === 1) {
|
|
data.presence = 'unavailable';
|
|
data.status_msg = message.params[0];
|
|
}
|
|
this.apiCall.put(`/presence/${this.user.mxid}/status`, data).then(r => {
|
|
// Returning the IRC numerics here because most servers have presence disabled anyways
|
|
if (data.presence === 'online') {
|
|
this.sendMessage(this.server.name, "305", [this.user.nick, "You are no longer marked as being away"], message.tags);
|
|
} else {
|
|
this.sendMessage(this.server.name, "306", [this.user.nick, "You have been marked as being away"], 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);
|
|
}
|
|
})
|
|
}
|
|
|
|
doBATCH(message: IRCMessage) {
|
|
const referenceTag = message.params[0].substring(1);
|
|
if (message.params[0].startsWith('+')) {
|
|
this.batchesInProgress.set(referenceTag, new Batch(referenceTag, message))
|
|
} else if (message.params[0].startsWith('-')) {
|
|
const readyBatch = this.batchesInProgress.get(referenceTag);
|
|
if (readyBatch) {
|
|
switch (readyBatch.batchType) {
|
|
case 'draft/multiline':
|
|
case 'multiline':
|
|
this.doMultiline(readyBatch);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
this.batchesInProgress.delete(referenceTag);
|
|
}
|
|
}
|
|
|
|
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.isRegistered) {
|
|
this.doRegistration(message);
|
|
}
|
|
else {
|
|
this.closeConnectionWithError("You must use SASL to connect to this server");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
doDELETEMSG(message: IRCMessage) {
|
|
if (!this.checkIfRegistered() || !this.checkMinParams(message, 1))
|
|
return;
|
|
const targetChannel = this.getChannel(message.params[0], message);
|
|
const eventId = message.tags.get("reflectionircd.chat/delete-message");
|
|
if (!this.user || !targetChannel || !eventId) return;
|
|
if (!this.checkIfInChannel(targetChannel, message)) return;
|
|
const data = {
|
|
"reason": (message.params.length === 2) ? message.params[1] : ""
|
|
}
|
|
const newTxnid = randomUUID();
|
|
this.apiCall.put(`/rooms/${targetChannel.roomId}/redact/${eventId}/${newTxnid}`, data).then(r => {
|
|
const maybeEventID = r.data["event_id"];
|
|
if (maybeEventID) {
|
|
this.server.eventIdStore.set(maybeEventID, this);
|
|
const maybeLabel = message.tags.get("label") || "";
|
|
if (maybeLabel !== "") {
|
|
this.server.eventIDToLabel.set(maybeEventID, maybeLabel)
|
|
}
|
|
}
|
|
}).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);
|
|
}
|
|
})
|
|
}
|
|
|
|
doINVITE(message: IRCMessage) {
|
|
if (!this.checkIfRegistered() || !this.checkMinParams(message, 2))
|
|
return;
|
|
const targetUser = this.getMatrixUserFromNick(message.params[0], message);
|
|
const targetChannel = this.getChannel(message.params[1], message);
|
|
if (!this.user || !targetUser || !targetChannel) return;
|
|
if (!this.checkIfInChannel(targetChannel, message)) 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
|
|
}
|
|
this.apiCall.post(`/rooms/${targetChannel.roomId}/invite`, data).then(r => {
|
|
const maybeEventID = r.data["event_id"];
|
|
if (maybeEventID) {
|
|
this.server.eventIdStore.set(maybeEventID, this);
|
|
const maybeLabel = message.tags.get("label") || "";
|
|
if (maybeLabel !== "") {
|
|
this.server.eventIDToLabel.set(maybeEventID, maybeLabel)
|
|
}
|
|
}
|
|
}).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() || !this.checkMinParams(message, 2))
|
|
return;
|
|
const targetChannel = this.getChannel(message.params[0], message);
|
|
const targetUser = this.getMatrixUserFromNick(message.params[1], message);
|
|
if (!this.user || !targetUser || !targetChannel) return;
|
|
if (!this.checkIfInChannel(targetChannel, message)) 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
|
|
}
|
|
this.apiCall.post(`/rooms/${targetChannel.roomId}/kick`, data).then(r => {
|
|
const maybeEventID = r.data["event_id"];
|
|
if (maybeEventID) {
|
|
this.server.eventIdStore.set(maybeEventID, this);
|
|
const maybeLabel = message.tags.get("label") || "";
|
|
if (maybeLabel !== "") {
|
|
this.server.eventIDToLabel.set(maybeEventID, maybeLabel)
|
|
}
|
|
}
|
|
}).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() || !this.checkMinParams(message, 1) || !this.user)
|
|
return;
|
|
const targetChannel = this.server.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"]);
|
|
return;
|
|
}
|
|
if (!this.checkIfInChannel(targetChannel, message)) return;
|
|
if (message.params.length === 1) {
|
|
const chanModes = [...targetChannel.channelModes.keys()].sort().join('');
|
|
this.sendMessage(this.server.name, "324", [this.user.nick, targetChannel.name, `+${chanModes}`], message.tags);
|
|
return;
|
|
}
|
|
}
|
|
|
|
doMultiline(batch: Batch) {
|
|
if (batch.messages.size === 0) {
|
|
return;
|
|
}
|
|
let fullMessage = '';
|
|
const firstMessage = [...batch.messages][0];
|
|
const msgType = (firstMessage.command === 'NOTICE') ? 'm.notice' : 'm.text';
|
|
const targetChannel = this.getChannel(firstMessage.params[0], firstMessage);
|
|
if (!this.user || !targetChannel) return;
|
|
if (!this.checkIfInChannel(targetChannel, firstMessage)) return;
|
|
if (targetChannel.roomType === "m.space") {
|
|
this.sendMessage(this.server.name, "NOTICE", [targetChannel.name, "Sending messages to spaces is not allowed"], batch.openingBatch.tags);
|
|
return;
|
|
}
|
|
for (const msg of batch.messages) {
|
|
console.log(firstMessage.params[1], msg.params[1]);
|
|
const separator = (msg.tags.has('draft/multiline-concat') || firstMessage.params[1] === msg.params[1]) ? '' : '\n';
|
|
fullMessage = `${fullMessage}${separator}${msg.params[1]}`;
|
|
console.log(fullMessage);
|
|
}
|
|
const highlightFilteredMsg = fullMessage.split(" ").map(w => {
|
|
if (!w.startsWith('@')) return w;
|
|
const endingCharMatch = w.match(/[,:]$/);
|
|
const endingChar = (endingCharMatch) ? endingCharMatch[0] : "";
|
|
const endingCharIndex = (endingCharMatch) ? endingCharMatch.index : 0;
|
|
const nickToSearch = (endingCharIndex === 0) ? w.substring(1) : w.substring(1, endingCharIndex);
|
|
const maybeHighlight = targetChannel.matrixUsers.get(nickToSearch);
|
|
return (maybeHighlight) ? `${maybeHighlight.mxid}${endingChar}` : w;
|
|
})
|
|
const content = {
|
|
"body": highlightFilteredMsg.join(" "),
|
|
"msgtype": msgType,
|
|
"m.relates_to": {}
|
|
}
|
|
if (batch.openingBatch.tags.has("+draft/reply")) {
|
|
content["m.relates_to"] = {
|
|
"m.in_reply_to": {
|
|
"event_id": batch.openingBatch.tags.get("+draft/reply")
|
|
}
|
|
}
|
|
}
|
|
const newTxnid = randomUUID();
|
|
this.apiCall.put(`/rooms/${targetChannel.roomId}/send/m.room.message/${newTxnid}`, content).then(r => {
|
|
const maybeEventID = r.data["event_id"];
|
|
if (maybeEventID) {
|
|
this.server.eventIdStore.set(maybeEventID, this);
|
|
const maybeLabel = batch.openingBatch.tags.get("label") || "";
|
|
if (maybeLabel !== "") {
|
|
this.server.eventIDToLabel.set(maybeEventID, maybeLabel)
|
|
}
|
|
}
|
|
}).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);
|
|
}
|
|
});
|
|
}
|
|
|
|
doMSG(message: IRCMessage) {
|
|
if (!this.checkIfRegistered() || !this.checkMinParams(message, 2))
|
|
return;
|
|
const targetChannel = this.getChannel(message.params[0], message);
|
|
if (!this.user || !targetChannel) return;
|
|
if (!this.checkIfInChannel(targetChannel, message)) return;
|
|
if (targetChannel.roomType === "m.space") {
|
|
this.sendMessage(this.server.name, "NOTICE", [targetChannel.name, "Sending messages to spaces is not allowed"], 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 highlightFilteredMsg = msgbody.split(" ").map(w => {
|
|
if (!w.startsWith('@')) return w;
|
|
const endingCharMatch = w.match(/[,:]$/);
|
|
const endingChar = (endingCharMatch) ? endingCharMatch[0] : "";
|
|
const endingCharIndex = (endingCharMatch) ? endingCharMatch.index : 0;
|
|
const nickToSearch = (endingCharIndex === 0) ? w.substring(1) : w.substring(1, endingCharIndex);
|
|
const maybeHighlight = targetChannel.matrixUsers.get(nickToSearch);
|
|
return (maybeHighlight) ? `${maybeHighlight.mxid}${endingChar}` : w;
|
|
})
|
|
const content = {
|
|
"body": highlightFilteredMsg.join(" "),
|
|
"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.apiCall.put(`/rooms/${targetChannel.roomId}/send/m.room.message/${newTxnid}`, content).then(r => {
|
|
const maybeEventID = r.data["event_id"];
|
|
if (maybeEventID) {
|
|
this.server.eventIdStore.set(maybeEventID, this);
|
|
const maybeLabel = message.tags.get("label") || "";
|
|
if (maybeLabel !== "") {
|
|
this.server.eventIDToLabel.set(maybeEventID, maybeLabel)
|
|
}
|
|
}
|
|
}).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, batchLabel: string = "") {
|
|
if (!this.user) return;
|
|
const newTag = new Map();
|
|
if (batchLabel) {
|
|
newTag.set('batch', batchLabel);
|
|
}
|
|
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 {
|
|
this.sendMessage(this.server.name, "353", [this.user.nick, "=", targetChannel.name, `${singleNamesList.join(' ')}`], newTag);
|
|
singleNamesList = [];
|
|
}
|
|
})
|
|
if (singleNamesList.length !== 0) {
|
|
this.sendMessage(this.server.name, "353", [this.user.nick, "=", targetChannel.name, `${singleNamesList.join(' ')}`], newTag);
|
|
}
|
|
this.sendMessage(this.server.name, "366", [this.user.nick, targetChannel.name, "End of /NAMES list"], newTag);
|
|
}
|
|
|
|
doNAMES(message: IRCMessage) {
|
|
if (!this.checkIfRegistered() || !this.checkMinParams(message, 1))
|
|
return;
|
|
const targetChannel = this.getChannel(message.params[0], message);
|
|
if (!this.user || !targetChannel) return;
|
|
if (!this.checkIfInChannel(targetChannel, message)) return;
|
|
const messageLabel = message.tags.get('label') || "";
|
|
let batchLabel = "";
|
|
if (messageLabel) {
|
|
batchLabel = Math.random().toString(36).substring(2,7);
|
|
this.sendMessage(this.server.name, 'BATCH', [`+${batchLabel}`, 'labeled-response'], message.tags);
|
|
}
|
|
this.sendNAMES(targetChannel, batchLabel);
|
|
if (messageLabel) {
|
|
this.sendMessage(this.server.name, 'BATCH', [`-${batchLabel}`]);
|
|
}
|
|
}
|
|
|
|
doPART(message: IRCMessage) {
|
|
if (!this.checkIfRegistered() || !this.checkMinParams(message, 1))
|
|
return;
|
|
const targetChannel = this.getChannel(message.params[0], message);
|
|
if (!this.user || !targetChannel) return;
|
|
if (!this.checkIfInChannel(targetChannel, message)) return;
|
|
const reason = (message.params.length === 2) ? message.params[1] : "";
|
|
this.apiCall.post(`/rooms/${targetChannel.roomId}/leave`, {"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() || !this.checkMinParams(message, 1))
|
|
return;
|
|
const targetChannel = this.getChannel(message.params[0], message);
|
|
if (!this.user || !targetChannel) return;
|
|
if (!this.checkIfInChannel(targetChannel, message)) 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.apiCall.put(`/rooms/${targetChannel.roomId}/send/m.reaction/${newTxnid}`, content).then(r => {
|
|
const maybeEventID = r.data["event_id"];
|
|
if (maybeEventID) {
|
|
this.server.eventIdStore.set(maybeEventID, this);
|
|
const maybeLabel = message.tags.get("label") || "";
|
|
if (maybeLabel !== "") {
|
|
this.server.eventIDToLabel.set(maybeEventID, maybeLabel)
|
|
}
|
|
}
|
|
}).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);
|
|
}
|
|
});
|
|
}
|
|
if (message.tags.has("+typing")) {
|
|
const data = {
|
|
"typing": false
|
|
}
|
|
if (message.tags.get("+typing") === "active") {
|
|
data.typing = true;
|
|
}
|
|
this.apiCall.put(`/rooms/${targetChannel.roomId}/typing/${this.user.mxid}`, data).then(r => {
|
|
// No response body for successful request
|
|
}).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, messageTags: Map<string, string>) {
|
|
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'], messageTags);
|
|
return;
|
|
}
|
|
const messageLabel = messageTags.get('label') || "";
|
|
let batchLabel = "";
|
|
if (messageLabel) {
|
|
batchLabel = Math.random().toString(36).substring(2,7);
|
|
this.sendMessage(this.server.name, 'BATCH', [`+${batchLabel}`, 'labeled-response'], messageTags);
|
|
}
|
|
const newTag = new Map();
|
|
if (batchLabel) {
|
|
newTag.set('batch', batchLabel);
|
|
}
|
|
this.sendMessage(this.server.name, '332', [this.user.nick, targetChannel.name, topicText], newTag);
|
|
this.sendMessage(this.server.name, '333', [this.user.nick, targetChannel.name, topicSetter, topicTimestamp], newTag);
|
|
if (messageLabel) {
|
|
this.sendMessage(this.server.name, 'BATCH', [`-${batchLabel}`]);
|
|
}
|
|
}
|
|
|
|
doTOPIC(message: IRCMessage) {
|
|
if (!this.checkIfRegistered() || !this.checkMinParams(message, 1))
|
|
return;
|
|
const targetChannel = this.getChannel(message.params[0], message);
|
|
if (!this.user || !targetChannel) return;
|
|
if (!this.checkIfInChannel(targetChannel, message)) return;
|
|
if (message.params.length === 1) {
|
|
this.sendTOPIC(targetChannel, message.tags);
|
|
return;
|
|
}
|
|
const topic = message.params[1];
|
|
this.apiCall.put(`/rooms/${targetChannel.roomId}/state/m.room.topic`, {"topic": topic}).then(r => {
|
|
const maybeEventID = r.data["event_id"];
|
|
if (maybeEventID) {
|
|
this.server.eventIdStore.set(maybeEventID, this);
|
|
const maybeLabel = message.tags.get("label") || "";
|
|
if (maybeLabel !== "") {
|
|
this.server.eventIDToLabel.set(maybeEventID, maybeLabel)
|
|
}
|
|
}
|
|
}).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() || !this.checkMinParams(message, 1))
|
|
return;
|
|
const targetChannel = this.getChannel(message.params[0], message);
|
|
if (!this.user || !targetChannel) return;
|
|
if (!this.checkIfInChannel(targetChannel, message)) return;
|
|
const newTags = new Map();
|
|
if (message.tags.get('label')) {
|
|
const batchLabel = Math.random().toString(36).substring(2,7);
|
|
this.sendMessage(this.server.name, 'BATCH', [`+${batchLabel}`, 'labeled-response'], message.tags);
|
|
newTags.set('batch', batchLabel);
|
|
}
|
|
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, newTags);
|
|
}
|
|
this.sendMessage(this.server.name, '315', [this.user.nick, targetChannel.name, "End of /WHO"], newTags);
|
|
if (message.tags.get('label')) {
|
|
this.sendMessage(this.server.name, 'BATCH', [`-${newTags.get('batch')}`]);
|
|
}
|
|
}
|
|
|
|
doWHOIS(message: IRCMessage) {
|
|
if (!this.checkIfRegistered() || !this.checkMinParams(message, 1))
|
|
return;
|
|
const targetUser = this.getMatrixUserFromNick(message.params[0], message);
|
|
if (!targetUser) return;
|
|
const tNick = targetUser.nick;
|
|
const newTags = new Map();
|
|
if (message.tags.get('label')) {
|
|
const batchLabel = Math.random().toString(36).substring(2,7);
|
|
this.sendMessage(this.server.name, 'BATCH', [`+${batchLabel}`, 'labeled-response'], message.tags);
|
|
newTags.set('batch', batchLabel);
|
|
}
|
|
this.sendMessage(this.server.name, '311', [this.user.nick, tNick, targetUser.ident, targetUser.hostname, '*', targetUser.realname], newTags);
|
|
this.sendMessage(this.server.name, '330', [this.user.nick, tNick, targetUser.accountName, 'is logged in as'], newTags);
|
|
this.sendMessage(this.server.name, '318', [this.user.nick, tNick, "End of /WHOIS list"], newTags);
|
|
if (message.tags.get('label')) {
|
|
this.sendMessage(this.server.name, 'BATCH', [`-${newTags.get('batch')}`]);
|
|
}
|
|
}
|
|
|
|
doRegistration(message: IRCMessage) {
|
|
if (this.user === null) {
|
|
this.closeConnectionWithError("You must use SASL to connect to this server");
|
|
return;
|
|
}
|
|
this.sendMessage(this.server.name, '001', [this.user.nick, `Welcome to the ${this.server.name} network, ${this.user.nick}`])
|
|
this.sendMessage(this.server.name, '002', [this.user.nick, `Your host is ${this.server.name}, running version 0.1.0`]);
|
|
this.sendMessage(this.server.name, '003', [this.user.nick, `This server was created yesterday`]);
|
|
this.sendMessage(this.server.name, '004', [this.user.nick, this.server.name, '0.1.0', 'i', 'Shnouv']);
|
|
const iSupportArray = [
|
|
'CASEMAPPING=ascii',
|
|
'CHANMODES=,,,Snu',
|
|
'CHANTYPES=#&!',
|
|
'MAXTARGETS=1',
|
|
'MODES=1',
|
|
'PREFIX=(ohv)@%+',
|
|
]
|
|
if (this.enabledCaps.has('draft/chathistory')) {
|
|
iSupportArray.push('CHATHISTORY=50');
|
|
}
|
|
this.sendMessage(this.server.name, '005', [this.user.nick, ...iSupportArray, 'are supported by this server']);
|
|
|
|
this.sendMessage(this.server.name, '375', [this.user.nick, "- Start of MOTD"]);
|
|
this.sendMessage(this.server.name, '372', [this.user.nick, "It's an MOTD"]);
|
|
this.sendMessage(this.server.name, '376', [this.user.nick, "- End of MOTD"]);
|
|
|
|
this.sendMessage(this.user.nick, 'MODE', [this.user.nick, '+i']);
|
|
this.server.addClient(this);
|
|
}
|
|
|
|
sendMessage(prefix: string, command: string, params: string[], tags: Map<string, string> = new Map()) {
|
|
const capTagMapping = new Map([
|
|
['account', 'account-tag'],
|
|
['batch', 'batch'],
|
|
['label', 'labeled-response'],
|
|
['msgid', 'message-tags'],
|
|
['reflectionircd.chat/delete-message', 'reflectionircd.chat/edit-message'],
|
|
['reflectionircd.chat/edit-message', 'reflectionircd.chat/edit-message'],
|
|
['time', 'server-time'],
|
|
])
|
|
const ourTags: Map<string, string> = new Map();
|
|
if (this.enabledCaps.has('server-time') && !tags.has('time'))
|
|
ourTags.set('time', new Date().toISOString());
|
|
|
|
tags.forEach((v, k) => {
|
|
if (k.startsWith('+')) {
|
|
if (this.enabledCaps.has('message-tags')) {
|
|
ourTags.set(k, v);
|
|
}
|
|
}
|
|
else {
|
|
const capToCheck = capTagMapping.get(k) || '';
|
|
if (this.enabledCaps.has(capToCheck)) {
|
|
ourTags.set(k, v);
|
|
}
|
|
}
|
|
})
|
|
const newMsg = new IRCMessage(ourTags, prefix, command, params);
|
|
const msgToSend = newMsg.toString();
|
|
this.writeMessage(`${msgToSend}\r\n`);
|
|
}
|
|
|
|
writeMessage(message: string): void {};
|
|
|
|
closeConnectionWithError(message: string) {
|
|
this.sendMessage(this.server.name, 'ERROR', [message], new Map());
|
|
this.closeConnection();
|
|
}
|
|
|
|
closeConnection(): void {};
|
|
} |