Newer
Older
percord / gateway / src / opcodes / LazyRequest.ts
import { getPermission, listenEvent, Member, Role, Session } from "@fosscord/util";
import { LazyRequest } from "../schema/LazyRequest";
import { Send } from "../util/Send";
import { OPCODES } from "../util/Constants";
import { WebSocket, Payload, handlePresenceUpdate } from "@fosscord/gateway";
import { check } from "./instanceOf";
import "missing-native-js-functions";
import { getRepository } from "typeorm";
import "missing-native-js-functions";

// TODO: only show roles/members that have access to this channel
// TODO: config: to list all members (even those who are offline) sorted by role, or just those who are online
// TODO: rewrite typeorm

async function getMembers(guild_id: string, range: [number, number]) {
	if (!Array.isArray(range) || range.length !== 2) {
		throw new Error("range is not a valid array");
	}
	// TODO: wait for typeorm to implement ordering for .find queries https://github.com/typeorm/typeorm/issues/2620

	let members: Member[] = [];
	try {
		members = await getRepository(Member)
			.createQueryBuilder("member")
			.where("member.guild_id = :guild_id", { guild_id })
			.leftJoinAndSelect("member.roles", "role")
			.leftJoinAndSelect("member.user", "user")
			.leftJoinAndSelect("user.sessions", "session")
			.addSelect("user.settings")
			.addSelect(
				"CASE WHEN session.status = 'offline' THEN 0 ELSE 1 END",
				"_status"
			)
			.orderBy("role.position", "DESC")
			.addOrderBy("_status", "DESC")
			.addOrderBy("user.username", "ASC")
			.skip(Number(range[0]) || 0)
			.take(Number(range[1]) || 100)
			.getMany();
	}
	catch (e) {
		console.error(`LazyRequest`, e);
	}

	if (!members) {
		return {
			items: [],
			groups: [],
			range: [],
			members: [],
		};
	}

	const groups = [] as any[];
	const items = [];
	const member_roles = members
		.map((m) => m.roles)
		.flat()
		.unique((r: Role) => r.id);
	member_roles.push(member_roles.splice(member_roles.findIndex(x => x.id === x.guild_id), 1)[0]);

	const offlineItems = [];

	for (const role of member_roles) {
		// @ts-ignore
		const [role_members, other_members]: Member[][] = partition(members, (m: Member) =>
			m.roles.find((r) => r.id === role.id)
		);
		const group = {
			count: role_members.length,
			id: role.id === guild_id ? "online" : role.id,
		};

		items.push({ group });
		groups.push(group);

		for (const member of role_members) {
			const roles = member.roles
				.filter((x: Role) => x.id !== guild_id)
				.map((x: Role) => x.id);

			const statusMap = {
				"online": 0,
				"idle": 1,
				"dnd": 2,
				"invisible": 3,
				"offline": 4,
			};
			// sort sessions by relevance
			const sessions = member.user.sessions.sort((a, b) => {
				return (statusMap[a.status] - statusMap[b.status]) + ((a.activities.length - b.activities.length) * 2);
			});
			var session: Session | undefined = sessions.first();

			if (session?.status == "offline") {
				session.status = member.user.settings.status || "online";
			}

			const item = {
				member: {
					...member,
					roles,
					user: { ...member.user, sessions: undefined },
					presence: {
						...session,
						activities: session?.activities || [],
						user: { id: member.user.id },
					},
				},
			};

			if (!session || session.status == "invisible" || session.status == "offline") {
				item.member.presence.status = "offline";
				offlineItems.push(item);
				group.count--;
				continue;
			}

			items.push(item);
		}
		members = other_members;
	}

	if (offlineItems.length) {
		const group = {
			count: offlineItems.length,
			id: "offline",
		};
		items.push({ group });
		groups.push(group);

		items.push(...offlineItems);
	}

	return {
		items,
		groups,
		range,
		members: items.map((x) => 'member' in x ? x.member : undefined).filter(x => !!x),
	};
}

export async function onLazyRequest(this: WebSocket, { d }: Payload) {
	// TODO: check data
	check.call(this, LazyRequest, d);
	const { guild_id, typing, channels, activities } = d as LazyRequest;

	const channel_id = Object.keys(channels || {}).first();
	if (!channel_id) return;

	const permissions = await getPermission(this.user_id, guild_id, channel_id);
	permissions.hasThrow("VIEW_CHANNEL");

	const ranges = channels![channel_id];
	if (!Array.isArray(ranges)) throw new Error("Not a valid Array");

	const member_count = await Member.count({ where: { guild_id } });
	const ops = await Promise.all(ranges.map((x) => getMembers(guild_id, x)));

	// TODO: unsubscribe member_events that are not in op.members

	ops.forEach((op) => {
		op.members.forEach(async (member) => {
			if (!member) return;
			if (this.events[member.user.id]) return; // already subscribed as friend
			if (this.member_events[member.user.id]) return; // already subscribed in member list
			this.member_events[member.user.id] = await listenEvent(
				member.user.id,
				handlePresenceUpdate.bind(this),
				this.listen_options
			);
		});
	});

	const groups = ops
		.map((x) => x.groups)
		.flat()
		.unique();

	return await Send(this, {
		op: OPCODES.Dispatch,
		s: this.sequence++,
		t: "GUILD_MEMBER_LIST_UPDATE",
		d: {
			ops: ops.map((x) => ({
				items: x.items,
				op: "SYNC",
				range: x.range,
			})),
			online_count: member_count - (groups.find(x => x.id == "offline")?.count ?? 0),
			member_count,
			id: "everyone",
			guild_id,
			groups,
		},
	});
}

function partition<T>(array: T[], isValid: Function) {
	// @ts-ignore
	return array.reduce(
		// @ts-ignore
		([pass, fail], elem) => {
			return isValid(elem)
				? [[...pass, elem], fail]
				: [pass, [...fail, elem]];
		},
		[[], []]
	);
}