summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--gateway/src/events/Close.ts41
-rw-r--r--gateway/src/events/Connection.ts2
-rw-r--r--gateway/src/opcodes/LazyRequest.ts132
-rw-r--r--gateway/src/opcodes/PresenceUpdate.ts24
-rw-r--r--gateway/src/schema/LazyRequest.ts2
-rw-r--r--gateway/src/util/WebSocket.ts2
-rw-r--r--gateway/tsconfig.json2
-rw-r--r--util/src/entities/Member.ts18
-rw-r--r--util/src/entities/Session.ts20
9 files changed, 193 insertions, 50 deletions
diff --git a/gateway/src/events/Close.ts b/gateway/src/events/Close.ts
index 5c1bd292..5b7c512c 100644
--- a/gateway/src/events/Close.ts
+++ b/gateway/src/events/Close.ts
@@ -1,13 +1,46 @@
 import { WebSocket } from "@fosscord/gateway";
-import { Session } from "@fosscord/util";
+import {
+	emitEvent,
+	PresenceUpdateEvent,
+	PrivateSessionProjection,
+	Session,
+	SessionsReplace,
+	User,
+} from "@fosscord/util";
 
 export async function Close(this: WebSocket, code: number, reason: string) {
 	console.log("[WebSocket] closed", code, reason);
-	if (this.session_id) await Session.delete({ session_id: this.session_id });
 	if (this.heartbeatTimeout) clearTimeout(this.heartbeatTimeout);
 	if (this.readyTimeout) clearTimeout(this.readyTimeout);
-
 	this.deflate?.close();
-
 	this.removeAllListeners();
+
+	if (this.session_id) {
+		await Session.delete({ session_id: this.session_id });
+		const sessions = await Session.find({
+			where: { user_id: this.user_id },
+			select: PrivateSessionProjection,
+		});
+		await emitEvent({
+			event: "SESSIONS_REPLACE",
+			user_id: this.user_id,
+			data: sessions,
+		} as SessionsReplace);
+		const session = sessions.first() || {
+			activities: [],
+			client_info: {},
+			status: "offline",
+		};
+
+		await emitEvent({
+			event: "PRESENCE_UPDATE",
+			user_id: this.user_id,
+			data: {
+				user: await User.getPublicUser(this.user_id),
+				activities: session.activities,
+				client_status: session?.client_info,
+				status: session.status,
+			},
+		} as PresenceUpdateEvent);
+	}
 }
diff --git a/gateway/src/events/Connection.ts b/gateway/src/events/Connection.ts
index 9bb034f0..4954cd08 100644
--- a/gateway/src/events/Connection.ts
+++ b/gateway/src/events/Connection.ts
@@ -8,7 +8,6 @@ import { Close } from "./Close";
 import { Message } from "./Message";
 import { createDeflate } from "zlib";
 import { URL } from "url";
