summary refs log tree commit diff
path: root/src/gateway/listener/listener.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/gateway/listener/listener.ts')
-rw-r--r--src/gateway/listener/listener.ts252
1 files changed, 252 insertions, 0 deletions
diff --git a/src/gateway/listener/listener.ts b/src/gateway/listener/listener.ts
new file mode 100644
index 00000000..72dd9d5b
--- /dev/null
+++ b/src/gateway/listener/listener.ts
@@ -0,0 +1,252 @@
+import {
+	getPermission,
+	Permissions,
+	RabbitMQ,
+	listenEvent,
+	EventOpts,
+	ListenEventOpts,
+	Member,
+	EVENTEnum,
+	Relationship,
+	RelationshipType,
+} from "@fosscord/util";
+import { OPCODES } from "../util/Constants";
+import { Send } from "../util/Send";
+import { WebSocket } from "@fosscord/gateway";
+import "missing-native-js-functions";
+import { Channel as AMQChannel } from "amqplib";
+import { Recipient } from "@fosscord/util";
+
+// TODO: close connection on Invalidated Token
+// TODO: check intent
+// TODO: Guild Member Update is sent for current-user updates regardless of whether the GUILD_MEMBERS intent is set.
+
+// Sharding: calculate if the current shard id matches the formula: shard_id = (guild_id >> 22) % num_shards
+// https://discord.com/developers/docs/topics/gateway#sharding
+
+export function handlePresenceUpdate(
+	this: WebSocket,
+	{ event, acknowledge, data }: EventOpts
+) {
+	acknowledge?.();
+	if (event === EVENTEnum.PresenceUpdate) {
+		return Send(this, {
+			op: OPCODES.Dispatch,
+			t: event,
+			d: data,
+			s: this.sequence++,
+		});
+	}
+}
+
+// TODO: use already queried guilds/channels of Identify and don't fetch them again
+export async function setupListener(this: WebSocket) {
+	const [members, recipients, relationships] = await Promise.all([
+		Member.find({
+			where: { id: this.user_id },
+			relations: ["guild", "guild.channels"],
+		}),
+		Recipient.find({
+			where: { user_id: this.user_id, closed: false },
+			relations: ["channel"],
+		}),
+		Relationship.find({
+			where: {
+				from_id: this.user_id,
+				type: RelationshipType.friends,
+			}
+		}),
+	]);
+
+	const guilds = members.map((x) => x.guild);
+	const dm_channels = recipients.map((x) => x.channel);
+
+	const opts: { acknowledge: boolean; channel?: AMQChannel; } = {
+		acknowledge: true,
+	};
+	this.listen_options = opts;
+	const consumer = consume.bind(this);
+
+	if (RabbitMQ.connection) {
+		opts.channel = await RabbitMQ.connection.createChannel();
+		// @ts-ignore
+		opts.channel.queues = {};
+	}
+
+	this.events[this.user_id] = await listenEvent(this.user_id, consumer, opts);
+
+	relationships.forEach(async (relationship) => {
+		this.events[relationship.to_id] = await listenEvent(
+			relationship.to_id,
+			handlePresenceUpdate.bind(this),
+			opts
+		);
+	});
+
+	dm_channels.forEach(async (channel) => {
+		this.events[channel.id] = await listenEvent(channel.id, consumer, opts);
+	});
+
+	guilds.forEach(async (guild) => {
+		const permission = await getPermission(this.user_id, guild.id);
+		this.permissions[guild.id] = permission;
+		this.events[guild.id] = await listenEvent(guild.id, consumer, opts);
+
+		guild.channels.forEach(async (channel) => {
+			if (
+				permission
+					.overwriteChannel(channel.permission_overwrites!)
+					.has("VIEW_CHANNEL")
+			) {
+				this.events[channel.id] = await listenEvent(
+					channel.id,
+					consumer,
+					opts
+				);
+			}
+		});
+	});
+
+	this.once("close", () => {
+		if (opts.channel) opts.channel.close();
+		else {
+			Object.values(this.events).forEach((x) => x());
+			Object.values(this.member_events).forEach((x) => x());
+		}
+	});
+}
+
+// TODO: only subscribe for events that are in the connection intents
+async function consume(this: WebSocket, opts: EventOpts) {
+	const { data, event } = opts;
+	let id = data.id as string;
+	const permission = this.permissions[id] || new Permissions("ADMINISTRATOR"); // default permission for dm
+
+	const consumer = consume.bind(this);
+	const listenOpts = opts as ListenEventOpts;
+	opts.acknowledge?.();
+	// console.log("event", event);
+
+	// subscription managment
+	switch (event) {
+		case "GUILD_MEMBER_REMOVE":
+			this.member_events[data.user.id]?.();
+			delete this.member_events[data.user.id];
+		case "GUILD_MEMBER_ADD":
+			if (this.member_events[data.user.id]) break; // already subscribed
+			this.member_events[data.user.id] = await listenEvent(
+				data.user.id,
+				handlePresenceUpdate.bind(this),
+				this.listen_options
+			);
+			break;
+		case "GUILD_MEMBER_REMOVE":
+			if (!this.member_events[data.user.id]) break;
+			this.member_events[data.user.id]();
+			break;
+		case "RELATIONSHIP_REMOVE":
+		case "CHANNEL_DELETE":
+		case "GUILD_DELETE":
+			delete this.events[id];
+			opts.cancel();
+			break;
+		case "CHANNEL_CREATE":
+			if (
+				!permission
+					.overwriteChannel(data.permission_overwrites)
+					.has("VIEW_CHANNEL")
+			) {
+				return;
+			}
+			this.events[id] = await listenEvent(id, consumer, listenOpts);
+			break;
+		case "RELATIONSHIP_ADD":
+			this.events[data.user.id] = await listenEvent(
+				data.user.id,
+				handlePresenceUpdate.bind(this),
+				this.listen_options
+			);
+			break;
+		case "GUILD_CREATE":
+			this.events[id] = await listenEvent(id, consumer, listenOpts);
+			break;
+		case "CHANNEL_UPDATE":
+			const exists = this.events[id];
+			// @ts-ignore
+			if (
+				permission
+					.overwriteChannel(data.permission_overwrites)
+					.has("VIEW_CHANNEL")
+			) {
+				if (exists) break;
+				this.events[id] = await listenEvent(id, consumer, listenOpts);
+			} else {
+				if (!exists) return; // return -> do not send channel update events for hidden channels
+				opts.cancel(id);
+				delete this.events[id];
+			}
+			break;
+	}
+
+	// permission checking
+	switch (event) {
+		case "INVITE_CREATE":
+		case "INVITE_DELETE":
+		case "GUILD_INTEGRATIONS_UPDATE":
+			if (!permission.has("MANAGE_GUILD")) return;
+			break;
+		case "WEBHOOKS_UPDATE":
+			if (!permission.has("MANAGE_WEBHOOKS")) return;
+			break;
+		case "GUILD_MEMBER_ADD":
+		case "GUILD_MEMBER_REMOVE":
+		case "GUILD_MEMBER_UPDATE":
+		// only send them, if the user subscribed for this part of the member list, or is a bot
+		case "PRESENCE_UPDATE": // exception if user is friend
+			break;
+		case "GUILD_BAN_ADD":
+		case "GUILD_BAN_REMOVE":
+			if (!permission.has("BAN_MEMBERS")) return;
+			break;
+		case "VOICE_STATE_UPDATE":
+		case "MESSAGE_CREATE":
+		case "MESSAGE_DELETE":
+		case "MESSAGE_DELETE_BULK":
+		case "MESSAGE_UPDATE":
+		case "CHANNEL_PINS_UPDATE":
+		case "MESSAGE_REACTION_ADD":
+		case "MESSAGE_REACTION_REMOVE":
+		case "MESSAGE_REACTION_REMOVE_ALL":
+		case "MESSAGE_REACTION_REMOVE_EMOJI":
+		case "TYPING_START":
+			// only gets send if the user is alowed to view the current channel
+			if (!permission.has("VIEW_CHANNEL")) return;
+			break;
+		case "GUILD_CREATE":
+		case "GUILD_DELETE":
+		case "GUILD_UPDATE":
+		case "GUILD_ROLE_CREATE":
+		case "GUILD_ROLE_UPDATE":
+		case "GUILD_ROLE_DELETE":
+		case "CHANNEL_CREATE":
+		case "CHANNEL_DELETE":
+		case "CHANNEL_UPDATE":
+		case "GUILD_EMOJIS_UPDATE":
+		case "READY": // will be sent by the gateway
+		case "USER_UPDATE":
+		case "APPLICATION_COMMAND_CREATE":
+		case "APPLICATION_COMMAND_DELETE":
+		case "APPLICATION_COMMAND_UPDATE":
+		default:
+			// always gets sent
+			// Any events not defined in an intent are considered "passthrough" and will always be sent
+			break;
+	}
+
+	await Send(this, {
+		op: OPCODES.Dispatch,
+		t: event,
+		d: data,
+		s: this.sequence++,
+	});
+}