refactor: add File class

This commit is contained in:
Paul Makles
2023-04-09 17:15:05 +01:00
parent 26cbd52289
commit 4c5044ac5f
22 changed files with 203 additions and 134 deletions
-60
View File
@@ -348,55 +348,6 @@ export class Client extends EventEmitter<Events> {
.replace(RE_SPOILER, "<spoiler>");
}
/**
* 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
*/
createFileURL(
attachment?: {
tag: string;
_id: string;
content_type?: string;
metadata?: Metadata;
},
...args: FileArgs
) {
const [options, allowAnimation, fallback] = args;
const autumn = this.configuration?.features.autumn;
if (!autumn?.enabled) return fallback;
if (!attachment) return fallback;
const { tag, _id, content_type, metadata } = attachment;
// TODO: server-side
if (metadata?.type === "Image") {
if (
Math.min(metadata.width, metadata.height) <= 0 ||
(content_type === "image/gif" &&
Math.max(metadata.width, metadata.height) >= 4096)
)
return fallback;
}
let query = "";
if (options) {
if (!allowAnimation || content_type !== "image/gif") {
query =
"?" +
Object.keys(options)
.map((k) => `${k}=${options[k as keyof FileArgs[0]]}`)
.join("&");
}
}
return `${autumn.url}/${tag}/${_id}${query}`;
}
/**
* Proxy a file through January.
* @param url URL to proxy
@@ -410,14 +361,3 @@ export class Client extends EventEmitter<Events> {
}
}
}
export type FileArgs = [
options?: {
max_side?: number;
size?: number;
width?: number;
height?: number;
},
allowAnimation?: boolean,
fallback?: string
];
+6 -15
View File
@@ -291,11 +291,8 @@ export class Channel {
* URL to the channel icon
*/
get iconURL() {
return this.#collection.client.createFileURL(
this.icon ?? this.recipient?.avatar,
{
max_side: 256,
}
return (
this.icon?.createFileURL({ max_side: 256 }) ?? this.recipient?.avatarURL
);
}
@@ -303,22 +300,16 @@ export class Channel {
* URL to a small variant of the channel icon
*/
get smallIconURL() {
return this.#collection.client.createFileURL(
this.icon ?? this.recipient?.avatar,
{
max_side: 64,
}
);
return this.icon?.createFileURL({ max_side: 64 });
}
/**
* URL to the animated channel icon
*/
get animatedIconURL() {
return this.#collection.client.createFileURL(
this.icon ?? this.recipient?.avatar,
{ max_side: 256 },
true
return (
this.icon?.createFileURL({ max_side: 256 }, true) ??
this.recipient?.animatedAvatarURL
);
}
+128
View File
@@ -0,0 +1,128 @@
import { Metadata } from "revolt-api";
import { API, Client } from "..";
/**
* Uploaded File
*/
export class File {
#client: Client;
/**
* File Id
*/
readonly id: string;
/**
* File bucket
*/
readonly tag: string;
/**
* Original filename
*/
readonly filename: string;
/**
* Parsed metadata of the file
*/
readonly metadata: Metadata;
/**
* Raw content type of this file
*/
readonly contentType: string;
/**
* Size of the file (in bytes)
*/
readonly size: number;
/**
* Construct file
* @param client Client
* @param file File
*/
constructor(client: Client, file: API.File) {
this.#client = client;
this.id = file._id;
this.tag = file.tag;
this.filename = file.filename;
this.metadata = file.metadata;
this.contentType = file.content_type;
this.size = file.size;
}
/**
* Direct URL to the file
*/
get url() {
return `${this.#client.configuration?.features.autumn}/${this.tag}/${
this.id
}/${this.filename}`;
}
/**
* Download URL for the file
*/
get downloadURL() {
return `${this.#client.configuration?.features.autumn}/${
this.tag
}/download/${this.id}/${this.filename}`;
}
/**
* Human readable file size
*/
get humanReadableSize() {
if (this.size > 1e6) {
return `${(this.size / 1e6).toFixed(2)} MB`;
} else if (this.size > 1e3) {
return `${(this.size / 1e3).toFixed(2)} KB`;
}
return `${this.size} B`;
}
/**
* Creates a URL to a given file with given options.
* @param options Optional query parameters to modify object
* @param allowAnimation Returns GIF if applicable, no operations occur on image
* @returns Generated URL or nothing
*/
createFileURL(
options?: {
max_side?: number;
size?: number;
width?: number;
height?: number;
},
allowAnimation?: boolean
) {
const autumn = this.#client.configuration?.features.autumn;
if (!autumn?.enabled) return;
// TODO: server-side
if (this.metadata.type === "Image") {
if (
Math.min(this.metadata.width, this.metadata.height) <= 0 ||
(this.contentType === "image/gif" &&
Math.max(this.metadata.width, this.metadata.height) >= 4096)
)
return;
}
let query = "";
if (options) {
if (!allowAnimation || this.contentType !== "image/gif") {
query =
"?" +
Object.keys(options)
.map((k) => `${k}=${options[k as keyof typeof options]}`)
.join("&");
}
}
return `${autumn.url}/${this.tag}/${this.id}${query}`;
}
}
+3 -9
View File
@@ -259,27 +259,21 @@ export class Server {
* URL to the server's icon
*/
get iconURL() {
return this.#collection.client.createFileURL(this.icon, { max_side: 256 });
return this.icon?.createFileURL({ max_side: 256 });
}
/**
* URL to the server's animated icon
*/
get animatedIconURL() {
return this.#collection.client.createFileURL(
this.icon,
{ max_side: 256 },
true
);
return this.icon?.createFileURL({ max_side: 256 }, true);
}
/**
* URL to the server's banner
*/
get bannerURL() {
return this.#collection.client.createFileURL(this.banner, {
max_side: 256,
});
return this.banner?.createFileURL({ max_side: 256 }, true);
}
/**
+3 -7
View File
@@ -187,8 +187,7 @@ export class ServerMember {
*/
get avatarURL() {
return (
this.#collection.client.createFileURL(this.avatar, { max_side: 256 }) ??
this.user?.avatarURL
this.avatar?.createFileURL({ max_side: 256 }) ?? this.user?.avatarURL
);
}
@@ -197,11 +196,8 @@ export class ServerMember {
*/
get animatedAvatarURL() {
return (
this.#collection.client.createFileURL(
this.avatar,
{ max_side: 256 },
true
) ?? this.user?.animatedAvatarURL
this.avatar?.createFileURL({ max_side: 256 }, true) ??
this.user?.animatedAvatarURL
);
}
}
+3 -7
View File
@@ -112,8 +112,7 @@ export class User {
*/
get avatarURL() {
return (
this.#collection.client.createFileURL(this.avatar, { max_side: 256 }) ??
this.defaultAvatarURL
this.avatar?.createFileURL({ max_side: 256 }) ?? this.defaultAvatarURL
);
}
@@ -122,11 +121,8 @@ export class User {
*/
get animatedAvatarURL() {
return (
this.#collection.client.createFileURL(
this.avatar,
{ max_side: 256 },
true
) ?? this.defaultAvatarURL
this.avatar?.createFileURL({ max_side: 256 }, true) ??
this.defaultAvatarURL
);
}
+2
View File
@@ -4,3 +4,5 @@ export { Message } from "./Message";
export { Server } from "./Server";
export { ServerMember } from "./ServerMember";
export { User } from "./User";
export { File } from "./File";
export { ChannelUnread } from "./ChannelUnread";
+2 -2
View File
@@ -40,7 +40,7 @@ export class ChannelCollection extends ClassCollection<
return this.get(id)!;
} else {
const instance = new Channel(this, id);
this.create(id, "channel", instance, data);
this.create(id, "channel", instance, this.client, data);
isNew && this.client.emit("channelCreate", instance);
return instance;
}
@@ -55,7 +55,7 @@ export class ChannelCollection extends ClassCollection<
return this.get(id)!;
} else if (this.client.options.partials) {
const instance = new Channel(this, id);
this.create(id, "channel", instance, {
this.create(id, "channel", instance, this.client, {
id,
partial: true,
});
+1 -1
View File
@@ -36,7 +36,7 @@ export class ChannelUnreadCollection extends ClassCollection<
return this.get(id)!;
} else {
const instance = new ChannelUnread(this, id);
this.create(id, "channelUnread", instance, data);
this.create(id, "channelUnread", instance, this.client, data);
return instance;
}
}
+9 -2
View File
@@ -121,10 +121,17 @@ export abstract class StoreCollection<T, V> extends Collection<T> {
* @param id Id
* @param type Type
* @param instance Instance
* @param context Context
* @param data Data
*/
create(id: string, type: keyof Hydrators, instance: T, data?: unknown) {
this.#storage.hydrate(id, type, data);
create(
id: string,
type: keyof Hydrators,
instance: T,
context: any,
data?: unknown
) {
this.#storage.hydrate(id, type, context, data);
this.#objects.set(id, instance);
}
+2 -2
View File
@@ -27,7 +27,7 @@ export class EmojiCollection extends ClassCollection<Emoji, HydratedEmoji> {
return this.get(id)!;
} else {
const instance = new Emoji(this, id);
this.create(id, "emoji", instance, data);
this.create(id, "emoji", instance, this.client, data);
isNew && this.client.emit("emojiCreate", instance);
return instance;
}
@@ -42,7 +42,7 @@ export class EmojiCollection extends ClassCollection<Emoji, HydratedEmoji> {
return this.get(id)!;
} else if (this.client.options.partials) {
const instance = new Emoji(this, id);
this.create(id, "emoji", instance, {
this.create(id, "emoji", instance, this.client, {
id,
});
return instance;
+2 -2
View File
@@ -35,7 +35,7 @@ export class MessageCollection extends ClassCollection<
return this.get(id)!;
} else {
const instance = new Message(this, id);
this.create(id, "message", instance, data);
this.create(id, "message", instance, this.client, data);
isNew && this.client.emit("messageCreate", instance);
return instance;
}
@@ -50,7 +50,7 @@ export class MessageCollection extends ClassCollection<
return this.get(id)!;
} else if (this.client.options.partials) {
const instance = new Message(this, id);
this.create(id, "message", instance, {
this.create(id, "message", instance, this.client, {
id,
partial: true,
});
+2 -2
View File
@@ -27,7 +27,7 @@ export class ServerCollection extends ClassCollection<Server, HydratedServer> {
return this.get(id)!;
} else {
const instance = new Server(this, id);
this.create(id, "server", instance, data);
this.create(id, "server", instance, this.client, data);
isNew && this.client.emit("serverCreate", instance);
return instance;
}
@@ -42,7 +42,7 @@ export class ServerCollection extends ClassCollection<Server, HydratedServer> {
return this.get(id)!;
} else if (this.client.options.partials) {
const instance = new Server(this, id);
this.create(id, "server", instance, {
this.create(id, "server", instance, this.client, {
id,
partial: true,
});
+8 -2
View File
@@ -52,7 +52,13 @@ export class ServerMemberCollection extends ClassCollection<
return this.getByKey(id)!;
} else {
const instance = new ServerMember(this, id);
this.create(id.server + id.user, "serverMember", instance, data);
this.create(
id.server + id.user,
"serverMember",
instance,
this.client,
data
);
return instance;
}
}
@@ -66,7 +72,7 @@ export class ServerMemberCollection extends ClassCollection<
return this.getByKey(id)!;
} else if (this.client.options.partials) {
const instance = new ServerMember(this, id);
this.create(id.server + id.user, "serverMember", instance, {
this.create(id.server + id.user, "serverMember", instance, this.client, {
id,
partial: true,
});
+2 -2
View File
@@ -27,7 +27,7 @@ export class UserCollection extends ClassCollection<User, HydratedUser> {
return this.get(id)!;
} else {
const instance = new User(this, id);
this.create(id, "user", instance, data);
this.create(id, "user", instance, this.client, data);
return instance;
}
}
@@ -41,7 +41,7 @@ export class UserCollection extends ClassCollection<User, HydratedUser> {
return this.get(id)!;
} else if (this.client.options.partials) {
const instance = new User(this, id);
this.create(id, "user", instance, {
this.create(id, "user", instance, this.client, {
id,
partial: true,
});
+3 -2
View File
@@ -1,6 +1,7 @@
import { ReactiveSet } from "@solid-primitives/set";
import { Channel as ApiChannel, File, OverrideField } from "revolt-api";
import { Channel as ApiChannel, OverrideField } from "revolt-api";
import { File } from "..";
import type { Merge } from "../lib/merge";
import { Hydrate } from ".";
@@ -46,7 +47,7 @@ export const channelHydration: Hydrate<Merge<ApiChannel>, HydratedChannel> = {
channelType: (channel) => channel.channel_type,
name: (channel) => channel.name,
description: (channel) => channel.description!,
icon: (channel) => channel.icon!,
icon: (channel, ctx) => new File(ctx, channel.icon!),
active: (channel) => channel.active || false,
typingIds: () => new ReactiveSet(),
recipientIds: (channel) => new ReactiveSet(channel.recipients),
+7 -4
View File
@@ -19,7 +19,7 @@ export type { HydratedUser } from "./user";
*/
export type MappingFns<Input, Output, Key extends keyof Output> = Record<
Key,
(value: Input) => Output[Key]
(value: Input, context: any) => Output[Key]
>;
/**
@@ -44,13 +44,14 @@ export type Hydrate<Input, Output> = {
*/
function hydrateInternal<Input extends object, Output>(
hydration: Hydrate<Input, Output>,
input: Input
input: Input,
context: any
): 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);
value = hydration.functions[targetKey as keyof Output](input, context);
} catch (err) {
if (key === "type") return acc;
console.debug(`Skipping key ${String(key)} during hydration!`);
@@ -92,10 +93,12 @@ type ExtractOutput<T> = T extends Hydrate<any, infer O> ? O : never;
export function hydrate<T extends keyof Hydrators>(
type: T,
input: Partial<ExtractInput<Hydrators[T]>>,
context: any,
initial?: boolean
) {
return hydrateInternal(
hydrators[type] as never,
initial ? { ...hydrators[type].initialHydration(), ...input } : input
initial ? { ...hydrators[type].initialHydration(), ...input } : input,
context
) as ExtractOutput<Hydrators[T]>;
}
+3 -2
View File
@@ -3,12 +3,12 @@ import { ReactiveSet } from "@solid-primitives/set";
import {
Message as ApiMessage,
Embed,
File,
Interactions,
Masquerade,
SystemMessage,
} from "revolt-api";
import { File } from "..";
import type { Merge } from "../lib/merge";
import { Hydrate } from ".";
@@ -47,7 +47,8 @@ export const messageHydration: Hydrate<Merge<ApiMessage>, HydratedMessage> = {
authorId: (message) => message.author,
content: (message) => message.content!,
systemMessage: (message) => message.system!,
attachments: (message) => message.attachments!,
attachments: (message, ctx) =>
message.attachments!.map((file) => new File(ctx, file)),
editedAt: (message) => new Date(message.edited!),
embeds: (message) => message.embeds!,
mentionIds: (message) => message.mentions!,
+4 -3
View File
@@ -3,11 +3,12 @@ import { ReactiveSet } from "@solid-primitives/set";
import {
Server as ApiServer,
Category,
File,
Role,
SystemMessageChannels,
} from "revolt-api";
import { File } from "..";
import { Hydrate } from ".";
export type HydratedServer = {
@@ -54,8 +55,8 @@ export const serverHydration: Hydrate<ApiServer, HydratedServer> = {
Object.keys(server.roles!).map((id) => [id, server.roles![id]])
),
defaultPermissions: (server) => server.default_permissions,
icon: (server) => server.icon!,
banner: (server) => server.banner!,
icon: (server, ctx) => new File(ctx, server.icon!),
banner: (server, ctx) => new File(ctx, server.banner!),
flags: (server) => server.flags!,
analytics: (server) => server.analytics || false,
discoverable: (server) => server.discoverable || false,
+3 -2
View File
@@ -1,5 +1,6 @@
import { Member as ApiMember, File, MemberCompositeKey } from "revolt-api";
import { Member as ApiMember, MemberCompositeKey } from "revolt-api";
import { File } from "..";
import type { Merge } from "../lib/merge";
import { Hydrate } from ".";
@@ -25,7 +26,7 @@ export const serverMemberHydration: Hydrate<
id: (member) => member._id,
joinedAt: (member) => new Date(member.joined_at),
nickname: (member) => member.nickname!,
avatar: (member) => member.avatar!,
avatar: (member, ctx) => new File(ctx, member.avatar!),
roles: (member) => member.roles,
timeout: (member) => new Date(member.timeout!),
},
+7 -6
View File
@@ -1,11 +1,12 @@
import {
User as ApiUser,
BotInformation,
File,
RelationshipStatus,
UserStatus,
} from "revolt-api";
import { File } from "..";
import { Hydrate } from ".";
export type HydratedUser = {
@@ -33,15 +34,15 @@ export const userHydration: Hydrate<ApiUser, HydratedUser> = {
username: (user) => user.username,
relationship: (user) => user.relationship ?? "None",
online: (user) => user.online || false,
privileged: (user) => user.privileged || false,
online: (user) => user.online!,
privileged: (user) => user.privileged,
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,
avatar: (user, ctx) => new File(ctx, user.avatar!),
status: (user) => user.status!,
bot: (user) => user.bot!,
},
initialHydration: () => ({
relationship: "None",
+3 -2
View File
@@ -32,11 +32,12 @@ export class ObjectStorage<T> {
* Hydrate some data into storage
* @param id ID
* @param type Hydration type
* @param context Context
* @param data Input Data
*/
hydrate(id: string, type: keyof Hydrators, data?: unknown) {
hydrate(id: string, type: keyof Hydrators, context: any, data?: unknown) {
if (data) {
this.set(id, hydrate(type, data as never, true) as T);
this.set(id, hydrate(type, data as never, context, true) as T);
}
}
}