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",
+];
|