diff --git a/src/classes/MessageEmbed.ts b/src/classes/MessageEmbed.ts new file mode 100644 index 00000000..0be64a50 --- /dev/null +++ b/src/classes/MessageEmbed.ts @@ -0,0 +1,232 @@ +import { API, Client, File } from ".."; + +/** + * Message Embed + */ +export abstract class MessageEmbed { + protected client?: Client; + readonly type: API.Embed["type"]; + + /** + * Construct Embed + * @param client Client + * @param type Type + */ + constructor(client?: Client, type: API.Embed["type"] = "None") { + this.client = client; + this.type = type; + } + + /** + * Create an Embed from an API Embed + * @param client Client + * @param embed Data + * @returns Embed + */ + static from(client: Client, embed: API.Embed): MessageEmbed { + switch (embed.type) { + case "Image": + return new ImageEmbed(client, embed); + case "Video": + return new VideoEmbed(client, embed); + case "Website": + return new WebsiteEmbed(client, embed); + case "Text": + return new TextEmbed(client, embed); + default: + return new UnknownEmbed(client); + } + } +} + +/** + * Embed of unknown type + */ +export class UnknownEmbed extends MessageEmbed {} + +/** + * Image Embed + */ +export class ImageEmbed extends MessageEmbed { + readonly url: string; + readonly width: number; + readonly height: number; + readonly size: API.ImageSize; + + /** + * Construct Image Embed + * @param client Client + * @param embed Embed + */ + constructor( + client: Client, + embed: Omit + ) { + super(client, "Image"); + + this.url = embed.url; + this.width = embed.width; + this.height = embed.height; + this.size = embed.size; + } + + /** + * Proxied image URL + */ + get proxiedURL() { + return this.client?.proxyFile(this.url); + } +} + +/** + * Video Embed + */ +export class VideoEmbed extends MessageEmbed { + readonly url: string; + readonly width: number; + readonly height: number; + + /** + * Construct Video Embed + * @param client Client + * @param embed Embed + */ + constructor( + client: Client, + embed: Omit + ) { + super(client, "Video"); + + this.url = embed.url; + this.width = embed.width; + this.height = embed.height; + } + + /** + * Proxied video URL + */ + get proxiedURL() { + return this.client?.proxyFile(this.url); + } +} + +/** + * Website Embed + */ +export class WebsiteEmbed extends MessageEmbed { + readonly url?: string; + readonly originalUrl?: string; + readonly specialContent?: API.Special; + readonly title?: string; + readonly description?: string; + readonly image?: ImageEmbed; + readonly video?: VideoEmbed; + readonly siteName?: string; + readonly iconUrl?: string; + readonly colour?: string; + + /** + * Construct Video Embed + * @param client Client + * @param embed Embed + */ + constructor( + client: Client, + embed: Omit + ) { + super(client, "Website"); + + this.url = embed.url!; + this.originalUrl = embed.original_url!; + this.specialContent = embed.special!; + this.title = embed.title!; + this.description = embed.description!; + this.image = embed.image ? new ImageEmbed(client, embed.image) : undefined; + this.video = embed.video ? new VideoEmbed(client, embed.video) : undefined; + this.siteName = embed.site_name!; + this.iconUrl = embed.icon_url!; + this.colour = embed.colour!; + } + + /** + * Proxied icon URL + */ + get proxiedIconURL() { + return this.iconUrl ? this.client?.proxyFile(this.iconUrl) : undefined; + } + + /** + * If special content is present, generate the embed URL + */ + get embedURL() { + switch (this.specialContent?.type) { + case "YouTube": { + let timestamp = ""; + + if (this.specialContent.timestamp) { + timestamp = `&start=${this.specialContent.timestamp}`; + } + + return `https://www.youtube-nocookie.com/embed/${this.specialContent.id}?modestbranding=1${timestamp}`; + } + case "Twitch": + return `https://player.twitch.tv/?${this.specialContent.content_type.toLowerCase()}=${ + this.specialContent.id + }&parent=${(window ?? {})?.location?.hostname}&autoplay=false`; + case "Lightspeed": + return `https://new.lightspeed.tv/embed/${this.specialContent.id}/stream`; + case "Spotify": + return `https://open.spotify.com/embed/${this.specialContent.content_type}/${this.specialContent.id}`; + case "Soundcloud": + return `https://w.soundcloud.com/player/?url=${encodeURIComponent( + this.url! + )}&color=%23FF7F50&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true`; + case "Bandcamp": { + return `https://bandcamp.com/EmbeddedPlayer/${this.specialContent.content_type.toLowerCase()}=${ + this.specialContent.id + }/size=large/bgcol=181a1b/linkcol=056cc4/tracklist=false/transparent=true/`; + } + case "Streamable": { + return `https://streamable.com/e/${this.specialContent.id}?loop=0`; + } + } + } +} + +/** + * Text Embed + */ +export class TextEmbed extends MessageEmbed { + readonly iconUrl?: string; + readonly url?: string; + readonly title?: string; + readonly description?: string; + readonly media?: File; + readonly colour?: string; + + /** + * Construct Video Embed + * @param client Client + * @param embed Embed + */ + constructor( + client: Client, + embed: Omit + ) { + super(client, "Text"); + + this.iconUrl = embed.icon_url!; + this.url = embed.url!; + this.title = embed.title!; + this.description = embed.description!; + this.media = embed.media ? new File(client, embed.media) : undefined; + this.colour = embed.colour!; + } + + /** + * Proxied icon URL + */ + get proxiedIconURL() { + return this.iconUrl ? this.client?.proxyFile(this.iconUrl) : undefined; + } +} diff --git a/src/classes/index.ts b/src/classes/index.ts index f20804bc..b57524c5 100644 --- a/src/classes/index.ts +++ b/src/classes/index.ts @@ -6,3 +6,4 @@ export { ServerMember } from "./ServerMember"; export { User } from "./User"; export { File } from "./File"; export { ChannelUnread } from "./ChannelUnread"; +export * from "./MessageEmbed"; diff --git a/src/hydration/message.ts b/src/hydration/message.ts index 0f2c42e6..b69b3cf1 100644 --- a/src/hydration/message.ts +++ b/src/hydration/message.ts @@ -1,14 +1,7 @@ import { ReactiveMap } from "@solid-primitives/map"; import { ReactiveSet } from "@solid-primitives/set"; -import { - Message as ApiMessage, - Embed, - Interactions, - Masquerade, - SystemMessage, -} from "revolt-api"; -import { File } from ".."; +import { API, File, MessageEmbed } from ".."; import type { Merge } from "../lib/merge"; import { Hydrate } from "."; @@ -19,18 +12,18 @@ export type HydratedMessage = { channelId: string; authorId?: string; content?: string; - systemMessage?: SystemMessage; + systemMessage?: API.SystemMessage; attachments?: File[]; editedAt?: Date; - embeds?: Embed[]; + embeds?: MessageEmbed[]; mentionIds?: string[]; replyIds?: string[]; reactions: ReactiveMap>; - interactions?: Interactions; - masquerade?: Masquerade; + interactions?: API.Interactions; + masquerade?: API.Masquerade; }; -export const messageHydration: Hydrate, HydratedMessage> = { +export const messageHydration: Hydrate, HydratedMessage> = { keyMapping: { _id: "id", channel: "channelId", @@ -50,7 +43,8 @@ export const messageHydration: Hydrate, HydratedMessage> = { attachments: (message, ctx) => message.attachments!.map((file) => new File(ctx, file)), editedAt: (message) => new Date(message.edited!), - embeds: (message) => message.embeds!, + embeds: (message, ctx) => + message.embeds!.map((embed) => MessageEmbed.from(ctx, embed)), mentionIds: (message) => message.mentions!, replyIds: (message) => message.replies!, reactions: (message) => {