reflectionircd/src/Client.ts

427 lines
20 KiB
TypeScript
Raw Normal View History

2021-12-06 10:23:29 -05:00
import axios from 'axios';
2021-12-05 18:33:49 -05:00
import { Socket } from 'net';
2021-12-06 10:23:29 -05:00
import { Channel } from './Channel.js';
2021-12-05 18:33:49 -05:00
import { IRCUser } from './IRCUser.js';
import { IRCMessage, parseIRCMessage } from './Message.js';
import numerics from './numerics.js';
import { Server } from './Server.js';
export class Client {
user: IRCUser|null
capVersion: string
enabledCaps: Map<string, string>
allCaps: Map<string, string>
localNick: string
localUsername: string
localRealname: string
2021-12-06 10:23:29 -05:00
deviceId: string
2021-12-05 18:33:49 -05:00
constructor(private socket: Socket, public server: Server) {
this.user = null;
this.capVersion = '301';
this.enabledCaps = new Map();
this.allCaps = new Map([
["account-tag", ""],
["batch", ""],
["draft/chathistory", ""],
["draft/channel-rename", ""],
2021-12-05 18:33:49 -05:00
["echo-message", ""],
["draft/event-playback", ""],
2021-12-06 10:23:29 -05:00
["extended-join", ""],
2021-12-05 18:33:49 -05:00
["invite-notify", ""],
["message-tags", ""],
["sasl", "PLAIN"],
["server-time", ""],
]);
this.localNick = 'none';
this.localUsername = 'none';
this.localRealname = 'none';
2021-12-06 10:23:29 -05:00
this.deviceId = "";
2021-12-05 18:33:49 -05:00
this.socket.on('data', (data) => this.receiveData(data));
//this.socket.on('close', (e) => {if (this.user) this.user.handleClientClose(this, e)});
}
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);
});
}
2022-01-24 12:25:30 -05:00
checkIfRegistered(passedTags: Map<string, string> = new Map()) {
if (this.user === null)
this.sendMessage(this.server.name, "451", [this.localNick, "You have not registered"], passedTags);
return this.user !== null;
}
getMatrixUserFromNick(targetNick: string, passedTags: Map<string, string> = new Map()) {
if (!this.user) return false;
const targetMxid = this.server.nickToMxid.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) {
this.sendMessage(this.server.name, "401", [this.user.nick, targetNick, "No such nick"], passedTags);
return false;
}
return target;
}
getChannel(channel: string, passedTags: Map<string, string> = new Map()) {
if (!this.user) return false;
const targetChannel = this.server.ircChannels.get(channel);
if (!targetChannel) {
this.sendMessage(this.server.name, "403", [this.user.nick, channel, "No such channel"], passedTags);
return false;
}
return targetChannel;
}
checkIfInChannel(channel: Channel, passedTags: Map<string, string> = new Map()) {
if (!this.user) return false;
if (!channel.ircUsers.has(this.user.nick)) {
this.sendMessage(this.server.name, "442", [this.user.nick, "You're not on that channel"], passedTags);
return false;
}
return true;
}
checkMinParams(message: IRCMessage, neededNumber: number) {
if (!this.user) return false;
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;
}
2021-12-05 18:33:49 -05:00
routeMessage(data: string) {
const message = parseIRCMessage(data);
switch (message.command.toUpperCase()) {
case 'AUTHENTICATE': {
this.doAUTHENTICATE(message);
break;
}
case 'CAP': {
this.doCAP(message);
break;
}
2022-01-24 10:23:06 -05:00
case 'INVITE': {
2022-01-24 12:25:30 -05:00
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;
}
2022-01-24 10:23:06 -05:00
const reason = (message.params.length === 3) ? message.params[2] : "";
2022-01-24 12:25:30 -05:00
this.user.inviteMatrixUser(this, targetChannel, targetUser, reason, message.tags);
2022-01-24 10:23:06 -05:00
break;
}
case 'KICK': {
2022-01-24 12:25:30 -05:00
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 2))
2022-01-24 10:23:06 -05:00
return;
2022-01-24 12:25:30 -05:00
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;
}
2022-01-24 10:23:06 -05:00
const reason = (message.params.length === 3) ? message.params[2] : "";
2022-01-24 12:25:30 -05:00
this.user.kickMatrixUser(this, targetChannel, targetUser.mxid, reason, message.tags);
2022-01-24 10:23:06 -05:00
break;
}
2021-12-06 10:23:29 -05:00
case 'MODE': {
2022-01-24 12:25:30 -05:00
if (!this.checkIfRegistered(message.tags) || !this.checkMinParams(message, 1) || !this.user)
return;
const targetChannel = this.server.ircChannels.get(message.params[0]);
2022-01-24 12:25:30 -05:00
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);
2022-01-24 12:25:30 -05:00
else
targetChannel.handleModeChange(this, message);
2021-12-06 10:23:29 -05:00
break;
}
case 'NAMES': {
2022-01-24 12:25:30 -05:00
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);
2021-12-06 10:23:29 -05:00
break;
}
2021-12-07 10:36:09 -05:00
case 'NOTICE': {
2022-01-24 12:25:30 -05:00
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);
2021-12-07 10:36:09 -05:00
break;
}
2022-01-24 10:23:06 -05:00
case 'PART': {
2022-01-24 12:25:30 -05:00
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;
2022-01-24 10:23:06 -05:00
const reason = (message.params.length === 2) ? message.params[1] : "";
2022-01-24 12:25:30 -05:00
this.user.partMatrixRoom(this, targetChannel, reason, message.tags);
2022-01-24 10:23:06 -05:00
break;
}
2021-12-05 18:47:52 -05:00
case 'PING': {
this.sendMessage(this.server.name, "PONG", message.params, message.tags);
break;
}
2021-12-07 10:36:09 -05:00
case 'PRIVMSG': {
2022-01-24 12:25:30 -05:00
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);
2021-12-07 10:36:09 -05:00
break;
}
2022-01-24 10:23:06 -05:00
case 'TAGMSG': {
2022-01-24 12:25:30 -05:00
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);
2022-01-24 10:23:06 -05:00
break;
}
case 'TOPIC': {
2022-01-24 12:25:30 -05:00
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;
2022-01-24 10:23:06 -05:00
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;
}
2021-12-06 10:23:29 -05:00
case 'WHO': {
2022-01-24 12:25:30 -05:00
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);
2021-12-06 10:23:29 -05:00
break;
}
2021-12-05 18:33:49 -05:00
}
}
getCapString(capVersion: string) {
let capArray = [];
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(' ');
}
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 => {
2021-12-07 10:37:07 -05:00
if (this.allCaps.has(cap)) {
2021-12-05 18:33:49 -05:00
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");
}
}
}
}
doAUTHENTICATE(message: IRCMessage) {
if (message.params[0] === "PLAIN") {
this.sendMessage("", "AUTHENTICATE", ["+"], message.tags);
}
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', numerics['904']('*'), message.tags)
this.closeConnectionWithError('Invalid authentication')
}
const mxid = authArray[1];
const accessTokenAndServer = authArray[2];
const sepIndex = accessTokenAndServer.indexOf(":");
const accessToken = accessTokenAndServer.slice(0, sepIndex);
const homeserver = accessTokenAndServer.slice(sepIndex+1);
console.log(accessToken, homeserver);
const thisIRCUser = this.server.getOrCreateIRCUser(mxid, accessToken, homeserver);
2021-12-06 10:23:29 -05:00
thisIRCUser.getVerification().then((response) => {
if (response.status === 401 || response.status === 403) {
2021-12-05 18:33:49 -05:00
this.sendMessage(this.server.name, '904', numerics['904']('*'), message.tags)
this.closeConnectionWithError('Invalid authentication')
2021-12-06 10:23:29 -05:00
} else if (response.status === 429) {
this.sendMessage(this.server.name, '904', numerics['904']('*'), message.tags)
this.closeConnectionWithError('rate limited, please try again later')
} else if (response.status !== 200) {
this.sendMessage(this.server.name, '904', numerics['904']('*'), message.tags)
this.closeConnectionWithError('verification failed, please check credentials')
2021-12-05 18:33:49 -05:00
}
2021-12-06 10:23:29 -05:00
this.deviceId = response.data.device_id
if (response.data.user_id !== mxid) {
this.sendMessage(this.server.name, '904', numerics['904']('*'), message.tags)
this.closeConnectionWithError('access token does not match mxid, please check credentials')
} else {
2021-12-05 18:33:49 -05:00
this.user = thisIRCUser;
2021-12-05 18:44:21 -05:00
this.sendMessage(this.server.name, '900', numerics['900'](this.user.getMask(), this.user.nick), new Map());
this.sendMessage(this.server.name, '903', numerics['903'](this.user.nick), new Map());
if (this.user.isSynced()) {
2021-12-06 10:23:29 -05:00
this.user.addClient(this, message.tags);
} else {
axios.get(`https://${this.user.homeserver}/_matrix/client/v3/sync?access_token=${accessToken}`).then(response => {
const data = response.data;
const rooms = data.rooms;
if (this.user === null)
return;
if (rooms['join']) {
const joinedRooms: Set<Channel> = new Set();
for (const roomId of Object.keys(rooms.join)) {
2021-12-07 08:51:31 -05:00
const targetChannel = this.server.getOrCreateIRCChannel(roomId);
joinedRooms.add(targetChannel);
rooms.join[roomId].state.events.forEach((nextEvent: any) => targetChannel.routeMatrixEvent(nextEvent));
}
joinedRooms.forEach(c => {if (this.user !== null) this.user.channels.add(c)});
}
if (this.user === null)
return;
this.user.nextBatch = data.next_batch;
this.sendMessage(this.server.name, 'NOTICE', [this.user.nick, 'You are now synced to the network!'], message.tags);
this.user.addClient(this, 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);
}
})
}
2021-12-05 18:33:49 -05:00
}
2021-12-06 10:23:29 -05:00
})
2021-12-05 18:33:49 -05:00
}
}
doRegistration(message: IRCMessage) {
if (this.user === null) {
2021-12-05 18:44:21 -05:00
this.closeConnectionWithError("You must use SASL to connect to this server");
2021-12-05 18:33:49 -05:00
return;
}
this.sendMessage(this.server.name, '001', numerics['001'](this.user.nick, this.server.name), message.tags);
this.sendMessage(this.server.name, '002', numerics['002'](this.user.nick, this.server.name, '0.0.1'), message.tags);
this.sendMessage(this.server.name, '003', numerics['003'](this.user.nick, 'yesterday'), message.tags);
this.sendMessage(this.server.name, '004', numerics['004'](this.user.nick, this.server.name, '0.0.1', 'i', 'Lhionpsv'), message.tags);
const iSupportArray = [
'CASEMAPPING=ascii',
'CHANMODES=,,,Linps',
'CHANTYPES=#&!',
'MAXTARGETS=1',
'PREFIX=(ohv)@%+',
]
if (this.enabledCaps.has('draft/chathistory')) {
iSupportArray.push('CHATHISTORY=50');
}
this.sendMessage(this.server.name, '005', numerics['005'](this.user.nick, iSupportArray), message.tags);
this.sendMessage(this.server.name, '375', numerics['375'](this.user.nick), message.tags);
this.sendMessage(this.server.name, '372', numerics['372'](this.user.nick, "It's an MOTD"), message.tags);
this.sendMessage(this.server.name, '376', numerics['376'](this.user.nick), message.tags);
this.sendMessage(this.user.nick, 'MODE', [this.user.nick, '+i'], message.tags);
if (!this.user.isSynced())
2021-12-06 10:23:29 -05:00
this.sendMessage(this.server.name, 'NOTICE', [this.user.nick, 'Please wait for initial sync, this may take a while if you are in many large channels'], message.tags);
2021-12-05 18:33:49 -05:00
}
sendMessage(prefix: string, command: string, params: string[], tags: Map<string, string>) {
const capTagMapping = new Map([
['account', 'account-tag'],
['label', 'labeled-response'],
['msgid', 'message-tags'],
['reflectionircd.chat/delete-message', 'reflectionircd.chat/delete-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();
2021-12-06 18:33:32 -05:00
//console.log(`SENT: ${msgToSend}`);
2021-12-05 18:33:49 -05:00
this.socket.write(`${msgToSend}\r\n`);
}
closeConnectionWithError(message: string) {
this.sendMessage(this.server.name, 'ERROR', [message], new Map());
this.socket.destroy();
}
}