mirror of
https://github.com/stoatchat/javascript-client-sdk.git
synced 2026-07-18 16:34:27 -04:00
feat: implement store backed common classes
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
node_modules
|
||||
.env
|
||||
docs
|
||||
todo
|
||||
lib
|
||||
|
||||
# yarn
|
||||
|
||||
@@ -18,9 +18,12 @@
|
||||
"description": "Library for interacting with the Revolt API.",
|
||||
"packageManager": "pnpm@7.14.2",
|
||||
"dependencies": {
|
||||
"@solid-primitives/map": "^0.4.3",
|
||||
"@solid-primitives/set": "^0.4.3",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"revolt-api": "^0.5.17",
|
||||
"solid-js": "^1.7.2",
|
||||
"ulid": "^2.3.0",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
Generated
+46
@@ -1,6 +1,8 @@
|
||||
lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
'@solid-primitives/map': ^0.4.3
|
||||
'@solid-primitives/set': ^0.4.3
|
||||
'@trivago/prettier-plugin-sort-imports': ^4.1.1
|
||||
'@types/node': ^18.15.11
|
||||
'@types/ws': ^8.5.4
|
||||
@@ -18,12 +20,16 @@ specifiers:
|
||||
tsc-watch: ^6.0.0
|
||||
typed-emitter: ^2.1.0
|
||||
typescript: ^5.0.3
|
||||
ulid: ^2.3.0
|
||||
ws: ^8.13.0
|
||||
|
||||
dependencies:
|
||||
'@solid-primitives/map': 0.4.3_solid-js@1.7.2
|
||||
'@solid-primitives/set': 0.4.3_solid-js@1.7.2
|
||||
isomorphic-ws: 5.0.0_ws@8.13.0
|
||||
revolt-api: 0.5.17
|
||||
solid-js: 1.7.2
|
||||
ulid: 2.3.0
|
||||
ws: 8.13.0
|
||||
|
||||
devDependencies:
|
||||
@@ -248,6 +254,41 @@ packages:
|
||||
fastq: 1.15.0
|
||||
dev: true
|
||||
|
||||
/@solid-primitives/map/0.4.3_solid-js@1.7.2:
|
||||
resolution: {integrity: sha512-9cpa8MjhLNg+PTVQaIhDUuc192kXoyIAbSCGNTxjzqq7u4OwakhvZlt4C3rIkvLX6yThNlC4IBhOnOSpos5DQA==}
|
||||
peerDependencies:
|
||||
solid-js: ^1.6.12
|
||||
dependencies:
|
||||
'@solid-primitives/trigger': 1.0.5_solid-js@1.7.2
|
||||
solid-js: 1.7.2
|
||||
dev: false
|
||||
|
||||
/@solid-primitives/set/0.4.3_solid-js@1.7.2:
|
||||
resolution: {integrity: sha512-n/owKQy/76IWPLZkvLSWZ+0Ji4fndvQ6vj4idXEm5vS8hj/xvGHP2Fw6uBf2XrijGr3BVuSuq9zvnaGjIAB12w==}
|
||||
peerDependencies:
|
||||
solid-js: ^1.6.12
|
||||
dependencies:
|
||||
'@solid-primitives/trigger': 1.0.5_solid-js@1.7.2
|
||||
solid-js: 1.7.2
|
||||
dev: false
|
||||
|
||||
/@solid-primitives/trigger/1.0.5_solid-js@1.7.2:
|
||||
resolution: {integrity: sha512-NAnLYmUFPvJoH05qBf90IEcSTROM+woFdqcGgGmlcYcewewLNLIuJDB69CvPvM+O9yHvRk7UXcpapV25iXcoDQ==}
|
||||
peerDependencies:
|
||||
solid-js: ^1.6.12
|
||||
dependencies:
|
||||
'@solid-primitives/utils': 6.0.0_solid-js@1.7.2
|
||||
solid-js: 1.7.2
|
||||
dev: false
|
||||
|
||||
/@solid-primitives/utils/6.0.0_solid-js@1.7.2:
|
||||
resolution: {integrity: sha512-SSu12+vvU6J8SVWW9eGV+dqiMsc0WG1Oy8YYY9AWzRzssmzNXg/uexaW+0eqTuj8JcRAaGplMDjhraB01HOidA==}
|
||||
peerDependencies:
|
||||
solid-js: ^1.6.12
|
||||
dependencies:
|
||||
solid-js: 1.7.2
|
||||
dev: false
|
||||
|
||||
/@trivago/prettier-plugin-sort-imports/4.1.1_prettier@2.8.7:
|
||||
resolution: {integrity: sha512-dQ2r2uzNr1x6pJsuh/8x0IRA3CBUB+pWEW3J/7N98axqt7SQSm+2fy0FLNXvXGg77xEDC7KHxJlHfLYyi7PDcw==}
|
||||
peerDependencies:
|
||||
@@ -1834,6 +1875,11 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ulid/2.3.0:
|
||||
resolution: {integrity: sha512-keqHubrlpvT6G2wH0OEfSW4mquYRcbe/J8NMmveoQOjUqmo+hXtO+ORCpWhdbZ7k72UtY61BL7haGxW6enBnjw==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/unbox-primitive/1.0.2:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
dependencies:
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { API } from "revolt-api";
|
||||
|
||||
import channelClassFactory from "./classes/Channel";
|
||||
import emojiClassFactory from "./classes/Emoji";
|
||||
import serverClassFactory from "./classes/Server";
|
||||
import serverMemberClassFactory from "./classes/ServerMember";
|
||||
import userClassFactory from "./classes/User";
|
||||
import { EventClient, createEventClient } from "./events/client";
|
||||
|
||||
// eslint-disable-next-line
|
||||
type Obj<T extends (...args: any) => any> = InstanceType<ReturnType<T>>;
|
||||
|
||||
export type Emoji = Obj<typeof emojiClassFactory>;
|
||||
export type Channel = Obj<typeof channelClassFactory>;
|
||||
export type Server = Obj<typeof serverClassFactory>;
|
||||
export type ServerMember = Obj<typeof serverMemberClassFactory>;
|
||||
export type User = Obj<typeof userClassFactory>;
|
||||
|
||||
/**
|
||||
* Revolt.js Client
|
||||
*/
|
||||
export class Client {
|
||||
#Emoji = emojiClassFactory(this);
|
||||
#Channel = channelClassFactory(this);
|
||||
#User = userClassFactory(this);
|
||||
#Server = serverClassFactory(this);
|
||||
#ServerMember = serverMemberClassFactory(this);
|
||||
|
||||
readonly emojis = this.#Emoji;
|
||||
readonly channels = this.#Channel;
|
||||
readonly users = this.#User;
|
||||
readonly servers = this.#Server;
|
||||
readonly serverMembers = this.#ServerMember;
|
||||
|
||||
readonly api: API;
|
||||
readonly events: EventClient<1>;
|
||||
|
||||
/**
|
||||
* Create Revolt.js Client
|
||||
*/
|
||||
constructor() {
|
||||
this.api = new API({
|
||||
authentication: {
|
||||
rauth: process.env.TOKEN!,
|
||||
},
|
||||
});
|
||||
this.events = createEventClient(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
new this.#User(user._id, user);
|
||||
}
|
||||
console.timeEnd("load users");
|
||||
console.time("load servers");
|
||||
for (const server of event.servers) {
|
||||
new this.#Server(server._id, server);
|
||||
}
|
||||
console.timeEnd("load servers");
|
||||
console.time("load memberships");
|
||||
for (const member of event.members) {
|
||||
new this.#ServerMember(member._id, member);
|
||||
}
|
||||
console.timeEnd("load memberships");
|
||||
console.time("load channels");
|
||||
for (const channel of event.channels) {
|
||||
new this.#Channel(channel._id, channel);
|
||||
}
|
||||
console.timeEnd("load channels");
|
||||
console.time("load emojis");
|
||||
for (const emoji of event.emojis) {
|
||||
new this.#Emoji(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.events.on("state", (state) => console.info("STATE =", state));
|
||||
this.events.on("error", (error) => console.error("ERROR =", error));
|
||||
|
||||
this.events.connect("wss://ws.revolt.chat", process.env.TOKEN!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { Channel as ApiChannel } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { Client } from "../Client";
|
||||
import { HydratedChannel } from "../hydration/channel";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
export default (client: Client) =>
|
||||
/**
|
||||
* Channel Class
|
||||
*/
|
||||
class Channel {
|
||||
static #storage = new ObjectStorage<HydratedChannel>();
|
||||
static #objects: Record<string, Channel> = {};
|
||||
|
||||
/**
|
||||
* Get an existing Channel
|
||||
* @param id Channel ID
|
||||
* @returns Channel
|
||||
*/
|
||||
static get(id: string): Channel | undefined {
|
||||
return Channel.#objects[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch channel by ID
|
||||
* @param id ID
|
||||
* @returns Channel
|
||||
*/
|
||||
static async fetch(id: string): Promise<Channel | undefined> {
|
||||
const channel = Channel.get(id);
|
||||
if (channel) return channel;
|
||||
|
||||
const data = await client.api.get(`/channels/${id as ""}`);
|
||||
return new Channel(id, data);
|
||||
}
|
||||
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct Channel
|
||||
* @param id Channel Id
|
||||
*/
|
||||
constructor(id: string, data?: ApiChannel) {
|
||||
Channel.#storage.hydrate(id, "channel", data);
|
||||
Channel.#objects[id] = this;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this server was created
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel type
|
||||
*/
|
||||
get type() {
|
||||
return Channel.#storage.get(this.id).channelType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel name
|
||||
*/
|
||||
get name() {
|
||||
return Channel.#storage.get(this.id).name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel description
|
||||
*/
|
||||
get description() {
|
||||
return Channel.#storage.get(this.id).description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel icon
|
||||
*/
|
||||
get icon() {
|
||||
return Channel.#storage.get(this.id).icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the conversation is active
|
||||
*/
|
||||
get active() {
|
||||
return Channel.#storage.get(this.id).active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recipients of the group
|
||||
*/
|
||||
get recipients() {
|
||||
return Channel.#storage
|
||||
.get(this.id)
|
||||
.recipientIds.map((id) => client.users.get(id)!);
|
||||
}
|
||||
|
||||
/**
|
||||
* User this channel belongs to
|
||||
*/
|
||||
get user() {
|
||||
return client.users.get(Channel.#storage.get(this.id).userId!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner of the group
|
||||
*/
|
||||
get owner() {
|
||||
return client.users.get(Channel.#storage.get(this.id).ownerId!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server this channel is in
|
||||
*/
|
||||
get server() {
|
||||
return client.users.get(Channel.#storage.get(this.id).serverId!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissions allowed for users in this group
|
||||
*/
|
||||
get permissions() {
|
||||
return Channel.#storage.get(this.id).permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default permissions for this server channel
|
||||
*/
|
||||
get defaultPermissions() {
|
||||
return Channel.#storage.get(this.id).defaultPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Role permissions for this server channel
|
||||
*/
|
||||
get rolePermissions() {
|
||||
return Channel.#storage.get(this.id).rolePermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this channel is marked as mature
|
||||
*/
|
||||
get mature() {
|
||||
return Channel.#storage.get(this.id).nsfw;
|
||||
}
|
||||
|
||||
/**
|
||||
* ID of the last message sent in this channel
|
||||
*/
|
||||
get lastMessageId() {
|
||||
return Channel.#storage.get(this.id).lastMessageId;
|
||||
}
|
||||
|
||||
// TODO: lastMessage
|
||||
};
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Emoji as ApiEmoji } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { Client } from "../Client";
|
||||
import { HydratedEmoji } from "../hydration/emoji";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
export default (client: Client) =>
|
||||
/**
|
||||
* Emoji Class
|
||||
*/
|
||||
class Emoji {
|
||||
static #storage = new ObjectStorage<HydratedEmoji>();
|
||||
static #objects: Record<string, Emoji> = {};
|
||||
|
||||
/**
|
||||
* Get an existing Emoji
|
||||
* @param id Emoji ID
|
||||
* @returns Emoji
|
||||
*/
|
||||
static get(id: string): Emoji | undefined {
|
||||
return Emoji.#objects[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch emoji by ID
|
||||
* @param id ID
|
||||
* @returns Emoji
|
||||
*/
|
||||
static async fetch(id: string): Promise<Emoji | undefined> {
|
||||
const emoji = Emoji.get(id);
|
||||
if (emoji) return emoji;
|
||||
|
||||
const data = await client.api.get(`/custom/emoji/${id as ""}`);
|
||||
return new Emoji(id, data);
|
||||
}
|
||||
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct Emoji
|
||||
* @param id Emoji Id
|
||||
*/
|
||||
constructor(id: string, data?: ApiEmoji) {
|
||||
Emoji.#storage.hydrate(id, "emoji", data);
|
||||
Emoji.#objects[id] = this;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this emoji was created
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about the parent of this emoji
|
||||
*/
|
||||
get parent() {
|
||||
return Emoji.#storage.get(this.id).parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creator of the emoji
|
||||
*/
|
||||
get creator() {
|
||||
return client.users.get(Emoji.#storage.get(this.id).creatorId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
get name() {
|
||||
return Emoji.#storage.get(this.id).name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the emoji is animated
|
||||
*/
|
||||
get animated() {
|
||||
return Emoji.#storage.get(this.id).animated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the emoji is marked as mature
|
||||
*/
|
||||
get mature() {
|
||||
return Emoji.#storage.get(this.id).nsfw;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import type { Message as ApiMessage } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { Client } from "../Client";
|
||||
import { HydratedMessage } from "../hydration/message";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
export default (client: Client) =>
|
||||
/**
|
||||
* Message Class
|
||||
*/
|
||||
class Message {
|
||||
static #storage = new ObjectStorage<HydratedMessage>();
|
||||
static #objects: Record<string, Message> = {};
|
||||
|
||||
/**
|
||||
* Get an existing Message
|
||||
* @param id Message ID
|
||||
* @returns Message
|
||||
*/
|
||||
static get(id: string): Message | undefined {
|
||||
return Message.#objects[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch message by ID
|
||||
* @param channelId Channel ID
|
||||
* @param messageId Message ID
|
||||
* @returns Message
|
||||
*/
|
||||
static async fetch(
|
||||
channelId: string,
|
||||
messageId: string
|
||||
): Promise<Message | undefined> {
|
||||
const message = Message.get(messageId);
|
||||
if (message) return message;
|
||||
|
||||
const data = await client.api.get(
|
||||
`/channels/${channelId as ""}/messages/${messageId as ""}`
|
||||
);
|
||||
return new Message(messageId, data);
|
||||
}
|
||||
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct Message
|
||||
* @param id Message Id
|
||||
*/
|
||||
constructor(id: string, data?: ApiMessage) {
|
||||
Message.#storage.hydrate(id, "message", data);
|
||||
Message.#objects[id] = this;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this message was posted
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Nonce value
|
||||
*/
|
||||
get nonce() {
|
||||
return Message.#storage.get(this.id).nonce;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel this message was sent in
|
||||
*/
|
||||
get channel() {
|
||||
return client.channels.get(Message.#storage.get(this.id).channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* User this message was sent by
|
||||
*/
|
||||
get author() {
|
||||
return client.users.get(Message.#storage.get(this.id).authorId!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Content
|
||||
*/
|
||||
get content() {
|
||||
return Message.#storage.get(this.id).content;
|
||||
}
|
||||
|
||||
/**
|
||||
* System message content
|
||||
*/
|
||||
get systemMessage() {
|
||||
return Message.#storage.get(this.id).systemMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attachments
|
||||
*/
|
||||
get attachments() {
|
||||
return Message.#storage.get(this.id).attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time at which this message was edited
|
||||
*/
|
||||
get editedAt() {
|
||||
return Message.#storage.get(this.id).editedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds
|
||||
*/
|
||||
get embeds() {
|
||||
return Message.#storage.get(this.id).embeds;
|
||||
}
|
||||
|
||||
/**
|
||||
* IDs of users this message mentions
|
||||
*/
|
||||
get mentionIds() {
|
||||
return Message.#storage.get(this.id).mentionIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* IDs of messages this message replies to
|
||||
*/
|
||||
get replyIds() {
|
||||
return Message.#storage.get(this.id).replyIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reactions
|
||||
*/
|
||||
get reactions() {
|
||||
return Message.#storage.get(this.id).reactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactions
|
||||
*/
|
||||
get interactions() {
|
||||
return Message.#storage.get(this.id).interactions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Masquerade
|
||||
*/
|
||||
get masquerade() {
|
||||
return Message.#storage.get(this.id).masquerade;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
import type { Server as ApiServer, Category } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { Channel, Client } from "../Client";
|
||||
import { HydratedServer } from "../hydration/server";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
export default (client: Client) =>
|
||||
/**
|
||||
* Server Class
|
||||
*/
|
||||
class Server {
|
||||
static #storage = new ObjectStorage<HydratedServer>();
|
||||
static #objects: Record<string, Server> = {};
|
||||
|
||||
/**
|
||||
* Get an existing Server
|
||||
* @param id Server ID
|
||||
* @returns Server
|
||||
*/
|
||||
static get(id: string): Server | undefined {
|
||||
return Server.#objects[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch server by ID
|
||||
* @param id ID
|
||||
* @returns Server
|
||||
*/
|
||||
static async fetch(id: string): Promise<Server | undefined> {
|
||||
const server = Server.get(id);
|
||||
if (server) return server;
|
||||
|
||||
const data = await client.api.get(`/servers/${id as ""}`);
|
||||
return new Server(id, data);
|
||||
}
|
||||
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct Server
|
||||
* @param id Server Id
|
||||
*/
|
||||
constructor(id: string, data?: ApiServer) {
|
||||
Server.#storage.hydrate(id, "server", data);
|
||||
Server.#objects[id] = this;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this server was created
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner
|
||||
*/
|
||||
get owner() {
|
||||
return client.users.get(Server.#storage.get(this.id).ownerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name
|
||||
*/
|
||||
get name() {
|
||||
return Server.#storage.get(this.id).name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Description
|
||||
*/
|
||||
get description() {
|
||||
return Server.#storage.get(this.id).description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon
|
||||
*/
|
||||
get icon() {
|
||||
return Server.#storage.get(this.id).icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Banner
|
||||
*/
|
||||
get banner() {
|
||||
return Server.#storage.get(this.id).banner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channels
|
||||
*/
|
||||
get channels() {
|
||||
return [...Server.#storage.get(this.id).channelIds.values()]
|
||||
.map((id) => client.channels.get(id)!)
|
||||
.filter((x) => x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Categories
|
||||
*/
|
||||
get categories() {
|
||||
return Server.#storage.get(this.id).categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* System message channels
|
||||
*/
|
||||
get systemMessages() {
|
||||
return Server.#storage.get(this.id).systemMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Roles
|
||||
*/
|
||||
get roles() {
|
||||
return Server.#storage.get(this.id).roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default permissions
|
||||
*/
|
||||
get defaultPermissions() {
|
||||
return Server.#storage.get(this.id).defaultPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server flags
|
||||
*/
|
||||
get flags() {
|
||||
return Server.#storage.get(this.id).flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether analytics are enabled for this server
|
||||
*/
|
||||
get analytics() {
|
||||
return Server.#storage.get(this.id).analytics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this server is publicly discoverable
|
||||
*/
|
||||
get discoverable() {
|
||||
return Server.#storage.get(this.id).discoverable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this server is marked as mature
|
||||
*/
|
||||
get mature() {
|
||||
return Server.#storage.get(this.id).nsfw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of ordered categories with their respective channels.
|
||||
* Uncategorised channels are returned in `id="default"` category.
|
||||
*/
|
||||
get orderedChannels(): (Omit<Category, "channels"> & {
|
||||
channels: Channel[];
|
||||
})[] {
|
||||
const uncategorised = new Set(this.channels.map((channel) => channel.id));
|
||||
|
||||
const elements = [];
|
||||
let defaultCategory;
|
||||
|
||||
const categories = this.categories;
|
||||
if (categories) {
|
||||
for (const category of categories) {
|
||||
const channels = [];
|
||||
for (const key of category.channels) {
|
||||
if (uncategorised.delete(key)) {
|
||||
channels.push(client.channels.get(key)!);
|
||||
}
|
||||
}
|
||||
|
||||
const cat = {
|
||||
...category,
|
||||
channels,
|
||||
};
|
||||
|
||||
if (cat.id === "default") {
|
||||
if (channels.length === 0) continue;
|
||||
|
||||
defaultCategory = cat;
|
||||
}
|
||||
|
||||
elements.push(cat);
|
||||
}
|
||||
}
|
||||
|
||||
if (uncategorised.size > 0) {
|
||||
const channels = [...uncategorised].map(
|
||||
(key) => client.channels.get(key)!
|
||||
);
|
||||
|
||||
if (defaultCategory) {
|
||||
defaultCategory.channels = [...defaultCategory.channels, ...channels];
|
||||
} else {
|
||||
elements.unshift({
|
||||
id: "default",
|
||||
title: "Default",
|
||||
channels,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default channel for this server
|
||||
*/
|
||||
get defaultChannel(): Channel | undefined {
|
||||
return this.orderedChannels.find((cat) => cat.channels.length)
|
||||
?.channels[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ordered array of roles with their IDs attached.
|
||||
* The highest ranking roles will be first followed by lower
|
||||
* ranking roles. This is dictated by the "rank" property
|
||||
* which is smaller for higher priority roles.
|
||||
*/
|
||||
get orderedRoles() {
|
||||
const roles = this.roles;
|
||||
return roles
|
||||
? [...roles.entries()]
|
||||
.map(([id, role]) => ({ id, ...role }))
|
||||
.sort((a, b) => (a.rank || 0) - (b.rank || 0))
|
||||
: [];
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { Member as ApiMember, MemberCompositeKey } from "revolt-api";
|
||||
|
||||
import { Client } from "../Client";
|
||||
import { HydratedServerMember } from "../hydration/serverMember";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
/**
|
||||
* Deterministic conversion of member composite key to string ID
|
||||
* @param key Key
|
||||
* @returns String key
|
||||
*/
|
||||
function key(key: MemberCompositeKey) {
|
||||
return key.server + key.user;
|
||||
}
|
||||
|
||||
export default (client: Client) =>
|
||||
/**
|
||||
* Server Member Class
|
||||
*/
|
||||
class ServerMember {
|
||||
static #storage = new ObjectStorage<HydratedServerMember>();
|
||||
static #objects: Record<string, ServerMember> = {};
|
||||
|
||||
/**
|
||||
* Deterministic conversion of member composite key to string ID
|
||||
* @param key Key
|
||||
* @returns String key
|
||||
*/
|
||||
static keyToString = key;
|
||||
|
||||
/**
|
||||
* Get an existing Member
|
||||
* @param id Member ID
|
||||
* @returns Member
|
||||
*/
|
||||
static get(id: MemberCompositeKey): ServerMember | undefined {
|
||||
return ServerMember.#objects[key(id)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch member by ID
|
||||
* @param id Member ID
|
||||
* @returns Member
|
||||
*/
|
||||
static async fetch(
|
||||
id: MemberCompositeKey
|
||||
): Promise<ServerMember | undefined> {
|
||||
const channel = ServerMember.get(id);
|
||||
if (channel) return channel;
|
||||
|
||||
const data = await client.api.get(
|
||||
`/servers/${id.server as ""}/members/${id.user as ""}`
|
||||
);
|
||||
return new ServerMember(id, data);
|
||||
}
|
||||
|
||||
readonly id: MemberCompositeKey;
|
||||
|
||||
/**
|
||||
* Construct Member
|
||||
* @param id Member ID
|
||||
*/
|
||||
constructor(id: MemberCompositeKey, data?: ApiMember) {
|
||||
ServerMember.#storage.hydrate(key(id), "serverMember", data);
|
||||
ServerMember.#objects[key(id)] = this;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server this member belongs to
|
||||
*/
|
||||
get server() {
|
||||
return client.servers.get(this.id.server);
|
||||
}
|
||||
|
||||
/**
|
||||
* User corresponding to this member
|
||||
*/
|
||||
get user() {
|
||||
return client.users.get(this.id.user);
|
||||
}
|
||||
|
||||
/**
|
||||
* When this user joined the server
|
||||
*/
|
||||
get joinedAt() {
|
||||
return ServerMember.#storage.get(key(this.id)).joinedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nickname
|
||||
*/
|
||||
get nickname() {
|
||||
return ServerMember.#storage.get(key(this.id)).nickname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar
|
||||
*/
|
||||
get avatar() {
|
||||
return ServerMember.#storage.get(key(this.id)).avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of role IDs
|
||||
*/
|
||||
get roles() {
|
||||
return ServerMember.#storage.get(key(this.id)).roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time at which timeout expires
|
||||
*/
|
||||
get timeout() {
|
||||
return ServerMember.#storage.get(key(this.id)).timeout;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { User as ApiUser } from "revolt-api";
|
||||
import { decodeTime } from "ulid";
|
||||
|
||||
import { Client } from "../Client";
|
||||
import { HydratedUser } from "../hydration/user";
|
||||
import { ObjectStorage } from "../storage/ObjectStorage";
|
||||
|
||||
export default (client: Client) =>
|
||||
/**
|
||||
* User Class
|
||||
*/
|
||||
class User {
|
||||
static #storage = new ObjectStorage<HydratedUser>();
|
||||
static #objects: Record<string, User> = {};
|
||||
|
||||
/**
|
||||
* Get an existing User
|
||||
* @param id User ID
|
||||
* @returns User
|
||||
*/
|
||||
static get(id: string): User | undefined {
|
||||
return User.#objects[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch user by ID
|
||||
* @param id ID
|
||||
* @returns User
|
||||
*/
|
||||
static async fetch(id: string): Promise<User | undefined> {
|
||||
const user = User.get(id);
|
||||
if (user) return user;
|
||||
|
||||
const data = await client.api.get(`/users/${id as ""}`);
|
||||
return new User(id, data);
|
||||
}
|
||||
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* Construct User
|
||||
* @param id User Id
|
||||
*/
|
||||
constructor(id: string, data?: ApiUser) {
|
||||
User.#storage.hydrate(id, "user", data);
|
||||
User.#objects[id] = this;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time when this user created their account
|
||||
*/
|
||||
get createdAt() {
|
||||
return new Date(decodeTime(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Username
|
||||
*/
|
||||
get username() {
|
||||
return User.#storage.get(this.id).username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avatar
|
||||
*/
|
||||
get avatar() {
|
||||
return User.#storage.get(this.id).avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Badges
|
||||
*/
|
||||
get badges() {
|
||||
return User.#storage.get(this.id).badges;
|
||||
}
|
||||
|
||||
/**
|
||||
* User Status
|
||||
*/
|
||||
get status() {
|
||||
return User.#storage.get(this.id).status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship with user
|
||||
*/
|
||||
get relationship() {
|
||||
return User.#storage.get(this.id).relationship;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user is online
|
||||
*/
|
||||
get online() {
|
||||
return User.#storage.get(this.id).online;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user is privileged
|
||||
*/
|
||||
get privileged() {
|
||||
return User.#storage.get(this.id).privileged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags
|
||||
*/
|
||||
get flags() {
|
||||
return User.#storage.get(this.id).flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bot information
|
||||
*/
|
||||
get bot() {
|
||||
return User.#storage.get(this.id).bot;
|
||||
}
|
||||
};
|
||||
+12
-3
@@ -28,7 +28,7 @@ type Events<T extends AvailableProtocols, P extends EventProtocol<T>> = {
|
||||
/**
|
||||
* Simple wrapper around the Revolt websocket service.
|
||||
*/
|
||||
class EventClient<
|
||||
class Client<
|
||||
T extends AvailableProtocols,
|
||||
P extends EventProtocol<T>
|
||||
> extends EventEmitter {
|
||||
@@ -187,6 +187,15 @@ class EventClient<
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -203,10 +212,10 @@ export function createEventClient<
|
||||
heartbeatInterval?: number,
|
||||
pongTimeout?: number
|
||||
) {
|
||||
return new EventClient<T, P>(
|
||||
return new Client<T, P>(
|
||||
protocolVersion,
|
||||
transportFormat,
|
||||
heartbeatInterval,
|
||||
pongTimeout
|
||||
) as EventClient<T, P> & TypedEmitter<Events<T, P>>;
|
||||
) as EventClient<T>;
|
||||
}
|
||||
|
||||
+1
-1
@@ -169,7 +169,7 @@ type ReadyData = {
|
||||
servers: Server[];
|
||||
channels: Channel[];
|
||||
members: Member[];
|
||||
emojis?: Emoji[];
|
||||
emojis: Emoji[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Channel as ApiChannel, File, OverrideField } from "revolt-api";
|
||||
|
||||
import { Merge } from "../lib/merge";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedChannel = {
|
||||
id: string;
|
||||
channelType: ApiChannel["channel_type"];
|
||||
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: File;
|
||||
|
||||
active: boolean;
|
||||
recipientIds: string[];
|
||||
|
||||
userId?: string;
|
||||
ownerId?: string;
|
||||
serverId?: string;
|
||||
|
||||
permissions?: number;
|
||||
defaultPermissions?: OverrideField;
|
||||
rolePermissions?: Record<string, OverrideField>;
|
||||
nsfw: boolean;
|
||||
|
||||
lastMessageId?: string;
|
||||
};
|
||||
|
||||
export const channelHydration: Hydrate<Merge<ApiChannel>, HydratedChannel> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
channel_type: "channelType",
|
||||
recipients: "recipientIds",
|
||||
user: "userId",
|
||||
owner: "ownerId",
|
||||
server: "serverId",
|
||||
default_permissions: "defaultPermissions",
|
||||
role_permissions: "rolePermissions",
|
||||
last_message_id: "lastMessageId",
|
||||
},
|
||||
functions: {
|
||||
id: (channel) => channel._id,
|
||||
channelType: (channel) => channel.channel_type,
|
||||
name: (channel) => channel.name,
|
||||
description: (channel) => channel.description!,
|
||||
icon: (channel) => channel.icon!,
|
||||
active: (channel) => channel.active || false,
|
||||
recipientIds: (channel) => channel.recipients ?? [],
|
||||
userId: (channel) => channel.user,
|
||||
ownerId: (channel) => channel.owner,
|
||||
serverId: (channel) => channel.server,
|
||||
permissions: (channel) => channel.permissions!,
|
||||
defaultPermissions: (channel) => channel.default_permissions!,
|
||||
rolePermissions: (channel) => channel.role_permissions ?? {},
|
||||
nsfw: (channel) => channel.nsfw || false,
|
||||
lastMessageId: (channel) => channel.last_message_id!,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Emoji as ApiEmoji, EmojiParent } from "revolt-api";
|
||||
|
||||
import { Merge } from "../lib/merge";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedEmoji = {
|
||||
id: string;
|
||||
parent: EmojiParent;
|
||||
creatorId: string;
|
||||
name: string;
|
||||
animated: boolean;
|
||||
nsfw: boolean;
|
||||
};
|
||||
|
||||
export const emojiHydration: Hydrate<Merge<ApiEmoji>, HydratedEmoji> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
creator_id: "creatorId",
|
||||
},
|
||||
functions: {
|
||||
id: (emoji) => emoji._id,
|
||||
parent: (emoji) => emoji.parent,
|
||||
creatorId: (emoji) => emoji.creator_id,
|
||||
name: (emoji) => emoji.name,
|
||||
animated: (emoji) => emoji.animated || false,
|
||||
nsfw: (emoji) => emoji.nsfw || false,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { channelHydration } from "./channel";
|
||||
import { emojiHydration } from "./emoji";
|
||||
import { messageHydration } from "./message";
|
||||
import { serverHydration } from "./server";
|
||||
import { serverMemberHydration } from "./serverMember";
|
||||
import { userHydration } from "./user";
|
||||
|
||||
/**
|
||||
* Functions to map from one object to another
|
||||
*/
|
||||
export type MappingFns<Input, Output, Key extends keyof Output> = Record<
|
||||
Key,
|
||||
(value: Input) => Output[Key]
|
||||
>;
|
||||
|
||||
/**
|
||||
* Key mapping information
|
||||
*/
|
||||
export type KeyMapping<Input, Output> = Record<keyof Input, keyof Output>;
|
||||
|
||||
/**
|
||||
* Hydration information
|
||||
*/
|
||||
export type Hydrate<Input, Output> = {
|
||||
keyMapping: Partial<KeyMapping<Input, Output>>;
|
||||
functions: MappingFns<Input, Output, keyof Output>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hydrate some data
|
||||
* @param hydration Hydration data
|
||||
* @param input Input data
|
||||
* @returns Output data
|
||||
*/
|
||||
function hydrateInternal<Input extends object, Output>(
|
||||
hydration: Hydrate<Input, Output>,
|
||||
input: Input
|
||||
): Output {
|
||||
return (Object.keys(input) as (keyof Input)[]).reduce((acc, key) => {
|
||||
let targetKey, value;
|
||||
try {
|
||||
targetKey = hydration.keyMapping[key] ?? key;
|
||||
value = hydration.functions[targetKey as keyof Output](input);
|
||||
} catch (err) {
|
||||
console.debug(`Skipping key ${String(key)} during hydration!`);
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[targetKey]: value,
|
||||
};
|
||||
}, {} as Output);
|
||||
}
|
||||
|
||||
const hydrators = {
|
||||
channel: channelHydration,
|
||||
emoji: emojiHydration,
|
||||
message: messageHydration,
|
||||
server: serverHydration,
|
||||
serverMember: serverMemberHydration,
|
||||
user: userHydration,
|
||||
};
|
||||
|
||||
export type Hydrators = typeof hydrators;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ExtractInput<T> = T extends Hydrate<infer I, any> ? I : never;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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
|
||||
* @returns Hydrated Object
|
||||
*/
|
||||
export function hydrate<T extends keyof Hydrators>(
|
||||
type: T,
|
||||
input: ExtractInput<Hydrators[T]>
|
||||
) {
|
||||
return hydrateInternal(hydrators[type] as never, input) as ExtractOutput<
|
||||
Hydrators[T]
|
||||
>;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ReactiveMap } from "@solid-primitives/map";
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
import {
|
||||
Message as ApiMessage,
|
||||
Embed,
|
||||
File,
|
||||
Interactions,
|
||||
Masquerade,
|
||||
SystemMessage,
|
||||
} from "revolt-api";
|
||||
|
||||
import { Merge } from "../lib/merge";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedMessage = {
|
||||
id: string;
|
||||
nonce?: string;
|
||||
channelId: string;
|
||||
authorId?: string;
|
||||
content?: string;
|
||||
systemMessage?: SystemMessage;
|
||||
attachments?: File[];
|
||||
editedAt?: Date;
|
||||
embeds?: Embed[];
|
||||
mentionIds?: string[];
|
||||
replyIds?: string[];
|
||||
reactions?: ReactiveMap<string, ReactiveSet<string>>;
|
||||
interactions?: Interactions;
|
||||
masquerade?: Masquerade;
|
||||
};
|
||||
|
||||
export const messageHydration: Hydrate<Merge<ApiMessage>, HydratedMessage> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
channel: "channelId",
|
||||
author: "authorId",
|
||||
system: "systemMessage",
|
||||
edited: "editedAt",
|
||||
mentions: "mentionIds",
|
||||
replies: "replyIds",
|
||||
},
|
||||
functions: {
|
||||
id: (message) => message._id,
|
||||
nonce: (message) => message.nonce!,
|
||||
channelId: (message) => message.channel,
|
||||
authorId: (message) => message.author,
|
||||
content: (message) => message.content!,
|
||||
systemMessage: (message) => message.system!,
|
||||
attachments: (message) => message.attachments!,
|
||||
editedAt: (message) => new Date(message.edited!),
|
||||
embeds: (message) => message.embeds!,
|
||||
mentionIds: (message) => message.mentions!,
|
||||
replyIds: (message) => message.replies!,
|
||||
reactions: (message) => {
|
||||
const map = new ReactiveMap<string, ReactiveSet<string>>();
|
||||
if (message.reactions) {
|
||||
for (const reaction of Object.keys(message.reactions)) {
|
||||
map.set(reaction, new ReactiveSet(message.reactions![reaction]));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
},
|
||||
interactions: (message) => message.interactions,
|
||||
masquerade: (message) => message.masquerade!,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { ReactiveMap } from "@solid-primitives/map";
|
||||
import { ReactiveSet } from "@solid-primitives/set";
|
||||
import {
|
||||
Server as ApiServer,
|
||||
Category,
|
||||
File,
|
||||
Role,
|
||||
SystemMessageChannels,
|
||||
} from "revolt-api";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedServer = {
|
||||
id: string;
|
||||
ownerId: string;
|
||||
|
||||
name: string;
|
||||
description?: string;
|
||||
|
||||
icon?: File;
|
||||
banner?: File;
|
||||
|
||||
channelIds: ReactiveSet<string>;
|
||||
categories?: Category[];
|
||||
|
||||
systemMessages?: SystemMessageChannels;
|
||||
roles?: ReactiveMap<string, Role>;
|
||||
defaultPermissions: number;
|
||||
|
||||
flags: ServerFlags;
|
||||
analytics: boolean;
|
||||
discoverable: boolean;
|
||||
nsfw: boolean;
|
||||
};
|
||||
|
||||
export const serverHydration: Hydrate<ApiServer, HydratedServer> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
owner: "ownerId",
|
||||
channels: "channelIds",
|
||||
system_messages: "systemMessages",
|
||||
default_permissions: "defaultPermissions",
|
||||
},
|
||||
functions: {
|
||||
id: (server) => server._id,
|
||||
ownerId: (server) => server.owner,
|
||||
name: (server) => server.name,
|
||||
description: (server) => server.description!,
|
||||
channelIds: (server) => new ReactiveSet(server.channels),
|
||||
categories: (server) => server.categories ?? [],
|
||||
systemMessages: (server) => server.system_messages ?? {},
|
||||
roles: (server) =>
|
||||
new ReactiveMap(
|
||||
Object.keys(server.roles!).map((id) => [id, server.roles![id]])
|
||||
),
|
||||
defaultPermissions: (server) => server.default_permissions,
|
||||
icon: (server) => server.icon!,
|
||||
banner: (server) => server.banner!,
|
||||
flags: (server) => server.flags!,
|
||||
analytics: (server) => server.analytics || false,
|
||||
discoverable: (server) => server.discoverable || false,
|
||||
nsfw: (server) => server.nsfw || false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Flags attributed to servers
|
||||
*/
|
||||
export enum ServerFlags {
|
||||
Official = 1,
|
||||
Verified = 2,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Member as ApiMember, File, MemberCompositeKey } from "revolt-api";
|
||||
|
||||
import { Merge } from "../lib/merge";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedServerMember = {
|
||||
id: MemberCompositeKey;
|
||||
joinedAt: Date;
|
||||
nickname?: string;
|
||||
avatar?: File;
|
||||
roles?: string[];
|
||||
timeout?: Date;
|
||||
};
|
||||
|
||||
export const serverMemberHydration: Hydrate<
|
||||
Merge<ApiMember>,
|
||||
HydratedServerMember
|
||||
> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
joined_at: "joinedAt",
|
||||
},
|
||||
functions: {
|
||||
id: (member) => member._id,
|
||||
joinedAt: (member) => new Date(member.joined_at),
|
||||
nickname: (member) => member.nickname!,
|
||||
avatar: (member) => member.avatar!,
|
||||
roles: (member) => member.roles,
|
||||
timeout: (member) => new Date(member.timeout!),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
User as ApiUser,
|
||||
BotInformation,
|
||||
File,
|
||||
RelationshipStatus,
|
||||
UserStatus,
|
||||
} from "revolt-api";
|
||||
|
||||
import { Hydrate } from ".";
|
||||
|
||||
export type HydratedUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
relationship: RelationshipStatus;
|
||||
|
||||
online: boolean;
|
||||
privileged: boolean;
|
||||
|
||||
badges: UserBadges;
|
||||
flags: UserFlags;
|
||||
|
||||
avatar?: File;
|
||||
status?: UserStatus;
|
||||
bot?: BotInformation;
|
||||
};
|
||||
|
||||
export const userHydration: Hydrate<ApiUser, HydratedUser> = {
|
||||
keyMapping: {
|
||||
_id: "id",
|
||||
},
|
||||
functions: {
|
||||
id: (user) => user._id,
|
||||
username: (user) => user.username,
|
||||
relationship: (user) => user.relationship ?? "None",
|
||||
|
||||
online: (user) => user.online || false,
|
||||
privileged: (user) => user.privileged || false,
|
||||
|
||||
badges: (user) => user.badges ?? 0,
|
||||
flags: (user) => user.flags ?? 0,
|
||||
|
||||
avatar: (user) => user.avatar! ?? undefined,
|
||||
status: (user) => user.status! ?? undefined,
|
||||
bot: (user) => user.bot! ?? undefined,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Badges available to users
|
||||
*/
|
||||
export enum UserBadges {
|
||||
Developer = 1,
|
||||
Translator = 2,
|
||||
Supporter = 4,
|
||||
ResponsibleDisclosure = 8,
|
||||
Founder = 16,
|
||||
PlatformModeration = 32,
|
||||
ActiveSupporter = 64,
|
||||
Paw = 128,
|
||||
EarlyAdopter = 256,
|
||||
ReservedRelevantJokeBadge1 = 512,
|
||||
ReservedRelevantJokeBadge2 = 1024,
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags attributed to users
|
||||
*/
|
||||
export enum UserFlags {
|
||||
Suspended = 1,
|
||||
Deleted = 2,
|
||||
Banned = 4,
|
||||
}
|
||||
+3
-10
@@ -1,15 +1,8 @@
|
||||
import { config } from "dotenv";
|
||||
|
||||
import { createEventClient } from "./events/client";
|
||||
import { Client } from "./Client";
|
||||
|
||||
config();
|
||||
|
||||
const ws = createEventClient(1);
|
||||
|
||||
ws.on("event", (event) =>
|
||||
console.info("[EVENT]", JSON.stringify(event).substring(0, 32))
|
||||
);
|
||||
ws.on("state", (state) => console.info("STATE =", state));
|
||||
ws.on("error", (error) => console.error("ERROR =", error));
|
||||
|
||||
ws.connect("wss://ws.revolt.chat", process.env.TOKEN!);
|
||||
const client = new Client();
|
||||
client.connect();
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { SetStoreFunction, createStore } from "solid-js/store";
|
||||
|
||||
import { Hydrators, hydrate } from "../hydration";
|
||||
|
||||
/**
|
||||
* Wrapper around Solid.js store
|
||||
*/
|
||||
export class ObjectStorage<T> {
|
||||
private store: Record<string, T>;
|
||||
readonly set: SetStoreFunction<Record<string, T>>;
|
||||
|
||||
/**
|
||||
* Create new object storage
|
||||
*/
|
||||
constructor() {
|
||||
const [store, setStore] = createStore({});
|
||||
this.store = store as never;
|
||||
this.set = setStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object by ID
|
||||
* @param id ID
|
||||
* @returns Object
|
||||
*/
|
||||
get(id: string) {
|
||||
return this.store[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Hydrate some data into storage
|
||||
* @param id ID
|
||||
* @param type Hydration type
|
||||
* @param data Input Data
|
||||
*/
|
||||
hydrate(id: string, type: keyof Hydrators, data?: unknown) {
|
||||
if (data) {
|
||||
this.set(id, hydrate(type, data as never) as T);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user