diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts
index 330ce561..26a625f7 100644
--- a/src/gateway/opcodes/Identify.ts
+++ b/src/gateway/opcodes/Identify.ts
@@ -82,6 +82,7 @@ export async function onIdentify(this: WebSocket, data: Payload) {
const identify: IdentifySchema = data.d;
this.capabilities = new Capabilities(identify.capabilities || 0);
+ this.large_threshold = identify.large_threshold || 250;
const user = await tryGetUserFromToken(identify.token, {
relations: ["relationships", "relationships.to", "settings"],
diff --git a/src/gateway/opcodes/RequestGuildMembers.ts b/src/gateway/opcodes/RequestGuildMembers.ts
index 304d4b39..13b8488d 100644
--- a/src/gateway/opcodes/RequestGuildMembers.ts
+++ b/src/gateway/opcodes/RequestGuildMembers.ts
@@ -16,8 +16,128 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-import { WebSocket } from "@spacebar/gateway";
+import { OPCODES, Payload, Send, WebSocket } from "@spacebar/gateway";
+import { Member, RequestGuildMembersSchema, getDatabase } from "@spacebar/util";
+import { check } from "./instanceOf";
-export function onRequestGuildMembers(this: WebSocket) {
- // return this.close(CLOSECODES.Unknown_error);
+function partition(members: Member[], size: number) {
+ const chunks = [];
+
+ for (let i = 0; i < members.length; i += size) {
+ chunks.push(members.slice(i, i + size));
+ }
+
+ return chunks;
+}
+
+export async function onRequestGuildMembers(this: WebSocket, { d }: Payload) {
+ check.call(this, RequestGuildMembersSchema, d);
+
+ const { guild_id, query, limit, presences, user_ids, nonce } =
+ d as RequestGuildMembersSchema;
+
+ if (query && user_ids) {
+ throw new Error("Cannot query and provide user ids");
+ }
+
+ if (query && !limit) {
+ throw new Error("Must provide limit when querying");
+ }
+
+ const takeLimit =
+ (query && query !== "") || user_ids
+ ? limit && limit !== 0 && limit <= 1000
+ ? limit
+ : 100
+ : undefined;
+
+ const member_count = await Member.count({
+ where: {
+ guild_id,
+ },
+ });
+
+ // TODO: if member count is >75k, only return members in voice plus the connecting users member object
+ // TODO: if member count is >large_threshold, send members who are online, have a role, have a nickname, or are in a voice channel
+ // TODO: if member count is <large_threshold, send all members
+
+ let members: Member[] = [];
+
+ if (member_count > 75000) {
+ // since we dont have voice channels yet, just return the connecting users member object
+ members = await Member.find({
+ where: {
+ guild_id,
+ user: {
+ id: this.user_id,
+ },
+ },
+ relations: ["user", "roles"],
+ });
+ } else if (member_count > this.large_threshold) {
+ try {
+ // find all members who are online, have a role, have a nickname, or are in a voice channel, as well as respecting the query and user_ids
+ const db = getDatabase();
+ if (!db) throw new Error("Database not initialized");
+ const repo = db.getRepository(Member);
+ const q = repo
+ .createQueryBuilder("member")
+ .where("member.guild_id = :guild_id", { guild_id })
+ .leftJoinAndSelect("member.roles", "role")
+ .leftJoinAndSelect("member.user", "user")
+ .leftJoinAndSelect("user.sessions", "session")
+ .andWhere(
+ `',' || member.roles || ',' NOT LIKE :everyoneRoleIdList`,
+ { everyoneRoleIdList: `%,${guild_id},%` },
+ )
+ .andWhere("session.status != 'offline'")
+ .addOrderBy("user.username", "ASC")
+ .limit(takeLimit);
+
+ if (query && query !== "") {
+ q.andWhere(`user.username ILIKE :query`, {
+ query: `${query}%`,
+ });
+ } else if (user_ids) {
+ q.andWhere(`user.id IN (:...user_ids)`, { user_ids });
+ }
+
+ members = await q.getMany();
+ } catch (e) {
+ console.error(`request guild members`, e);
+ }
+ } else {
+ members = await Member.find({
+ where: {
+ guild_id,
+ ...(user_ids && { user_id: user_ids }),
+ ...(query && { username: { startsWith: query } }),
+ },
+ take: takeLimit,
+ relations: ["user", "roles"],
+ });
+ }
+
+ const chunks = partition(members, 1000);
+
+ for (const [i, chunk] of chunks.entries()) {
+ await Send(this, {
+ op: OPCODES.Dispatch,
+ s: this.sequence++,
+ t: "GUILD_MEMBERS_CHUNK",
+ d: {
+ guild_id,
+ members: chunk.map((member) => ({
+ ...member,
+ roles: member.roles.map((role) => role.id),
+ user: member.user.toPublicUser(),
+ })),
+ chunk_index: i + 1,
+ chunk_count: chunks.length,
+ // not_found: []
+ // presences: []
+ nonce,
+ },
+ });
+ }
}
diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts
index 833756ff..7869bed0 100644
--- a/src/gateway/util/WebSocket.ts
+++ b/src/gateway/util/WebSocket.ts
@@ -17,8 +17,8 @@
*/
import { Intents, ListenEventOpts, Permissions } from "@spacebar/util";
-import WS from "ws";
import { Deflate, Inflate } from "fast-zlib";
+import WS from "ws";
import { Capabilities } from "./Capabilities";
// import { Client } from "@spacebar/webrtc";
@@ -43,4 +43,5 @@ export interface WebSocket extends WS {
listen_options: ListenEventOpts;
capabilities?: Capabilities;
// client?: Client;
+ large_threshold: number;
}
diff --git a/src/util/schemas/RequestGuildMembersSchema.ts b/src/util/schemas/RequestGuildMembersSchema.ts
new file mode 100644
index 00000000..89f91e21
--- /dev/null
+++ b/src/util/schemas/RequestGuildMembersSchema.ts
@@ -0,0 +1,35 @@
+/*
+ Spacebar: A FOSS re-implementation and extension of the Discord.com backend.
+ Copyright (C) 2023 Spacebar and Spacebar Contributors
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+*/
+
+export interface RequestGuildMembersSchema {
+ guild_id: string;
+ query?: string;
+ limit?: number;
+ presences?: boolean;
+ user_ids?: string[];
+ nonce?: string;
+}
+
+export const RequestGuildMembersSchema = {
+ guild_id: String,
+ $query: String,
+ $limit: Number,
+ $presences: Boolean,
+ $user_ids: [] as string[],
+ $nonce: String,
+};
diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts
index 44a504cd..98216b2f 100644
--- a/src/util/schemas/index.ts
+++ b/src/util/schemas/index.ts
@@ -58,6 +58,7 @@ export * from "./PurgeSchema";
export * from "./RegisterSchema";
export * from "./RelationshipPostSchema";
export * from "./RelationshipPutSchema";
+export * from "./RequestGuildMembersSchema";
export * from "./RoleModifySchema";
export * from "./RolePositionUpdateSchema";
export * from "./SelectProtocolSchema";
|