✨ User presence/status
This commit is contained in:
parent
116e17f6d0
commit
68053418f3
@ -1,13 +1,46 @@
|
|||||||
import { WebSocket } from "@fosscord/gateway";
|
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) {
|
export async function Close(this: WebSocket, code: number, reason: string) {
|
||||||
console.log("[WebSocket] closed", code, reason);
|
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.heartbeatTimeout) clearTimeout(this.heartbeatTimeout);
|
||||||
if (this.readyTimeout) clearTimeout(this.readyTimeout);
|
if (this.readyTimeout) clearTimeout(this.readyTimeout);
|
||||||
|
|
||||||
this.deflate?.close();
|
this.deflate?.close();
|
||||||
|
|
||||||
this.removeAllListeners();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import { Close } from "./Close";
|
|||||||
import { Message } from "./Message";
|
import { Message } from "./Message";
|
||||||
import { createDeflate } from "zlib";
|
import { createDeflate } from "zlib";
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
import { Session } from "@fosscord/util";
|
|
||||||
var erlpack: any;
|
var erlpack: any;
|
||||||
try {
|
try {
|
||||||
erlpack = require("@yukikaze-bot/erlpack");
|
erlpack = require("@yukikaze-bot/erlpack");
|
||||||
@ -57,6 +56,7 @@ export async function Connection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
socket.events = {};
|
socket.events = {};
|
||||||
|
socket.member_events = {};
|
||||||
socket.permissions = {};
|
socket.permissions = {};
|
||||||
socket.sequence = 0;
|
socket.sequence = 0;
|
||||||
|
|
||||||
|
@ -1,46 +1,55 @@
|
|||||||
import {
|
import {
|
||||||
|
EVENTEnum,
|
||||||
|
EventOpts,
|
||||||
getPermission,
|
getPermission,
|
||||||
|
listenEvent,
|
||||||
Member,
|
Member,
|
||||||
PublicMemberProjection,
|
|
||||||
Role,
|
Role,
|
||||||
} from "@fosscord/util";
|
} from "@fosscord/util";
|
||||||
import { LazyRequest } from "../schema/LazyRequest";
|
import { LazyRequest } from "../schema/LazyRequest";
|
||||||
import { Send } from "../util/Send";
|
import { Send } from "../util/Send";
|
||||||
import { OPCODES } from "../util/Constants";
|
import { OPCODES } from "../util/Constants";
|
||||||
import { WebSocket, Payload } from "@fosscord/gateway";
|
import { WebSocket, Payload, handlePresenceUpdate } from "@fosscord/gateway";
|
||||||
import { check } from "./instanceOf";
|
import { check } from "./instanceOf";
|
||||||
import "missing-native-js-functions";
|
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: config: to list all members (even those who are offline) sorted by role, or just those who are online
|
||||||
// TODO: rewrite typeorm
|
// TODO: rewrite typeorm
|
||||||
|
|
||||||
export async function onLazyRequest(this: WebSocket, { d }: Payload) {
|
async function getMembers(guild_id: string, range: [number, number]) {
|
||||||
// TODO: check data
|
if (!Array.isArray(range) || range.length !== 2) {
|
||||||
check.call(this, LazyRequest, d);
|
throw new Error("range is not a valid array");
|
||||||
const { guild_id, typing, channels, activities } = d as LazyRequest;
|
}
|
||||||
|
// TODO: wait for typeorm to implement ordering for .find queries https://github.com/typeorm/typeorm/issues/2620
|
||||||
|
|
||||||
const permissions = await getPermission(this.user_id, guild_id);
|
let members = await getRepository(Member)
|
||||||
permissions.hasThrow("VIEW_CHANNEL");
|
.createQueryBuilder("member")
|
||||||
|
.where("member.guild_id = :guild_id", { guild_id })
|
||||||
var members = await Member.find({
|
.leftJoinAndSelect("member.roles", "role")
|
||||||
where: { guild_id: guild_id },
|
.leftJoinAndSelect("member.user", "user")
|
||||||
relations: ["roles", "user"],
|
.leftJoinAndSelect("user.sessions", "session")
|
||||||
select: PublicMemberProjection,
|
.addSelect(
|
||||||
});
|
"CASE WHEN session.status = 'offline' THEN 0 ELSE 1 END",
|
||||||
|
"_status"
|
||||||
const roles = await Role.find({
|
)
|
||||||
where: { guild_id: guild_id },
|
.orderBy("role.position", "DESC")
|
||||||
order: {
|
.addOrderBy("_status", "DESC")
|
||||||
position: "DESC",
|
.addOrderBy("user.username", "ASC")
|
||||||
},
|
.offset(Number(range[0]) || 0)
|
||||||
});
|
.limit(Number(range[1]) || 100)
|
||||||
|
.getMany();
|
||||||
|
|
||||||
const groups = [] as any[];
|
const groups = [] as any[];
|
||||||
var member_count = 0;
|
|
||||||
const items = [];
|
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
|
// @ts-ignore
|
||||||
const [role_members, other_members] = partition(members, (m: Member) =>
|
const [role_members, other_members] = partition(members, (m: Member) =>
|
||||||
m.roles.find((r) => r.id === role.id)
|
m.roles.find((r) => r.id === role.id)
|
||||||
@ -54,35 +63,86 @@ export async function onLazyRequest(this: WebSocket, { d }: Payload) {
|
|||||||
groups.push(group);
|
groups.push(group);
|
||||||
|
|
||||||
for (const member of role_members) {
|
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({
|
items.push({
|
||||||
member: {
|
member: {
|
||||||
...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;
|
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, {
|
return Send(this, {
|
||||||
op: OPCODES.Dispatch,
|
op: OPCODES.Dispatch,
|
||||||
s: this.sequence++,
|
s: this.sequence++,
|
||||||
t: "GUILD_MEMBER_LIST_UPDATE",
|
t: "GUILD_MEMBER_LIST_UPDATE",
|
||||||
d: {
|
d: {
|
||||||
ops: [
|
ops: ops.map((x) => ({
|
||||||
{
|
items: x.items,
|
||||||
range: [0, 99],
|
op: "SYNC",
|
||||||
op: "SYNC",
|
range: x.range,
|
||||||
items,
|
})),
|
||||||
},
|
online_count: member_count,
|
||||||
],
|
|
||||||
online_count: member_count, // TODO count online count
|
|
||||||
member_count,
|
member_count,
|
||||||
id: "everyone",
|
id: "everyone",
|
||||||
guild_id,
|
guild_id,
|
||||||
groups,
|
groups: ops
|
||||||
|
.map((x) => x.groups)
|
||||||
|
.flat()
|
||||||
|
.unique(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,25 @@
|
|||||||
import { WebSocket, Payload } from "@fosscord/gateway";
|
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) {
|
export async function onPresenceUpdate(this: WebSocket, { d }: Payload) {
|
||||||
// return this.close(CLOSECODES.Unknown_error);
|
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);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export interface LazyRequest {
|
export interface LazyRequest {
|
||||||
guild_id: string;
|
guild_id: string;
|
||||||
channels?: Record<string, [number, number]>;
|
channels?: Record<string, [number, number][]>;
|
||||||
activities?: boolean;
|
activities?: boolean;
|
||||||
threads?: boolean;
|
threads?: boolean;
|
||||||
typing?: true;
|
typing?: true;
|
||||||
|
@ -17,4 +17,6 @@ export interface WebSocket extends WS {
|
|||||||
sequence: number;
|
sequence: number;
|
||||||
permissions: Record<string, Permissions>;
|
permissions: Record<string, Permissions>;
|
||||||
events: Record<string, Function>;
|
events: Record<string, Function>;
|
||||||
|
member_events: Record<string, Function>;
|
||||||
|
listen_options: any;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
/* Strict Type-Checking Options */
|
/* 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. */,
|
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||||
"strictNullChecks": true /* Enable strict null checks. */,
|
"strictNullChecks": true /* Enable strict null checks. */,
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
@ -26,6 +26,22 @@ import { BaseClassWithoutId } from "./BaseClass";
|
|||||||
import { Ban, PublicGuildRelations } from ".";
|
import { Ban, PublicGuildRelations } from ".";
|
||||||
import { DiscordApiErrors } from "../util/Constants";
|
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")
|
@Entity("members")
|
||||||
@Index(["id", "guild_id"], { unique: true })
|
@Index(["id", "guild_id"], { unique: true })
|
||||||
export class Member extends BaseClassWithoutId {
|
export class Member extends BaseClassWithoutId {
|
||||||
@ -81,7 +97,7 @@ export class Member extends BaseClassWithoutId {
|
|||||||
@Column()
|
@Column()
|
||||||
pending: boolean;
|
pending: boolean;
|
||||||
|
|
||||||
@Column({ type: "simple-json" })
|
@Column({ type: "simple-json", select: false })
|
||||||
settings: UserGuildSettings;
|
settings: UserGuildSettings;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { User } from "./User";
|
import { User } from "./User";
|
||||||
import { BaseClass } from "./BaseClass";
|
import { BaseClass } from "./BaseClass";
|
||||||
import { Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
|
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
|
//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;
|
user: User;
|
||||||
|
|
||||||
//TODO check, should be 32 char long hex string
|
//TODO check, should be 32 char long hex string
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false, select: false })
|
||||||
session_id: string;
|
session_id: string;
|
||||||
|
|
||||||
activities: []; //TODO
|
@Column({ type: "simple-json", nullable: true })
|
||||||
|
activities: Activity[] = [];
|
||||||
|
|
||||||
|
// TODO client_status
|
||||||
@Column({ type: "simple-json", select: false })
|
@Column({ type: "simple-json", select: false })
|
||||||
client_info: {
|
client_info: {
|
||||||
client: string;
|
client: string;
|
||||||
@ -29,6 +33,14 @@ export class Session extends BaseClass {
|
|||||||
version: number;
|
version: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false, type: "varchar" })
|
||||||
status: string; //TODO enum
|
status: Status; //TODO enum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PrivateSessionProjection: (keyof Session)[] = [
|
||||||
|
"user_id",
|
||||||
|
"session_id",
|
||||||
|
"activities",
|
||||||
|
"client_info",
|
||||||
|
"status",
|
||||||
|
];
|
||||||
|
Loading…
x
Reference in New Issue
Block a user