-import { Session } from "@fosscord/util";
 var erlpack: any;
 try {
 	erlpack = require("@yukikaze-bot/erlpack");
@@ -57,6 +56,7 @@ export async function Connection(
 		}
 
 		socket.events = {};
+		socket.member_events = {};
 		socket.permissions = {};
 		socket.sequence = 0;
 
diff --git a/gateway/src/opcodes/LazyRequest.ts b/gateway/src/opcodes/LazyRequest.ts
index f5fd561a..c304dfe7 100644
--- a/gateway/src/opcodes/LazyRequest.ts
+++ b/gateway/src/opcodes/LazyRequest.ts
@@ -1,46 +1,55 @@
 import {
+	EVENTEnum,
+	EventOpts,
 	getPermission,
+	listenEvent,
 	Member,
-	PublicMemberProjection,
 	Role,
 } from "@fosscord/util";
 import { LazyRequest } from "../schema/LazyRequest";
 import { Send } from "../util/Send";
 import { OPCODES } from "../util/Constants";
-import { WebSocket, Payload } from "@fosscord/gateway";
+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: check permission and only show roles/members that have access to this channel
+// 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
 
-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 permissions = await getPermission(this.user_id, guild_id);
-	permissions.hasThrow("VIEW_CHANNEL");
-
-	var members = await Member.find({
-		where: { guild_id: guild_id },
-		relations: ["roles", "user"],
-		select: PublicMemberProjection,
-	});
+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
 
-	const roles = await Role.find({
-		where: { guild_id: guild_id },
-		order: {
-			position: "DESC",
-		},
-	});
+	let 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(
+			"CASE WHEN session.status = 'offline' THEN 0 ELSE 1 END",
+			"_status"
+		)
+		.orderBy("role.position", "DESC")
+		.addOrderBy("_status", "DESC")
+		.addOrderBy("user.username", "ASC")
+		.offset(Number(range[0]) || 0)
+		.limit(Number(range[1]) || 100)
+		.getMany();
 
 	const groups = [] as any[];
-	var member_count = 0;
 	const items = [];
+	const member_roles = members
+		.map((m) => m.roles)
+		.flat()
+		.unique((r) => r.id);
 
-	for (const role of roles) {
+	for (const role of member_roles) {
 		// @ts-ignore
 		const [role_members, other_members] = partition(members, (m: Member) =>
 			m.roles.find((r) => r.id === role.id)
@@ -54,35 +63,86 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
 		groups.push(group);
 
 		for (const member of role_members) {
-			member.roles = member.roles.filter((x: Role) => x.id !== guild_id);
+			const roles = member.roles
+				.filter((x: Role) => x.id !== guild_id)
+				.map((x: Role) => x.id);
+
+			const session = member.user.sessions.first();
+
+			// TODO: properly mock/hide offline/invisible status
 			items.push({
 				member: {
 					...member,
-					roles: member.roles.map((x: Role) => x.id),
+					roles,
+					user: { ...member.user, sessions: undefined },
+					presence: {
+						...session,
+						activities: session?.activities || [],
+						user: { id: member.user.id },
+					},
 				},
 			});
 		}
 		members = other_members;
-		member_count += role_members.length;
 	}
 
+	return {
+		items,
+		groups,
+		range,
+		members: items.map((x) => x.member).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({ 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 (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
+			);
+		});
+	});
+
 	return Send(this, {
 		op: OPCODES.Dispatch,
 		s: this.sequence++,
 		t: "GUILD_MEMBER_LIST_UPDATE",
 		d: {
-			ops: [
-				{
-					range: [0, 99],
-					op: "SYNC",
-					items,
-				},
-			],
-			online_count: member_count, // TODO count online count
+			ops: ops.map((x) => ({
+				items: x.items,
+				op: "SYNC",
+				range: x.range,
+			})),
+			online_count: member_count,
 			member_count,
 			id: "everyone",
 			guild_id,
-			groups,
+			groups: ops
+				.map((x) => x.groups)
+				.flat()
+				.unique(),
 		},
 	});
 }
diff --git a/gateway/src/opcodes/PresenceUpdate.ts b/gateway/src/opcodes/PresenceUpdate.ts
index 53d7b9d2..415df6ee 100644
--- a/gateway/src/opcodes/PresenceUpdate.ts
+++ b/gateway/src/opcodes/PresenceUpdate.ts
@@ -1,5 +1,25 @@
 import { WebSocket, Payload } from "@fosscord/gateway";
+import { emitEvent, PresenceUpdateEvent, Session, User } from "@fosscord/util";
+import { ActivitySchema } from "../schema/Activity";
+import { check } from "./instanceOf";
 
-export function onPresenceUpdate(this: WebSocket, data: Payload) {
-	// return this.close(CLOSECODES.Unknown_error);
+export async function onPresenceUpdate(this: WebSocket, { d }: Payload) {
+	check.call(this, ActivitySchema, d);
+	const presence = d as ActivitySchema;
+
+	await Session.update(
+		{ session_id: this.session_id },
+		{ status: presence.status, activities: presence.activities }
+	);
+
+	await emitEvent({
+		event: "PRESENCE_UPDATE",
+		user_id: this.user_id,
+		data: {
+			user: await User.getPublicUser(this.user_id),
+			activities: presence.activities,
+			client_status: {}, // TODO:
+			status: presence.status,
+		},
+	} as PresenceUpdateEvent);
 }
diff --git a/gateway/src/schema/LazyRequest.ts b/gateway/src/schema/LazyRequest.ts
index 7c828ac6..1fe658bb 100644
--- a/gateway/src/schema/LazyRequest.ts
+++ b/gateway/src/schema/LazyRequest.ts
@@ -1,6 +1,6 @@
 export interface LazyRequest {
 	guild_id: string;
-	channels?: Record<string, [number, number]>;
+	channels?: Record<string, [number, number][]>;
 	activities?: boolean;
 	threads?: boolean;
 	typing?: true;
diff --git a/gateway/src/util/WebSocket.ts b/gateway/src/util/WebSocket.ts
index 49626b2a..e3313f40 100644
--- a/gateway/src/util/WebSocket.ts
+++ b/gateway/src/util/WebSocket.ts
@@ -17,4 +17,6 @@ export interface WebSocket extends WS {
 	sequence: number;
 	permissions: Record<string, Permissions>;
 	events: Record<string, Function>;
+	member_events: Record<string, Function>;
+	listen_options: any;
 }
diff --git a/gateway/tsconfig.json b/gateway/tsconfig.json
index 2ad38f93..b6ae9455 100644
--- a/gateway/tsconfig.json
+++ b/gateway/tsconfig.json
@@ -27,7 +27,7 @@
 		// "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
 
 		/* Strict Type-Checking Options */
-		"strict": false /* Enable all strict type-checking options. */,
+		"strict": true /* Enable all strict type-checking options. */,
 		"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
 		"strictNullChecks": true /* Enable strict null checks. */,
 		// "strictFunctionTypes": true,           /* Enable strict checking of function types. */
diff --git a/util/src/entities/Member.ts b/util/src/entities/Member.ts
index 12b0b49a..0f7be2a7 100644
--- a/util/src/entities/Member.ts
+++ b/util/src/entities/Member.ts
@@ -26,6 +26,22 @@ import { BaseClassWithoutId } from "./BaseClass";
 import { Ban, PublicGuildRelations } from ".";
 import { DiscordApiErrors } from "../util/Constants";
 
+export const MemberPrivateProjection: (keyof Member)[] = [
+	"id",
+	"guild",
+	"guild_id",
+	"deaf",
+	"joined_at",
+	"last_message_id",
+	"mute",
+	"nick",
+	"pending",
+	"premium_since",
+	"roles",
+	"settings",
+	"user",
+];
+
 @Entity("members")
 @Index(["id", "guild_id"], { unique: true })
 export class Member extends BaseClassWithoutId {
@@ -81,7 +97,7 @@ export class Member extends BaseClassWithoutId {
 	@Column()
 	pending: boolean;
 
-	@Column({ type: "simple-json" })
+	@Column({ type: "simple-json", select: false })
 	settings: UserGuildSettings;
 
 	@Column({ nullable: true })
diff --git a/util/src/entities/Session.ts b/util/src/entities/Session.ts
index 7cc325f5..ac5313f1 100644
--- a/util/src/entities/Session.ts
+++ b/util/src/entities/Session.ts
@@ -1,6 +1,8 @@
 import { User } from "./User";
 import { BaseClass } from "./BaseClass";
 import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
+import { Status } from "../interfaces/Status";
+import { Activity } from "../interfaces/Activity";
 
 //TODO we need to remove all sessions on server start because if the server crashes without closing websockets it won't delete them
 
@@ -17,11 +19,13 @@ export class Session extends BaseClass {
 	user: User;
 
 	//TODO check, should be 32 char long hex string
-	@Column({ nullable: false })
+	@Column({ nullable: false, select: false })
 	session_id: string;
 
-	activities: []; //TODO
+	@Column({ type: "simple-json", nullable: true })
+	activities: Activity[] = [];
 
+	// TODO client_status
 	@Column({ type: "simple-json", select: false })
 	client_info: {
 		client: string;
@@ -29,6 +33,14 @@ export class Session extends BaseClass {
 		version: number;
 	};
 
-	@Column({ nullable: false })
-	status: string; //TODO enum
+	@Column({ nullable: false, type: "varchar" })
+	status: Status; //TODO enum
 }
+
+export const PrivateSessionProjection: (keyof Session)[] = [
+	"user_id",
+	"session_id",
+	"activities",
+	"client_info",
+	"status",
+];