mirror of
https://github.com/stoatchat/javascript-client-sdk.git
synced 2026-07-20 01:23:33 -04:00
feat: handle all events from current protocol
This commit is contained in:
+121
-72
@@ -1,9 +1,10 @@
|
||||
import { Accessor, Setter, createSignal } from "solid-js";
|
||||
|
||||
import { API, Metadata } from "revolt-api";
|
||||
import EventEmitter from "eventemitter3";
|
||||
import { API, Metadata, RelationshipStatus, Role } from "revolt-api";
|
||||
import type { DataLogin, RevoltConfig } from "revolt-api";
|
||||
|
||||
import { User } from "./classes";
|
||||
import { Channel, Emoji, Message, Server, ServerMember, User } from "./classes";
|
||||
import {
|
||||
ChannelCollection,
|
||||
EmojiCollection,
|
||||
@@ -12,14 +13,89 @@ import {
|
||||
ServerMemberCollection,
|
||||
UserCollection,
|
||||
} from "./collections";
|
||||
import { EventClient, createEventClient } from "./events/client";
|
||||
import { ConnectionState, EventClient } from "./events/client";
|
||||
import { handleEvent } from "./events/v1";
|
||||
import {
|
||||
HydratedChannel,
|
||||
HydratedEmoji,
|
||||
HydratedMessage,
|
||||
HydratedServer,
|
||||
HydratedServerMember,
|
||||
HydratedUser,
|
||||
} from "./hydration";
|
||||
|
||||
export type Session = { token: string; user_id: string } | string;
|
||||
export type Session = { _id: string; token: string; user_id: string } | string;
|
||||
|
||||
/**
|
||||
* Revolt.js Client
|
||||
* Events provided by the client
|
||||
*/
|
||||
export class Client {
|
||||
type Events = {
|
||||
error(error: Error): void;
|
||||
|
||||
connected(): void;
|
||||
connecting(): void;
|
||||
disconnected(): void;
|
||||
ready(): void;
|
||||
logout(): void;
|
||||
|
||||
messageCreate(message: Message): void;
|
||||
messageUpdate(message: Message, previousMessage: HydratedMessage): void;
|
||||
messageDelete(message: HydratedMessage): void;
|
||||
messageDeleteBulk(messages: HydratedMessage[], channel?: Channel): void;
|
||||
messageReactionAdd(message: Message, userId: string, emoji: string): void;
|
||||
messageReactionRemove(message: Message, userId: string, emoji: string): void;
|
||||
messageReactionRemoveEmoji(message: Message, emoji: string): void;
|
||||
|
||||
channelCreate(channel: Channel): void;
|
||||
channelUpdate(channel: Channel, previousChannel: HydratedChannel): void;
|
||||
channelDelete(channel: HydratedChannel): void;
|
||||
channelGroupJoin(channel: Channel, user: User): void;
|
||||
channelGroupLeave(channel: Channel, user?: User): void;
|
||||
channelStartTyping(channel: Channel, user?: User): void;
|
||||
channelStopTyping(channel: Channel, user?: User): void;
|
||||
channelAcknowledged(channel: Channel, messageId: string): void;
|
||||
|
||||
serverCreate(server: Server): void;
|
||||
serverUpdate(server: Server, previousServer: HydratedServer): void;
|
||||
serverDelete(server: HydratedServer): void;
|
||||
serverRoleUpdate(server: Server, roleId: string, previousRole: Role): void;
|
||||
serverRoleDelete(server: Server, roleId: string, role: Role): void;
|
||||
|
||||
serverMemberUpdate(
|
||||
member: ServerMember,
|
||||
previousMember: HydratedServerMember
|
||||
): void;
|
||||
serverMemberJoin(member: ServerMember): void;
|
||||
serverMemberLeave(member: HydratedServerMember): void;
|
||||
|
||||
userUpdate(user: User, previousUser: HydratedUser): void;
|
||||
// ^ userRelationshipChanged(user: User, previousRelationship: RelationshipStatus): void;
|
||||
// ^ userPresenceChanged(user: User, previousPresence: boolean): void;
|
||||
userSettingsUpdate(
|
||||
id: string,
|
||||
update: Record<string, [number, string]>
|
||||
): void;
|
||||
|
||||
emojiCreate(emoji: Emoji): void;
|
||||
emojiDelete(emoji: HydratedEmoji): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Client options object
|
||||
*/
|
||||
export interface ClientOptions {
|
||||
baseURL: string;
|
||||
|
||||
/**
|
||||
* Whether to allow partial objects to emit from events.
|
||||
*/
|
||||
partials: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revolt.js Clients
|
||||
*/
|
||||
export class Client extends EventEmitter<Events> {
|
||||
readonly channels;
|
||||
readonly emojis;
|
||||
readonly messages;
|
||||
@@ -28,7 +104,7 @@ export class Client {
|
||||
readonly serverMembers;
|
||||
|
||||
readonly api: API;
|
||||
readonly baseURL: string;
|
||||
readonly options: ClientOptions;
|
||||
readonly events: EventClient<1>;
|
||||
|
||||
configuration: RevoltConfig | undefined;
|
||||
@@ -41,13 +117,18 @@ export class Client {
|
||||
/**
|
||||
* Create Revolt.js Client
|
||||
*/
|
||||
constructor(baseURL?: string) {
|
||||
this.baseURL = baseURL ?? "https://api.revolt.chat";
|
||||
this.api = new API({
|
||||
baseURL,
|
||||
});
|
||||
constructor(options?: Partial<ClientOptions>) {
|
||||
super();
|
||||
|
||||
this.events = createEventClient(1);
|
||||
this.options = {
|
||||
baseURL: "https://api.revolt.chat",
|
||||
partials: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
this.api = new API({
|
||||
baseURL: this.options.baseURL,
|
||||
});
|
||||
|
||||
const [ready, setReady] = createSignal(false);
|
||||
this.ready = ready;
|
||||
@@ -59,67 +140,35 @@ export class Client {
|
||||
this.users = new UserCollection(this);
|
||||
this.servers = new ServerCollection(this);
|
||||
this.serverMembers = new ServerMemberCollection(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect
|
||||
*/
|
||||
connect() {
|
||||
this.events.on("event", (event) => {
|
||||
console.info("[EVENT]", JSON.stringify(event).substring(0, 32));
|
||||
if (event.type === "Ready") {
|
||||
console.time("load users");
|
||||
for (const user of event.users) {
|
||||
const u = this.users.getOrCreate(user._id, user);
|
||||
|
||||
if (u.relationship === "User") {
|
||||
this.user = u;
|
||||
}
|
||||
}
|
||||
console.timeEnd("load users");
|
||||
console.time("load servers");
|
||||
for (const server of event.servers) {
|
||||
this.servers.getOrCreate(server._id, server);
|
||||
}
|
||||
console.timeEnd("load servers");
|
||||
console.time("load memberships");
|
||||
for (const member of event.members) {
|
||||
this.serverMembers.getOrCreate(member._id, member);
|
||||
}
|
||||
console.timeEnd("load memberships");
|
||||
console.time("load channels");
|
||||
for (const channel of event.channels) {
|
||||
this.channels.getOrCreate(channel._id, channel);
|
||||
}
|
||||
console.timeEnd("load channels");
|
||||
console.time("load emojis");
|
||||
for (const emoji of event.emojis) {
|
||||
this.emojis.getOrCreate(emoji._id, emoji);
|
||||
}
|
||||
console.timeEnd("load emojis");
|
||||
|
||||
const lounge = this.servers.get("01F7ZSBSFHQ8TA81725KQCSDDP")!;
|
||||
console.info(
|
||||
`The owner of ${lounge.name} is ${lounge.owner!.username}!`
|
||||
);
|
||||
console.log(lounge.owner);
|
||||
console.log(
|
||||
"It has the channels:",
|
||||
lounge.channels.map((channel) => channel.name)
|
||||
);
|
||||
/*console.log(
|
||||
"They joined at:",
|
||||
this.serverMembers.get({ server: lounge.id, user: lounge.owner!.id })
|
||||
?.joinedAt
|
||||
);*/
|
||||
|
||||
this.#setReady(true);
|
||||
this.events = new EventClient(1);
|
||||
this.events.on("error", (error) => this.emit("error", error));
|
||||
this.events.on("state", (state) => {
|
||||
console.info("[state]", state);
|
||||
switch (state) {
|
||||
case ConnectionState.Connected:
|
||||
this.emit("connected");
|
||||
break;
|
||||
case ConnectionState.Connecting:
|
||||
this.emit("connecting");
|
||||
break;
|
||||
case ConnectionState.Disconnected:
|
||||
this.emit("disconnected");
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
this.events.on("state", (state) => console.info("STATE =", state));
|
||||
this.events.on("error", (error) => console.error("ERROR =", error));
|
||||
this.events.on("event", (event) =>
|
||||
handleEvent(this, event, this.#setReady)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to Revolt
|
||||
*/
|
||||
connect() {
|
||||
this.events.disconnect();
|
||||
this.#setReady(false);
|
||||
this.events.connect(
|
||||
"wss://ws.revolt.chat",
|
||||
typeof this.session === "string" ? this.session : this.session!.token
|
||||
@@ -140,7 +189,7 @@ export class Client {
|
||||
*/
|
||||
#updateHeaders() {
|
||||
(this.api as API) = new API({
|
||||
baseURL: this.baseURL,
|
||||
baseURL: this.options.baseURL,
|
||||
authentication: {
|
||||
revolt: this.session,
|
||||
},
|
||||
@@ -187,14 +236,14 @@ export class Client {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a URL to a given file with given options.
|
||||
* Creates a URL to a given file with given options.
|
||||
* @param attachment Partial of attachment object
|
||||
* @param options Optional query parameters to modify object
|
||||
* @param allowAnimation Returns GIF if applicable, no operations occur on image
|
||||
* @param fallback Fallback URL
|
||||
* @returns Generated URL or nothing
|
||||
*/
|
||||
generateFileURL(
|
||||
createFileURL(
|
||||
attachment?: {
|
||||
tag: string;
|
||||
_id: string;
|
||||
|
||||
+40
-25
@@ -1,16 +1,17 @@
|
||||
import type {
|
||||
Channel as ApiChannel,
|
||||
Member as ApiMember,
|
||||
Message as ApiMessage,
|
||||
User as ApiUser,
|
||||
DataEditChannel,
|
||||
DataMessageSend,
|
||||
Member,
|
||||
Message,
|
||||
OptionsMessageSearch,
|
||||
Override,
|
||||
User,
|
||||
} from "revolt-api";
|
||||
import { APIRoutes } from "revolt-api/dist/routes";
|
||||
import { decodeTime, ulid } from "ulid";
|
||||
|
||||
import { Message } from "..";
|
||||
import { ChannelCollection } from "../collections";
|
||||
import { bitwiseAndEq, calculatePermission } from "../permissions/calculator";
|
||||
import { Permission } from "../permissions/definitions";
|
||||
@@ -93,7 +94,23 @@ export class Channel {
|
||||
}
|
||||
|
||||
/**
|
||||
* User Ids of recipients of the group
|
||||
* User ids of people currently typing in channel
|
||||
*/
|
||||
get typingIds() {
|
||||
return this.collection.getUnderlyingObject(this.id).recipientIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Users currently trying in channel
|
||||
*/
|
||||
get typing() {
|
||||
return [
|
||||
...this.collection.getUnderlyingObject(this.id).typingIds.values(),
|
||||
].map((id) => this.collection.client.users.get(id)!);
|
||||
}
|
||||
|
||||
/**
|
||||
* User ids of recipients of the group
|
||||
*/
|
||||
get recipientIds() {
|
||||
return this.collection.getUnderlyingObject(this.id).recipientIds;
|
||||
@@ -103,9 +120,9 @@ export class Channel {
|
||||
* Recipients of the group
|
||||
*/
|
||||
get recipients() {
|
||||
return this.collection
|
||||
.getUnderlyingObject(this.id)
|
||||
.recipientIds.map((id) => this.collection.client.users.get(id)!);
|
||||
return [
|
||||
...this.collection.getUnderlyingObject(this.id).recipientIds.values(),
|
||||
].map((id) => this.collection.client.users.get(id)!);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,7 +249,7 @@ export class Channel {
|
||||
* URL to the channel icon
|
||||
*/
|
||||
get iconURL() {
|
||||
return this.collection.client.generateFileURL(
|
||||
return this.collection.client.createFileURL(
|
||||
this.icon ?? this.recipient?.avatar,
|
||||
{
|
||||
max_side: 256,
|
||||
@@ -244,7 +261,7 @@ export class Channel {
|
||||
* URL to a small variant of the channel icon
|
||||
*/
|
||||
get smallIconURL() {
|
||||
return this.collection.client.generateFileURL(
|
||||
return this.collection.client.createFileURL(
|
||||
this.icon ?? this.recipient?.avatar,
|
||||
{
|
||||
max_side: 64,
|
||||
@@ -256,7 +273,7 @@ export class Channel {
|
||||
* URL to the animated channel icon
|
||||
*/
|
||||
get animatedIconURL() {
|
||||
return this.collection.client.generateFileURL(
|
||||
return this.collection.client.createFileURL(
|
||||
this.icon ?? this.recipient?.avatar,
|
||||
{ max_side: 256 },
|
||||
true
|
||||
@@ -322,13 +339,6 @@ export class Channel {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.type === "TextChannel" || this.type === "VoiceChannel") {
|
||||
const server = this.server;
|
||||
if (server) {
|
||||
server.channelIds.delete(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
this.collection.client.channels.delete(this.id);
|
||||
}
|
||||
|
||||
@@ -373,7 +383,11 @@ export class Channel {
|
||||
}
|
||||
);
|
||||
|
||||
return this.collection.client.messages.getOrCreate(message._id, message);
|
||||
return this.collection.client.messages.getOrCreate(
|
||||
message._id,
|
||||
message,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -406,7 +420,7 @@ export class Channel {
|
||||
const messages = (await this.collection.client.api.get(
|
||||
`/channels/${this.id as ""}/messages`,
|
||||
{ ...params }
|
||||
)) as Message[];
|
||||
)) as ApiMessage[];
|
||||
|
||||
return messages.map((message) =>
|
||||
this.collection.client.messages.getOrCreate(message._id, message)
|
||||
@@ -430,7 +444,7 @@ export class Channel {
|
||||
const data = (await this.collection.client.api.get(
|
||||
`/channels/${this.id as ""}/messages`,
|
||||
{ ...params, include_users: true }
|
||||
)) as { messages: Message[]; users: User[]; members?: Member[] };
|
||||
)) as { messages: ApiMessage[]; users: ApiUser[]; members?: ApiMember[] };
|
||||
|
||||
return {
|
||||
messages: data.messages.map((message) =>
|
||||
@@ -454,7 +468,7 @@ export class Channel {
|
||||
const messages = (await this.collection.client.api.post(
|
||||
`/channels/${this.id as ""}/search`,
|
||||
params
|
||||
)) as Message[];
|
||||
)) as ApiMessage[];
|
||||
|
||||
return messages.map((message) =>
|
||||
this.collection.client.messages.getOrCreate(message._id, message)
|
||||
@@ -473,7 +487,7 @@ export class Channel {
|
||||
...params,
|
||||
include_users: true,
|
||||
}
|
||||
)) as { messages: Message[]; users: User[]; members?: Member[] };
|
||||
)) as { messages: ApiMessage[]; users: ApiUser[]; members?: ApiMember[] };
|
||||
|
||||
return {
|
||||
messages: data.messages.map((message) =>
|
||||
@@ -517,16 +531,17 @@ export class Channel {
|
||||
*/
|
||||
async ack(message?: Message | string, skipRateLimiter?: boolean) {
|
||||
const id =
|
||||
(typeof message === "string" ? message : message?._id) ??
|
||||
(typeof message === "string" ? message : message?.id) ??
|
||||
this.lastMessageId ??
|
||||
ulid();
|
||||
|
||||
const performAck = () => {
|
||||
this.#ackLimit = undefined;
|
||||
this.collection.client.api.put(`/channels/${this.id}/ack/${id as ""}`);
|
||||
};
|
||||
|
||||
/* TODO: if (!this.collection.client.options.ackRateLimiter || skipRateLimiter)
|
||||
return performAck();*/
|
||||
// TODO: !this.collection.client.options.ackRateLimiter
|
||||
if (skipRateLimiter) return performAck();
|
||||
|
||||
clearTimeout(this.#ackTimeout);
|
||||
if (this.#ackLimit && +new Date() > this.#ackLimit) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DataEditMessage, DataMessageSend } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { MessageCollection } from "../collections";
|
||||
@@ -247,4 +248,82 @@ export class Message {
|
||||
return { type, from: get(system.from), to: get(system.to) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a message
|
||||
* @param data Message edit route data
|
||||
*/
|
||||
async edit(data: DataEditMessage) {
|
||||
return await this.collection.client.api.patch(
|
||||
`/channels/${this.channelId as ""}/messages/${this.id as ""}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a message
|
||||
*/
|
||||
async delete() {
|
||||
return await this.collection.client.api.delete(
|
||||
`/channels/${this.channelId as ""}/messages/${this.id as ""}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledge this message as read
|
||||
*/
|
||||
ack() {
|
||||
this.channel?.ack(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to Message
|
||||
*/
|
||||
reply(
|
||||
data:
|
||||
| string
|
||||
| (Omit<DataMessageSend, "nonce"> & {
|
||||
nonce?: string;
|
||||
}),
|
||||
mention = true
|
||||
) {
|
||||
const obj = typeof data === "string" ? { content: data } : data;
|
||||
return this.channel?.sendMessage({
|
||||
...obj,
|
||||
replies: [{ id: this.id, mention }],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all reactions from this message
|
||||
*/
|
||||
async clearReactions() {
|
||||
return await this.collection.client.api.delete(
|
||||
`/channels/${this.channelId as ""}/messages/${this.id as ""}/reactions`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* React to a message
|
||||
* @param emoji Unicode or emoji ID
|
||||
*/
|
||||
async react(emoji: string) {
|
||||
return await this.collection.client.api.put(
|
||||
`/channels/${this.channelId as ""}/messages/${this.id as ""}/reactions/${
|
||||
emoji as ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unreact from a message
|
||||
* @param emoji Unicode or emoji ID
|
||||
*/
|
||||
async unreact(emoji: string) {
|
||||
return await this.collection.client.api.delete(
|
||||
`/channels/${this.channelId as ""}/messages/${this.id as ""}/reactions/${
|
||||
emoji as ""
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Server as ApiServer, Category } from "revolt-api";
|
||||
import type { Category } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { ServerCollection } from "../collections";
|
||||
@@ -237,14 +237,14 @@ export class Server {
|
||||
* URL to the server's icon
|
||||
*/
|
||||
get iconURL() {
|
||||
return this.collection.client.generateFileURL(this.icon, { max_side: 256 });
|
||||
return this.collection.client.createFileURL(this.icon, { max_side: 256 });
|
||||
}
|
||||
|
||||
/**
|
||||
* URL to the server's animated icon
|
||||
*/
|
||||
get animatedIconURL() {
|
||||
return this.collection.client.generateFileURL(
|
||||
return this.collection.client.createFileURL(
|
||||
this.icon,
|
||||
{ max_side: 256 },
|
||||
true
|
||||
@@ -255,7 +255,7 @@ export class Server {
|
||||
* URL to the server's banner
|
||||
*/
|
||||
get bannerURL() {
|
||||
return this.collection.client.generateFileURL(this.banner, {
|
||||
return this.collection.client.createFileURL(this.banner, {
|
||||
max_side: 256,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ export class ServerMember {
|
||||
*/
|
||||
get avatarURL() {
|
||||
return (
|
||||
this.collection.client.generateFileURL(this.avatar, { max_side: 256 }) ??
|
||||
this.collection.client.createFileURL(this.avatar, { max_side: 256 }) ??
|
||||
this.user?.avatarURL
|
||||
);
|
||||
}
|
||||
@@ -189,7 +189,7 @@ export class ServerMember {
|
||||
*/
|
||||
get animatedAvatarURL() {
|
||||
return (
|
||||
this.collection.client.generateFileURL(
|
||||
this.collection.client.createFileURL(
|
||||
this.avatar,
|
||||
{ max_side: 256 },
|
||||
true
|
||||
|
||||
+2
-2
@@ -102,7 +102,7 @@ export class User {
|
||||
*/
|
||||
get avatarURL() {
|
||||
return (
|
||||
this.collection.client.generateFileURL(this.avatar, { max_side: 256 }) ??
|
||||
this.collection.client.createFileURL(this.avatar, { max_side: 256 }) ??
|
||||
this.defaultAvatarURL
|
||||
);
|
||||
}
|
||||
@@ -112,7 +112,7 @@ export class User {
|
||||
*/
|
||||
get animatedAvatarURL() {
|
||||
return (
|
||||
this.collection.client.generateFileURL(
|
||||
this.collection.client.createFileURL(
|
||||
this.avatar,
|
||||
{ max_side: 256 },
|
||||
true
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SetStoreFunction } from "solid-js/store";
|
||||
|
||||
import { ReactiveMap } from "@solid-primitives/map";
|
||||
|
||||
import { Hydrators, hydrate } from "../hydration";
|
||||
import { Hydrators } from "../hydration";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
/**
|
||||
@@ -127,6 +127,15 @@ export abstract class StoreCollection<T, V> extends Collection<T> {
|
||||
this.#objects.set(id, instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether an object is partially defined
|
||||
* @param id Id
|
||||
* @returns Whether it is a partial
|
||||
*/
|
||||
isPartial(id: string): boolean {
|
||||
return !!(this.getUnderlyingObject(id) as { partial: boolean }).partial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of stored objects
|
||||
* @returns Size
|
||||
|
||||
+131
-17
@@ -33,12 +33,22 @@ export class ChannelCollection extends ClassCollection<
|
||||
Channel,
|
||||
HydratedChannel
|
||||
> {
|
||||
/**
|
||||
* Delete an object
|
||||
* @param id Id
|
||||
*/
|
||||
override delete(id: string): void {
|
||||
let channel = this.get(id);
|
||||
channel?.server?.channelIds.delete(id);
|
||||
super.delete(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch channel by ID
|
||||
* @param id Id
|
||||
* @returns Channel
|
||||
*/
|
||||
async fetch(id: string): Promise<Channel | undefined> {
|
||||
async fetch(id: string): Promise<Channel> {
|
||||
const channel = this.get(id);
|
||||
if (channel) return channel;
|
||||
const data = await this.client.api.get(`/channels/${id as ""}`);
|
||||
@@ -49,13 +59,32 @@ export class ChannelCollection extends ClassCollection<
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @param isNew Whether this object is new
|
||||
*/
|
||||
getOrCreate(id: string, data: API.Channel) {
|
||||
getOrCreate(id: string, data: API.Channel, isNew = false) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new Channel(this, id);
|
||||
this.create(id, "channel", instance, data);
|
||||
isNew && this.client.emit("channelCreate", instance);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: string) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new Channel(this, id);
|
||||
this.create(id, "channel", instance, {
|
||||
id,
|
||||
partial: true,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +96,7 @@ export class EmojiCollection extends ClassCollection<Emoji, HydratedEmoji> {
|
||||
* @param id Id
|
||||
* @returns Emoji
|
||||
*/
|
||||
async fetch(id: string): Promise<Emoji | undefined> {
|
||||
async fetch(id: string): Promise<Emoji> {
|
||||
const emoji = this.get(id);
|
||||
if (emoji) return emoji;
|
||||
const data = await this.client.api.get(`/custom/emoji/${id as ""}`);
|
||||
@@ -78,13 +107,31 @@ export class EmojiCollection extends ClassCollection<Emoji, HydratedEmoji> {
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @param isNew Whether this object is new
|
||||
*/
|
||||
getOrCreate(id: string, data: API.Emoji) {
|
||||
getOrCreate(id: string, data: API.Emoji, isNew = false) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new Emoji(this, id);
|
||||
this.create(id, "emoji", instance, data);
|
||||
isNew && this.client.emit("emojiCreate", instance);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: string) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new Emoji(this, id);
|
||||
this.create(id, "emoji", instance, {
|
||||
id,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -100,10 +147,7 @@ export class MessageCollection extends ClassCollection<
|
||||
* @param messageId Message Id
|
||||
* @returns Message
|
||||
*/
|
||||
async fetch(
|
||||
channelId: string,
|
||||
messageId: string
|
||||
): Promise<Message | undefined> {
|
||||
async fetch(channelId: string, messageId: string): Promise<Message> {
|
||||
const message = this.get(messageId);
|
||||
if (message) return message;
|
||||
|
||||
@@ -111,20 +155,39 @@ export class MessageCollection extends ClassCollection<
|
||||
`/channels/${channelId as ""}/messages/${messageId as ""}`
|
||||
);
|
||||
|
||||
return this.getOrCreate(data._id, data);
|
||||
return this.getOrCreate(data._id, data, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @param isNew Whether this object is new
|
||||
*/
|
||||
getOrCreate(id: string, data: API.Message) {
|
||||
getOrCreate(id: string, data: API.Message, isNew = false) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new Message(this, id);
|
||||
this.create(id, "message", instance, data);
|
||||
isNew && this.client.emit("messageCreate", instance);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: string) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new Message(this, id);
|
||||
this.create(id, "message", instance, {
|
||||
id,
|
||||
partial: true,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -136,7 +199,7 @@ export class ServerCollection extends ClassCollection<Server, HydratedServer> {
|
||||
* @param id Id
|
||||
* @returns Server
|
||||
*/
|
||||
async fetch(id: string): Promise<Server | undefined> {
|
||||
async fetch(id: string): Promise<Server> {
|
||||
const server = this.get(id);
|
||||
if (server) return server;
|
||||
const data = await this.client.api.get(`/servers/${id as ""}`);
|
||||
@@ -147,13 +210,32 @@ export class ServerCollection extends ClassCollection<Server, HydratedServer> {
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @param isNew Whether this object is new
|
||||
*/
|
||||
getOrCreate(id: string, data: API.Server) {
|
||||
getOrCreate(id: string, data: API.Server, isNew = false) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else {
|
||||
const instance = new Server(this, id);
|
||||
this.create(id, "server", instance, data);
|
||||
isNew && this.client.emit("serverCreate", instance);
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: string) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new Server(this, id);
|
||||
this.create(id, "server", instance, {
|
||||
id,
|
||||
partial: true,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -165,7 +247,7 @@ export class UserCollection extends ClassCollection<User, HydratedUser> {
|
||||
* @param id Id
|
||||
* @returns User
|
||||
*/
|
||||
async fetch(id: string): Promise<User | undefined> {
|
||||
async fetch(id: string): Promise<User> {
|
||||
const user = this.get(id);
|
||||
if (user) return user;
|
||||
const data = await this.client.api.get(`/users/${id as ""}`);
|
||||
@@ -176,6 +258,7 @@ export class UserCollection extends ClassCollection<User, HydratedUser> {
|
||||
* Get or create
|
||||
* @param id Id
|
||||
* @param data Data
|
||||
* @param isNew Whether this object is new
|
||||
*/
|
||||
getOrCreate(id: string, data: API.User) {
|
||||
if (this.has(id)) {
|
||||
@@ -186,6 +269,23 @@ export class UserCollection extends ClassCollection<User, HydratedUser> {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: string) {
|
||||
if (this.has(id)) {
|
||||
return this.get(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new User(this, id);
|
||||
this.create(id, "user", instance, {
|
||||
id,
|
||||
partial: true,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ServerMemberCollection extends ClassCollection<
|
||||
@@ -216,10 +316,7 @@ export class ServerMemberCollection extends ClassCollection<
|
||||
* @param userId User Id
|
||||
* @returns Message
|
||||
*/
|
||||
async fetch(
|
||||
serverId: string,
|
||||
userId: string
|
||||
): Promise<ServerMember | undefined> {
|
||||
async fetch(serverId: string, userId: string): Promise<ServerMember> {
|
||||
const member = this.get(userId);
|
||||
if (member) return member;
|
||||
|
||||
@@ -244,4 +341,21 @@ export class ServerMemberCollection extends ClassCollection<
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or return partial
|
||||
* @param id Id
|
||||
*/
|
||||
getOrPartial(id: API.MemberCompositeKey) {
|
||||
if (this.hasByKey(id)) {
|
||||
return this.getByKey(id)!;
|
||||
} else if (this.client.options.partials) {
|
||||
const instance = new ServerMember(this, id);
|
||||
this.create(id.server + id.user, "serverMember", instance, {
|
||||
id,
|
||||
partial: true,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-43
@@ -2,7 +2,6 @@ import { Accessor, Setter, createSignal } from "solid-js";
|
||||
|
||||
import EventEmitter from "eventemitter3";
|
||||
import WebSocket from "isomorphic-ws";
|
||||
import type TypedEmitter from "typed-emitter";
|
||||
|
||||
import type { AvailableProtocols, EventProtocol } from ".";
|
||||
|
||||
@@ -28,10 +27,9 @@ type Events<T extends AvailableProtocols, P extends EventProtocol<T>> = {
|
||||
/**
|
||||
* Simple wrapper around the Revolt websocket service.
|
||||
*/
|
||||
class Client<
|
||||
T extends AvailableProtocols,
|
||||
P extends EventProtocol<T>
|
||||
> extends EventEmitter {
|
||||
export class EventClient<T extends AvailableProtocols> extends EventEmitter<
|
||||
Events<T, EventProtocol<T>>
|
||||
> {
|
||||
#protocolVersion: T;
|
||||
#transportFormat: "json" | "msgpack";
|
||||
#heartbeatInterval: number;
|
||||
@@ -78,7 +76,6 @@ class Client<
|
||||
private setState(state: ConnectionState) {
|
||||
this.#setStateSetter(state);
|
||||
this.emit("state", state);
|
||||
// TODO: debug weird state changes
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,7 +104,7 @@ class Client<
|
||||
};
|
||||
|
||||
this.#socket.onerror = (error) => {
|
||||
this.emit("error", error);
|
||||
this.emit("error", error as never);
|
||||
};
|
||||
|
||||
this.#socket.onmessage = (event) => {
|
||||
@@ -129,14 +126,16 @@ class Client<
|
||||
*/
|
||||
disconnect() {
|
||||
if (!this.#socket) return;
|
||||
this.setState(ConnectionState.Disconnected);
|
||||
this.#socket.close();
|
||||
this.#socket = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an event to the server.
|
||||
* @param event Event
|
||||
*/
|
||||
send(event: P["client"]) {
|
||||
send(event: EventProtocol<T>["client"]) {
|
||||
console.info(event);
|
||||
}
|
||||
|
||||
@@ -144,7 +143,7 @@ class Client<
|
||||
* Handle events intended for client before passing them along.
|
||||
* @param event Event
|
||||
*/
|
||||
handle(event: P["server"]) {
|
||||
handle(event: EventProtocol<T>["server"]) {
|
||||
switch (event.type) {
|
||||
case "Ping":
|
||||
this.send({
|
||||
@@ -156,7 +155,7 @@ class Client<
|
||||
clearTimeout(this.#pongTimeoutReference);
|
||||
return;
|
||||
case "Error":
|
||||
this.emit("error", event);
|
||||
this.emit("error", event as never);
|
||||
this.disconnect();
|
||||
return;
|
||||
}
|
||||
@@ -186,36 +185,3 @@ class Client<
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event client
|
||||
*/
|
||||
export type EventClient<T extends AvailableProtocols> = Omit<
|
||||
Client<T, EventProtocol<T>>,
|
||||
"on" | "once"
|
||||
> &
|
||||
TypedEmitter<Events<T, EventProtocol<T>>>;
|
||||
|
||||
/**
|
||||
* Create a new event client.
|
||||
* @param protocolVersion Target protocol version
|
||||
* @param transportFormat Communication format
|
||||
* @param heartbeatInterval Interval in seconds to send ping
|
||||
* @param pongTimeout Time in seconds until heartbeat times out
|
||||
*/
|
||||
export function createEventClient<
|
||||
T extends AvailableProtocols,
|
||||
P extends EventProtocol<T>
|
||||
>(
|
||||
protocolVersion: T,
|
||||
transportFormat?: "json",
|
||||
heartbeatInterval?: number,
|
||||
pongTimeout?: number
|
||||
) {
|
||||
return new Client<T, P>(
|
||||
protocolVersion,
|
||||
transportFormat,
|
||||
heartbeatInterval,
|
||||
pongTimeout
|
||||
) as never as EventClient<T>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Setter } from "solid-js";
|
||||
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
import type {
|
||||
Channel,
|
||||
Embed,
|
||||
Emoji,
|
||||
FieldsChannel,
|
||||
FieldsMember,
|
||||
@@ -14,6 +18,9 @@ import type {
|
||||
User,
|
||||
} from "revolt-api";
|
||||
|
||||
import { Client } from "..";
|
||||
import { Merge } from "../lib/merge";
|
||||
|
||||
/**
|
||||
* Version 1 of the events protocol
|
||||
*/
|
||||
@@ -182,3 +189,592 @@ type WebSocketError = {
|
||||
| "OnboardingNotFinished"
|
||||
| "AlreadyAuthenticated";
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle an event for the Client
|
||||
* @param client Client
|
||||
* @param event Event
|
||||
* @param setReady Signal state change
|
||||
*/
|
||||
export async function handleEvent(
|
||||
client: Client,
|
||||
event: ServerMessage,
|
||||
setReady: Setter<boolean>
|
||||
) {
|
||||
console.info("[EVENT]", JSON.stringify(event).substring(0, 64));
|
||||
switch (event.type) {
|
||||
case "Bulk": {
|
||||
for (const item of event.v) {
|
||||
handleEvent(client, item, setReady);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Ready": {
|
||||
for (const user of event.users) {
|
||||
const u = client.users.getOrCreate(user._id, user);
|
||||
|
||||
if (u.relationship === "User") {
|
||||
client.user = u;
|
||||
}
|
||||
}
|
||||
|
||||
for (const server of event.servers) {
|
||||
client.servers.getOrCreate(server._id, server);
|
||||
}
|
||||
|
||||
for (const member of event.members) {
|
||||
client.serverMembers.getOrCreate(member._id, member);
|
||||
}
|
||||
|
||||
for (const channel of event.channels) {
|
||||
client.channels.getOrCreate(channel._id, channel);
|
||||
}
|
||||
|
||||
for (const emoji of event.emojis) {
|
||||
client.emojis.getOrCreate(emoji._id, emoji);
|
||||
}
|
||||
|
||||
setReady(true);
|
||||
client.emit("ready");
|
||||
break;
|
||||
}
|
||||
case "Message": {
|
||||
if (!client.messages.has(event._id)) {
|
||||
client.messages.getOrCreate(event._id, event, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageUpdate": {
|
||||
const message = client.messages.getOrPartial(event.id);
|
||||
if (message) {
|
||||
const previousMessage = {
|
||||
...client.messages.getUnderlyingObject(event.id),
|
||||
};
|
||||
|
||||
client.messages.updateUnderlyingObject(event.id, {
|
||||
...(event.data as {}),
|
||||
editedAt: new Date(),
|
||||
});
|
||||
|
||||
client.emit("messageUpdate", message, previousMessage);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageAppend": {
|
||||
const message = client.messages.getOrPartial(event.id);
|
||||
if (message) {
|
||||
const previousMessage = {
|
||||
...client.messages.getUnderlyingObject(event.id),
|
||||
};
|
||||
|
||||
client.messages.updateUnderlyingObject(
|
||||
event.id,
|
||||
"embeds",
|
||||
(embeds) => [...(embeds ?? []), event.append.embeds ?? []] as Embed[]
|
||||
);
|
||||
|
||||
client.emit("messageUpdate", message, previousMessage);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageDelete": {
|
||||
if (client.messages.getOrPartial(event.id)) {
|
||||
const message = client.messages.getUnderlyingObject(event.id);
|
||||
client.emit("messageDelete", message);
|
||||
client.messages.delete(event.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "BulkMessageDelete": {
|
||||
client.emit(
|
||||
"messageDeleteBulk",
|
||||
event.ids
|
||||
.map((id) => {
|
||||
if (client.messages.has(id)) {
|
||||
const message = client.messages.getUnderlyingObject(id);
|
||||
client.messages.delete(id);
|
||||
return message!;
|
||||
}
|
||||
|
||||
return undefined!;
|
||||
})
|
||||
.filter((x) => x),
|
||||
client.channels.get(event.channel)
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "MessageReact": {
|
||||
const message = client.messages.getOrPartial(event.id);
|
||||
if (message) {
|
||||
const reactions = message.reactions;
|
||||
const set = reactions.get(event.emoji_id)!;
|
||||
if (set) {
|
||||
if (set.has(event.user_id)) return;
|
||||
set.add(event.user_id);
|
||||
} else {
|
||||
reactions.set(event.emoji_id, new ReactiveSet([event.user_id]));
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"messageReactionAdd",
|
||||
message,
|
||||
event.user_id,
|
||||
event.emoji_id
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageUnreact": {
|
||||
const message = client.messages.getOrPartial(event.id);
|
||||
if (message) {
|
||||
const set = message.reactions.get(event.emoji_id);
|
||||
if (set?.has(event.user_id)) {
|
||||
set.delete(event.user_id);
|
||||
} else if (!client.messages.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"messageReactionRemove",
|
||||
message,
|
||||
event.user_id,
|
||||
event.emoji_id
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MessageRemoveReaction": {
|
||||
const message = client.messages.getOrPartial(event.id);
|
||||
if (message) {
|
||||
const reactions = message.reactions;
|
||||
if (reactions.has(event.emoji_id)) {
|
||||
reactions.delete(event.emoji_id);
|
||||
} else if (!client.messages.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit("messageReactionRemoveEmoji", message, event.emoji_id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelCreate": {
|
||||
if (!client.channels.has(event._id)) {
|
||||
client.channels.getOrCreate(event._id, event, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelUpdate": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
const previousChannel = {
|
||||
...client.channels.getUnderlyingObject(event.id),
|
||||
};
|
||||
|
||||
const changes = {
|
||||
...(event.data as Merge<Channel>),
|
||||
};
|
||||
|
||||
if (event.clear) {
|
||||
for (const remove of event.clear) {
|
||||
switch (remove) {
|
||||
case "Description":
|
||||
changes["description"] = undefined;
|
||||
break;
|
||||
case "DefaultPermissions":
|
||||
changes["default_permissions"] = undefined;
|
||||
break;
|
||||
case "Icon":
|
||||
changes["icon"] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.channels.updateUnderlyingObject(event.id, changes as never);
|
||||
client.emit("channelUpdate", channel, previousChannel);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelDelete": {
|
||||
if (client.channels.getOrPartial(event.id)) {
|
||||
const channel = client.channels.getUnderlyingObject(event.id);
|
||||
client.emit("channelDelete", channel);
|
||||
client.channels.delete(event.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelGroupJoin": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
if (!channel.recipientIds.has(event.user)) {
|
||||
channel.recipientIds.add(event.user);
|
||||
} else if (!client.channels.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"channelGroupJoin",
|
||||
channel,
|
||||
await client.users.fetch(event.user)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelGroupLeave": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
if (channel.recipientIds.has(event.user)) {
|
||||
channel.recipientIds.delete(event.user);
|
||||
} else if (!client.channels.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"channelGroupLeave",
|
||||
channel,
|
||||
client.users.getOrPartial(event.user)!
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelStartTyping": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
if (!channel.typingIds.has(event.user)) {
|
||||
channel.typingIds.add(event.user);
|
||||
} else if (!client.channels.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"channelStartTyping",
|
||||
channel,
|
||||
client.users.getOrPartial(event.user)!
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelStopTyping": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
if (channel.typingIds.has(event.user)) {
|
||||
channel.typingIds.delete(event.user);
|
||||
} else if (!client.channels.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit(
|
||||
"channelStopTyping",
|
||||
channel,
|
||||
client.users.getOrPartial(event.user)!
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ChannelAck": {
|
||||
const channel = client.channels.getOrPartial(event.id);
|
||||
if (channel) {
|
||||
client.emit("channelAcknowledged", channel, event.message_id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerCreate": {
|
||||
if (!client.servers.has(event.server._id)) {
|
||||
for (const channel of event.channels) {
|
||||
client.channels.getOrCreate(channel._id, channel);
|
||||
}
|
||||
|
||||
client.servers.getOrCreate(event.server._id, event.server, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerUpdate": {
|
||||
const server = client.servers.getOrPartial(event.id);
|
||||
if (server) {
|
||||
const previousServer = {
|
||||
...client.servers.getUnderlyingObject(event.id),
|
||||
};
|
||||
|
||||
const changes = {
|
||||
...event.data,
|
||||
};
|
||||
|
||||
if (event.clear) {
|
||||
for (const remove of event.clear) {
|
||||
switch (remove) {
|
||||
case "Banner":
|
||||
changes["banner"] = undefined;
|
||||
break;
|
||||
case "Categories":
|
||||
changes["categories"] = undefined;
|
||||
break;
|
||||
case "SystemMessages":
|
||||
changes["system_messages"] = undefined;
|
||||
break;
|
||||
case "Description":
|
||||
changes["description"] = undefined;
|
||||
break;
|
||||
case "Icon":
|
||||
changes["icon"] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.servers.updateUnderlyingObject(event.id, changes as never);
|
||||
client.emit("serverUpdate", server, previousServer);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerDelete": {
|
||||
if (client.servers.getOrPartial(event.id)) {
|
||||
const server = client.servers.getUnderlyingObject(event.id);
|
||||
client.emit("serverDelete", server);
|
||||
client.servers.delete(event.id);
|
||||
|
||||
for (const channel of server.channelIds) {
|
||||
client.channels.delete(channel);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerRoleUpdate": {
|
||||
const server = client.servers.getOrPartial(event.id);
|
||||
if (server) {
|
||||
const role = server.roles.get(event.role_id) ?? {};
|
||||
server.roles.set(event.role_id, {
|
||||
...role,
|
||||
...event.data,
|
||||
} as Role);
|
||||
|
||||
client.emit("serverRoleUpdate", server, event.role_id, role as never);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerRoleDelete": {
|
||||
const server = client.servers.getOrPartial(event.id);
|
||||
if (server) {
|
||||
let role = {};
|
||||
const roles = server.roles;
|
||||
if (roles.has(event.role_id)) {
|
||||
role = roles.get(event.role_id) as Role;
|
||||
roles.delete(event.role_id);
|
||||
} else if (!client.servers.isPartial(event.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.emit("serverRoleDelete", server, event.role_id, role as never);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerMemberJoin": {
|
||||
const id = {
|
||||
server: event.id,
|
||||
user: event.user,
|
||||
};
|
||||
|
||||
if (!client.serverMembers.hasByKey(id)) {
|
||||
client.emit(
|
||||
"serverMemberJoin",
|
||||
client.serverMembers.getOrCreate(id, {
|
||||
_id: id,
|
||||
joined_at: new Date().toUTCString(),
|
||||
})
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerMemberUpdate": {
|
||||
const member = client.serverMembers.getOrPartial(event.id);
|
||||
if (member) {
|
||||
const previousMember = {
|
||||
...client.serverMembers.getUnderlyingObject(
|
||||
event.id.server + event.id.user
|
||||
),
|
||||
};
|
||||
|
||||
const changes = {
|
||||
...event.data,
|
||||
};
|
||||
|
||||
if (event.clear) {
|
||||
for (const remove of event.clear) {
|
||||
switch (remove) {
|
||||
case "Nickname":
|
||||
changes["nickname"] = undefined;
|
||||
break;
|
||||
case "Avatar":
|
||||
changes["avatar"] = undefined;
|
||||
break;
|
||||
case "Roles":
|
||||
changes["roles"] = undefined;
|
||||
break;
|
||||
case "Timeout":
|
||||
changes["timeout"] = undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.serverMembers.updateUnderlyingObject(
|
||||
event.id.server + event.id.user,
|
||||
changes as never
|
||||
);
|
||||
|
||||
client.emit("serverMemberUpdate", member, previousMember);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ServerMemberLeave": {
|
||||
const id = {
|
||||
server: event.id,
|
||||
user: event.user,
|
||||
};
|
||||
|
||||
if (client.serverMembers.getOrPartial(id)) {
|
||||
const member = client.serverMembers.getUnderlyingObject(
|
||||
id.server + id.user
|
||||
);
|
||||
client.emit("serverMemberLeave", member);
|
||||
client.serverMembers.delete(id.server + id.user);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "UserUpdate": {
|
||||
const user = client.users.getOrPartial(event.id);
|
||||
if (user) {
|
||||
const previousUser = {
|
||||
...client.users.getUnderlyingObject(event.id),
|
||||
};
|
||||
|
||||
const changes = {
|
||||
...event.data,
|
||||
};
|
||||
|
||||
if (event.clear) {
|
||||
for (const remove of event.clear) {
|
||||
switch (remove) {
|
||||
case "Avatar":
|
||||
changes["avatar"] = undefined;
|
||||
break;
|
||||
case "StatusPresence":
|
||||
changes["status"] = {
|
||||
...(previousUser.status ?? {}),
|
||||
...(changes["status"] ?? {}),
|
||||
presence: undefined,
|
||||
};
|
||||
break;
|
||||
case "StatusText":
|
||||
changes["status"] = {
|
||||
...(previousUser.status ?? {}),
|
||||
...(changes["status"] ?? {}),
|
||||
text: undefined,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.users.updateUnderlyingObject(event.id, changes as never);
|
||||
client.emit("userUpdate", user, previousUser);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "UserRelationship": {
|
||||
handleEvent(
|
||||
client,
|
||||
{
|
||||
type: "UserUpdate",
|
||||
id: event.user._id,
|
||||
data: {
|
||||
relationship: event.user.relationship!,
|
||||
},
|
||||
},
|
||||
setReady
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "UserPresence": {
|
||||
handleEvent(
|
||||
client,
|
||||
{
|
||||
type: "UserUpdate",
|
||||
id: event.id,
|
||||
data: {
|
||||
online: event.online,
|
||||
},
|
||||
},
|
||||
setReady
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "UserSettingsUpdate": {
|
||||
client.emit("userSettingsUpdate", event.id, event.update);
|
||||
break;
|
||||
}
|
||||
case "UserPlatformWipe": {
|
||||
handleEvent(
|
||||
client,
|
||||
{
|
||||
type: "BulkMessageDelete",
|
||||
channel: "0",
|
||||
ids: client.messages
|
||||
.toList()
|
||||
.filter((message) => message.authorId === event.user_id)
|
||||
.map((message) => message.id),
|
||||
},
|
||||
setReady
|
||||
);
|
||||
|
||||
handleEvent(
|
||||
client,
|
||||
{
|
||||
type: "UserUpdate",
|
||||
id: event.user_id,
|
||||
data: {
|
||||
username: `Deleted User`,
|
||||
online: false,
|
||||
flags: event.flags,
|
||||
badges: 0,
|
||||
relationship: "None",
|
||||
},
|
||||
clear: ["Avatar", "StatusPresence", "StatusText"],
|
||||
},
|
||||
setReady
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case "EmojiCreate": {
|
||||
if (!client.emojis.has(event._id)) {
|
||||
client.emojis.getOrCreate(event._id, event, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "EmojiDelete": {
|
||||
if (client.emojis.getOrPartial(event.id)) {
|
||||
const emoji = client.emojis.getUnderlyingObject(event.id);
|
||||
client.emit("emojiDelete", emoji);
|
||||
client.emojis.delete(event.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Auth": {
|
||||
if (typeof client.session === "string") throw "Unreachable code";
|
||||
|
||||
switch (event.event_type) {
|
||||
case "DeleteSession":
|
||||
if (event.session_id === client.session!._id) {
|
||||
// TODO: logout
|
||||
}
|
||||
break;
|
||||
case "DeleteAllSessions":
|
||||
if (event.exclude_session_id !== client.session!._id) {
|
||||
// TODO: logout
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
import { Channel as ApiChannel, File, OverrideField } from "revolt-api";
|
||||
|
||||
import type { Merge } from "../lib/merge";
|
||||
@@ -13,7 +14,8 @@ export type HydratedChannel = {
|
||||
icon?: File;
|
||||
|
||||
active: boolean;
|
||||
recipientIds: string[];
|
||||
typingIds: ReactiveSet<string>;
|
||||
recipientIds: ReactiveSet<string>;
|
||||
|
||||
userId?: string;
|
||||
ownerId?: string;
|
||||
@@ -46,7 +48,8 @@ export const channelHydration: Hydrate<Merge<ApiChannel>, HydratedChannel> = {
|
||||
description: (channel) => channel.description!,
|
||||
icon: (channel) => channel.icon!,
|
||||
active: (channel) => channel.active || false,
|
||||
recipientIds: (channel) => channel.recipients ?? [],
|
||||
typingIds: () => new ReactiveSet(),
|
||||
recipientIds: (channel) => new ReactiveSet(channel.recipients),
|
||||
userId: (channel) => channel.user,
|
||||
ownerId: (channel) => channel.owner,
|
||||
serverId: (channel) => channel.server,
|
||||
@@ -56,4 +59,8 @@ export const channelHydration: Hydrate<Merge<ApiChannel>, HydratedChannel> = {
|
||||
nsfw: (channel) => channel.nsfw || false,
|
||||
lastMessageId: (channel) => channel.last_message_id!,
|
||||
},
|
||||
initialHydration: () => ({
|
||||
typingIds: new ReactiveSet(),
|
||||
recipientIds: new ReactiveSet(),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -26,4 +26,5 @@ export const emojiHydration: Hydrate<Merge<ApiEmoji>, HydratedEmoji> = {
|
||||
animated: (emoji) => emoji.animated || false,
|
||||
nsfw: (emoji) => emoji.nsfw || false,
|
||||
},
|
||||
initialHydration: () => ({}),
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@ export type KeyMapping<Input, Output> = Record<keyof Input, keyof Output>;
|
||||
export type Hydrate<Input, Output> = {
|
||||
keyMapping: Partial<KeyMapping<Input, Output>>;
|
||||
functions: MappingFns<Input, Output, keyof Output>;
|
||||
initialHydration: () => Partial<Output>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -81,13 +82,16 @@ type ExtractOutput<T> = T extends Hydrate<any, infer O> ? O : never;
|
||||
* Hydrate some input with a given type
|
||||
* @param type Type
|
||||
* @param input Input Object
|
||||
* @param initial Whether this is the initial hydration
|
||||
* @returns Hydrated Object
|
||||
*/
|
||||
export function hydrate<T extends keyof Hydrators>(
|
||||
type: T,
|
||||
input: Partial<ExtractInput<Hydrators[T]>>
|
||||
input: Partial<ExtractInput<Hydrators[T]>>,
|
||||
initial?: boolean
|
||||
) {
|
||||
return hydrateInternal(hydrators[type] as never, input) as ExtractOutput<
|
||||
Hydrators[T]
|
||||
>;
|
||||
return hydrateInternal(
|
||||
hydrators[type] as never,
|
||||
initial ? { ...hydrators[type].initialHydration(), ...input } : input
|
||||
) as ExtractOutput<Hydrators[T]>;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export type HydratedMessage = {
|
||||
embeds?: Embed[];
|
||||
mentionIds?: string[];
|
||||
replyIds?: string[];
|
||||
reactions?: ReactiveMap<string, ReactiveSet<string>>;
|
||||
reactions: ReactiveMap<string, ReactiveSet<string>>;
|
||||
interactions?: Interactions;
|
||||
masquerade?: Masquerade;
|
||||
};
|
||||
@@ -64,4 +64,7 @@ export const messageHydration: Hydrate<Merge<ApiMessage>, HydratedMessage> = {
|
||||
interactions: (message) => message.interactions,
|
||||
masquerade: (message) => message.masquerade!,
|
||||
},
|
||||
initialHydration: () => ({
|
||||
reactions: new ReactiveMap(),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ export type HydratedServer = {
|
||||
categories?: Category[];
|
||||
|
||||
systemMessages?: SystemMessageChannels;
|
||||
roles?: ReactiveMap<string, Role>;
|
||||
roles: ReactiveMap<string, Role>;
|
||||
defaultPermissions: number;
|
||||
|
||||
flags: ServerFlags;
|
||||
@@ -61,6 +61,10 @@ export const serverHydration: Hydrate<ApiServer, HydratedServer> = {
|
||||
discoverable: (server) => server.discoverable || false,
|
||||
nsfw: (server) => server.nsfw || false,
|
||||
},
|
||||
initialHydration: () => ({
|
||||
channelIds: new ReactiveSet(),
|
||||
roles: new ReactiveMap(),
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ export type HydratedServerMember = {
|
||||
joinedAt: Date;
|
||||
nickname?: string;
|
||||
avatar?: File;
|
||||
roles?: string[];
|
||||
roles: string[];
|
||||
timeout?: Date;
|
||||
};
|
||||
|
||||
@@ -29,4 +29,7 @@ export const serverMemberHydration: Hydrate<
|
||||
roles: (member) => member.roles,
|
||||
timeout: (member) => new Date(member.timeout!),
|
||||
},
|
||||
initialHydration: () => ({
|
||||
roles: [],
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -43,6 +43,9 @@ export const userHydration: Hydrate<ApiUser, HydratedUser> = {
|
||||
status: (user) => user.status! ?? undefined,
|
||||
bot: (user) => user.bot! ?? undefined,
|
||||
},
|
||||
initialHydration: () => ({
|
||||
relationship: "None",
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -36,7 +36,7 @@ export class ObjectStorage<T> {
|
||||
*/
|
||||
hydrate(id: string, type: keyof Hydrators, data?: unknown) {
|
||||
if (data) {
|
||||
this.set(id, hydrate(type, data as never) as T);
|
||||
this.set(id, hydrate(type, data as never, true) as T);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user