/*
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 .
*/
import { OPCODES, Payload, Send, WebSocket } from "@spacebar/gateway";
import { Member, RequestGuildMembersSchema, getDatabase } from "@spacebar/util";
import { check } from "./instanceOf";
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 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,
},
});
}
}