From 54aae5de590326a5b0395d56fdcdd84981b21e8a Mon Sep 17 00:00:00 2001 From: Aura Date: Sat, 2 Nov 2024 09:20:01 +0100 Subject: [PATCH] feat(anilist): add share button for hidden responses (#245) --- package.json | 1 + src/commands/search/anime.ts | 6 +- src/commands/search/manga.ts | 6 +- src/interaction-handlers/anilist.ts | 23 +++ src/lib/i18n/LanguageKeys/Commands/AniList.ts | 3 + src/lib/structures/AnimeCommand.ts | 140 +------------ src/lib/utilities/anilist/index.ts | 3 + src/lib/utilities/anilist/response.ts | 191 ++++++++++++++++++ .../anilist/types.ts} | 2 +- .../anilist/utilities.ts} | 2 +- src/lib/utilities/discord-utilities.ts | 26 ++- src/locales/en-US/commands/anilist.json | 4 +- src/tsconfig.json | 4 +- 13 files changed, 266 insertions(+), 145 deletions(-) create mode 100644 src/interaction-handlers/anilist.ts create mode 100644 src/lib/utilities/anilist/index.ts create mode 100644 src/lib/utilities/anilist/response.ts rename src/lib/{apis/anilist/anilist-types.ts => utilities/anilist/types.ts} (98%) rename src/lib/{apis/anilist/anilist-utilities.ts => utilities/anilist/utilities.ts} (99%) diff --git a/package.json b/package.json index dbaa130d..fee0886a 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "main": "dist/main.js", "type": "module", "imports": { + "#lib/anilist": "./dist/lib/utilities/anilist/index.js", "#lib/*": "./dist/lib/*.js" }, "scripts": { diff --git a/src/commands/search/anime.ts b/src/commands/search/anime.ts index 3fc08ab2..30b64017 100644 --- a/src/commands/search/anime.ts +++ b/src/commands/search/anime.ts @@ -1,4 +1,4 @@ -import { anilistAnimeGet, anilistAnimeSearch } from '#lib/apis/anilist/anilist-utilities'; +import { anilistAnimeGet, anilistAnimeSearch, handleAniListResult } from '#lib/anilist'; import { LanguageKeys } from '#lib/i18n/LanguageKeys'; import { AnimeCommand } from '#lib/structures/AnimeCommand'; import { Command, RegisterCommand } from '@skyra/http-framework'; @@ -17,13 +17,15 @@ const Root = LanguageKeys.Commands.AniList; ) export class UserCommand extends AnimeCommand<'anime'> { public override async chatInputRun(interaction: Command.ChatInputInteraction, options: AnimeCommand.Arguments<'anime'>) { - return this.handleResult({ + const response = handleAniListResult({ interaction, result: await anilistAnimeGet(options.anime), kind: 'anime', hideDescription: options['hide-description'], hide: options.hide }); + + return interaction.reply(response); } protected override autocompleteFetch(options: AnimeCommand.AutocompleteArguments<'anime'>) { diff --git a/src/commands/search/manga.ts b/src/commands/search/manga.ts index 72d4bde3..2ed1b4cd 100644 --- a/src/commands/search/manga.ts +++ b/src/commands/search/manga.ts @@ -1,4 +1,4 @@ -import { anilistMangaGet, anilistMangaSearch } from '#lib/apis/anilist/anilist-utilities'; +import { anilistMangaGet, anilistMangaSearch, handleAniListResult } from '#lib/anilist'; import { LanguageKeys } from '#lib/i18n/LanguageKeys'; import { AnimeCommand } from '#lib/structures/AnimeCommand'; import { Command, RegisterCommand } from '@skyra/http-framework'; @@ -17,13 +17,15 @@ const Root = LanguageKeys.Commands.AniList; ) export class UserCommand extends AnimeCommand<'manga'> { public override async chatInputRun(interaction: Command.ChatInputInteraction, options: AnimeCommand.Arguments<'manga'>) { - return this.handleResult({ + const response = handleAniListResult({ interaction, result: await anilistMangaGet(options.manga), kind: 'manga', hideDescription: options['hide-description'], hide: options.hide }); + + return interaction.reply(response); } protected override autocompleteFetch(options: AnimeCommand.AutocompleteArguments<'manga'>) { diff --git a/src/interaction-handlers/anilist.ts b/src/interaction-handlers/anilist.ts new file mode 100644 index 00000000..42c68469 --- /dev/null +++ b/src/interaction-handlers/anilist.ts @@ -0,0 +1,23 @@ +import { anilistAnimeGet, anilistMangaGet, handleAniListResult } from '#lib/anilist'; +import { InteractionHandler, type Interactions } from '@skyra/http-framework'; + +export class UserHandler extends InteractionHandler { + public async run(interaction: Interactions.MessageComponentButton, parameters: Parameters) { + const kind = parameters[0]; + const hideDescription = parameters[1]; + const id = Number(parameters[2]); + + const result = await (kind === 'anime' ? anilistAnimeGet : anilistMangaGet)(id); + const response = handleAniListResult({ + interaction, + result, + kind, + hideDescription: hideDescription === '1', + hide: false + }); + + return interaction.reply(response); + } +} + +type Parameters = [kind: 'anime' | 'manga', hideDescription: '0' | '1', id: `${number}`]; diff --git a/src/lib/i18n/LanguageKeys/Commands/AniList.ts b/src/lib/i18n/LanguageKeys/Commands/AniList.ts index 03634fdb..4633d6ec 100644 --- a/src/lib/i18n/LanguageKeys/Commands/AniList.ts +++ b/src/lib/i18n/LanguageKeys/Commands/AniList.ts @@ -35,3 +35,6 @@ export const CountryChina = T('commands/anilist:countryChina'); export const CountryJapan = T('commands/anilist:countryJapan'); export const CountryKorea = T('commands/anilist:countryKorea'); export const CountryTaiwan = T('commands/anilist:countryTaiwan'); + +export const ButtonSource = T('commands/anilist:buttonSource'); +export const ButtonShare = T('commands/anilist:buttonShare'); diff --git a/src/lib/structures/AnimeCommand.ts b/src/lib/structures/AnimeCommand.ts index 170ea740..e16d6812 100644 --- a/src/lib/structures/AnimeCommand.ts +++ b/src/lib/structures/AnimeCommand.ts @@ -1,17 +1,12 @@ -import { MediaFormat, type MediaTitle } from '#lib/apis/anilist/anilist-types'; -import { parseAniListDescription, type AnilistEntryTypeByKind } from '#lib/apis/anilist/anilist-utilities'; -import { BrandingColors } from '#lib/common/constants'; +import { getAniListTitle, MediaFormat, type AnilistEntryTypeByKind } from '#lib/anilist'; import { LanguageKeys } from '#lib/i18n/LanguageKeys'; -import { durationFormatter } from '#lib/utilities/duration-formatter'; -import { minutes } from '#lib/utilities/time-utilities'; -import { EmbedBuilder, bold, hideLinkEmbed, hyperlink } from '@discordjs/builders'; import { AniListSearchTitleLanguage } from '@prisma/client'; import type { Result } from '@sapphire/result'; -import { cutText, filterNullish, isNullishOrEmpty, isNullishOrZero } from '@sapphire/utilities'; -import { Command, type AutocompleteInteractionArguments, type MessageResponseOptions } from '@skyra/http-framework'; -import { getSupportedLanguageT, getSupportedUserLanguageT, resolveUserKey, type TFunction, type TypedT } from '@skyra/http-framework-i18n'; +import { cutText, isNullishOrEmpty, isNullishOrZero } from '@sapphire/utilities'; +import { Command, type AutocompleteInteractionArguments } from '@skyra/http-framework'; +import { getSupportedUserLanguageT, type TFunction, type TypedT } from '@skyra/http-framework-i18n'; import type { FetchError } from '@skyra/safe-fetch'; -import { MessageFlags, type LocaleString } from 'discord-api-types/v10'; +import type { LocaleString } from 'discord-api-types/v10'; const Root = LanguageKeys.Commands.AniList; @@ -49,128 +44,13 @@ export abstract class AnimeCommand extends Comma const kind = t(isNullishOrEmpty(value.format) ? Root.Unknown : FormatKeys[value.format]); const description = ` — ${year} ${kind}`; - const title = this.getTitle(value.title, t.lng as LocaleString, value.countryOfOrigin, titleLanguage); + const title = getAniListTitle(value.title, t.lng as LocaleString, value.countryOfOrigin, titleLanguage); return `${cutText(title, 100 - description.length)}${description}`; } - protected handleResult(options: HandlerOptions) { - const { interaction, kind } = options; - const hide = options.hide ?? false; - const hideDescription = options.hideDescription ?? false; - - const t = hide ? getSupportedUserLanguageT(interaction) : getSupportedLanguageT(interaction); - const response = options.result.match({ - ok: (value) => - isNullishOrEmpty(value) ? this.createErrorResponse(interaction, kind) : this.createResponse(value, t, hideDescription, hide), - err: () => this.createErrorResponse(interaction, kind) - }); - return interaction.reply(response); - } - - protected createResponse(value: AnilistEntryTypeByKind, t: TFunction, hideDescription: boolean, hide: boolean): MessageResponseOptions { - return { embeds: [this.createEmbed(value, t, hideDescription).toJSON()], flags: hide ? MessageFlags.Ephemeral : undefined }; - } - - protected createErrorResponse(interaction: Command.ChatInputInteraction, kind: Kind) { - return { - content: resolveUserKey(interaction, kind === 'anime' ? Root.Anime.SearchError : Root.Manga.SearchError), - flags: MessageFlags.Ephemeral - }; - } - protected abstract autocompleteFetch( options: AnimeCommand.AutocompleteArguments ): Promise[], FetchError>>; - - private createEmbed(value: AnilistEntryTypeByKind, t: TFunction, hideDescription: boolean) { - const anilistTitles = t(Root.EmbedTitles); - const description = [ - `**${anilistTitles.romajiName}**: ${value.title.romaji || t(LanguageKeys.Common.None)}`, - `**${anilistTitles.englishName}**: ${value.title.english || t(LanguageKeys.Common.None)}`, - `**${anilistTitles.nativeName}**: ${value.title.native || t(LanguageKeys.Common.None)}` - ]; - - if (value.countryOfOrigin) { - description.push(`${bold(anilistTitles.countryOfOrigin)}: ${this.getCountry(t, value.countryOfOrigin)}`); - } - - if ('episodes' in value && value.episodes) { - description.push(`${bold(anilistTitles.episodes)}: ${t(LanguageKeys.Common.FormatNumber, { value: value.episodes })}`); - } - - if ('chapters' in value && value.chapters) { - description.push(`${bold(anilistTitles.chapters)}: ${t(LanguageKeys.Common.FormatNumber, { value: value.chapters })}`); - } - - if ('volumes' in value && value.volumes) { - description.push(`${bold(anilistTitles.volumes)}: ${t(LanguageKeys.Common.FormatNumber, { value: value.volumes })}`); - } - - if ('duration' in value && value.duration) { - description.push(`${bold(anilistTitles.episodeLength)}: ${durationFormatter.format(minutes(value.duration), 1)}`); - } - - if (value.externalLinks?.length) { - const externalLinks = value.externalLinks - .map((link) => (link?.url && link.site ? hyperlink(link.site, hideLinkEmbed(link.url)) : undefined)) - .filter(filterNullish); - - if (externalLinks.length) { - description.push(`${bold(anilistTitles.externalLinks)}: ${t(LanguageKeys.Common.FormatList, { value: externalLinks })}`); - } - } - - if (!hideDescription && value.description) { - description.push('', parseAniListDescription(value.description)); - } - - const locale = t.lng as LocaleString; - return new EmbedBuilder() - .setColor(BrandingColors.Primary) - .setTitle(this.getTitle(value.title, locale, value.countryOfOrigin, AniListSearchTitleLanguage.English)) - .setURL(value.siteUrl ?? null) - .setDescription(description.join('\n')) - .setImage(`https://img.anili.st/media/${value.id}`) - .setFooter({ text: '© anilist.co' }); - } - - private getCountry(t: TFunction, origin: NonNullable['countryOfOrigin']>) { - switch (origin) { - case 'CN': - return `${t(Root.CountryChina)} 🇨🇳`; - case 'JP': - return `${t(Root.CountryJapan)} 🇯🇵`; - case 'KR': - return `${t(Root.CountryKorea)} 🇰🇷`; - case 'TW': - return `${t(Root.CountryTaiwan)} 🇹🇼`; - default: - this.container.logger.warn(`[ANILIST] Received unknown origin: ${origin}`); - return origin; - } - } - - private getTitle(title: MediaTitle, locale: LocaleString, origin: string | null | undefined, titleLanguage: AniListSearchTitleLanguage) { - switch (titleLanguage) { - case AniListSearchTitleLanguage.English: - return title.english ?? title.romaji ?? title.native!; - case AniListSearchTitleLanguage.Romaji: - return title.romaji ?? title.english ?? title.native!; - case AniListSearchTitleLanguage.Native: - return title.native ?? title.romaji ?? title.english!; - default: - return this.shouldUseNative(locale, origin ?? 'JP') - ? (title.native ?? title.english ?? title.romaji!) - : (title.english ?? title.romaji ?? title.native!); - } - } - - private shouldUseNative(locale: LocaleString, origin: string) { - if (locale === 'ja') return origin === 'JP'; - if (locale === 'zh-CN') return origin === 'CN'; - if (locale === 'ko') return origin === 'KR'; - return false; - } } export namespace AnimeCommand { @@ -180,14 +60,6 @@ export namespace AnimeCommand { export type AutocompleteArguments = AutocompleteInteractionArguments>; } -export interface HandlerOptions { - interaction: Command.ChatInputInteraction; - result: Result | null, FetchError>; - kind: Kind; - hideDescription: boolean | null | undefined; - hide: boolean | null | undefined; -} - type Pretty = { [K in keyof Type]: Type[K] }; type MakeArguments = Pretty< { [key in Kind]: Value } & { hide?: boolean; 'hide-description'?: boolean } diff --git a/src/lib/utilities/anilist/index.ts b/src/lib/utilities/anilist/index.ts new file mode 100644 index 00000000..a44c88ca --- /dev/null +++ b/src/lib/utilities/anilist/index.ts @@ -0,0 +1,3 @@ +export * from '#lib/utilities/anilist/response'; +export * from '#lib/utilities/anilist/types'; +export * from '#lib/utilities/anilist/utilities'; diff --git a/src/lib/utilities/anilist/response.ts b/src/lib/utilities/anilist/response.ts new file mode 100644 index 00000000..178a7602 --- /dev/null +++ b/src/lib/utilities/anilist/response.ts @@ -0,0 +1,191 @@ +import { BrandingColors } from '#lib/common/constants'; +import { LanguageKeys } from '#lib/i18n/LanguageKeys'; +import type { MediaTitle } from '#lib/utilities/anilist/types'; +import { parseAniListDescription, type AnilistEntryTypeByKind } from '#lib/utilities/anilist/utilities'; +import { makeActionButton, makeActionRow, makeLinkButton } from '#lib/utilities/discord-utilities'; +import { durationFormatter } from '#lib/utilities/duration-formatter'; +import { minutes } from '#lib/utilities/time-utilities'; +import { bold, EmbedBuilder, hideLinkEmbed, hyperlink } from '@discordjs/builders'; +import { AniListSearchTitleLanguage } from '@prisma/client'; +import type { Result } from '@sapphire/result'; +import { filterNullish, isNullishOrEmpty } from '@sapphire/utilities'; +import { container, type Interactions, type MessageResponseOptions } from '@skyra/http-framework'; +import { getSupportedLanguageT, getSupportedUserLanguageT, resolveUserKey, type TFunction } from '@skyra/http-framework-i18n'; +import type { FetchError } from '@skyra/safe-fetch'; +import { ButtonStyle, MessageFlags, type APIButtonComponent, type APIMessageComponentEmoji, type LocaleString } from 'discord-api-types/v10'; + +type AniListKind = 'anime' | 'manga'; + +const Root = LanguageKeys.Commands.AniList; +const EmojiShare: APIMessageComponentEmoji = { id: '1302063661505839154', name: 'share', animated: false }; + +export function handleAniListResult(options: HandlerOptions) { + const { interaction, kind } = options; + const hide = options.hide ?? false; + const hideDescription = options.hideDescription ?? false; + + const t = hide ? getSupportedUserLanguageT(interaction) : getSupportedLanguageT(interaction); + return options.result.match({ + ok: (value) => (isNullishOrEmpty(value) ? createErrorResponse(interaction, kind) : createResponse(kind, value, t, hideDescription, hide)), + err: () => createErrorResponse(interaction, kind) + }); +} + +function createResponse( + kind: AniListKind, + value: AnilistEntryTypeByKind, + t: TFunction, + hideDescription: boolean, + hide: boolean +): MessageResponseOptions { + const buttons: APIButtonComponent[] = []; + if (value.siteUrl) { + const button = makeLinkButton({ + url: value.siteUrl, + label: t(Root.ButtonSource) + }); + buttons.push(button); + } + + if (hide) { + const button = makeActionButton({ + style: ButtonStyle.Primary, + custom_id: `anilist.${kind}.${hideDescription ? '1' : '0'}.${value.id}`, + label: t(Root.ButtonShare), + emoji: EmojiShare + }); + buttons.push(button); + } + + return { + embeds: [createEmbed(value, t, hideDescription).toJSON()], + components: [makeActionRow(buttons)], + flags: hide ? MessageFlags.Ephemeral : undefined + }; +} + +function createErrorResponse(interaction: SupportedInteraction, kind: AniListKind) { + return { + content: resolveUserKey(interaction, kind === 'anime' ? Root.Anime.SearchError : Root.Manga.SearchError), + flags: MessageFlags.Ephemeral + }; +} + +function createEmbed(value: AnilistEntryTypeByKind, t: TFunction, hideDescription: boolean) { + const locale = t.lng as LocaleString; + return new EmbedBuilder() + .setColor(BrandingColors.Primary) + .setTitle(getAniListTitle(value.title, locale, value.countryOfOrigin, AniListSearchTitleLanguage.English)) + .setURL(value.siteUrl ?? null) + .setDescription(createEmbedDescription(value, t, hideDescription)) + .setImage(`https://img.anili.st/media/${value.id}`) + .setFooter({ text: '© anilist.co' }); +} + +function createEmbedDescription(value: AnilistEntryTypeByKind, t: TFunction, hideDescription: boolean): string { + const anilistTitles = t(Root.EmbedTitles); + const description = [ + createEmbedDescriptionLanguageName(t, anilistTitles.romajiName, value.title.romaji), + createEmbedDescriptionLanguageName(t, anilistTitles.englishName, value.title.english), + createEmbedDescriptionLanguageName(t, anilistTitles.nativeName, value.title.native) + ]; + + if (value.countryOfOrigin) { + description.push(`${bold(anilistTitles.countryOfOrigin)}: ${getCountry(t, value.countryOfOrigin)}`); + } + + if ('episodes' in value && value.episodes) { + description.push(`${bold(anilistTitles.episodes)}: ${t(LanguageKeys.Common.FormatNumber, { value: value.episodes })}`); + } + + if ('chapters' in value && value.chapters) { + description.push(`${bold(anilistTitles.chapters)}: ${t(LanguageKeys.Common.FormatNumber, { value: value.chapters })}`); + } + + if ('volumes' in value && value.volumes) { + description.push(`${bold(anilistTitles.volumes)}: ${t(LanguageKeys.Common.FormatNumber, { value: value.volumes })}`); + } + + if ('duration' in value && value.duration) { + description.push(`${bold(anilistTitles.episodeLength)}: ${durationFormatter.format(minutes(value.duration), 1)}`); + } + + const externalLinks = createEmbedDescriptionExternalLinks(t, anilistTitles.externalLinks, value.externalLinks); + if (!isNullishOrEmpty(externalLinks)) { + description.push(externalLinks); + } + + if (!hideDescription && value.description) { + description.push('', parseAniListDescription(value.description)); + } + + return description.join('\n'); +} + +function createEmbedDescriptionLanguageName(t: TFunction, title: string, value: string | null | undefined) { + return `${bold(title)}: ${isNullishOrEmpty(value) ? t(LanguageKeys.Common.None) : value}`; +} + +type ExternalLinks = AnilistEntryTypeByKind['externalLinks']; +function createEmbedDescriptionExternalLinks(t: TFunction, title: string, links: ExternalLinks): string | null { + if (isNullishOrEmpty(links)) return null; + + const formatted = links // + .map((link) => (link?.url && link.site ? hyperlink(link.site, hideLinkEmbed(link.url)) : undefined)) + .filter(filterNullish); + + return isNullishOrEmpty(formatted) ? null : `${bold(title)}: ${t(LanguageKeys.Common.FormatList, { value: formatted })}`; +} + +function getCountry(t: TFunction, origin: NonNullable['countryOfOrigin']>) { + switch (origin) { + case 'CN': + return `${t(Root.CountryChina)} 🇨🇳`; + case 'JP': + return `${t(Root.CountryJapan)} 🇯🇵`; + case 'KR': + return `${t(Root.CountryKorea)} 🇰🇷`; + case 'TW': + return `${t(Root.CountryTaiwan)} 🇹🇼`; + default: + container.logger.warn(`[ANILIST] Received unknown origin: ${origin}`); + return origin; + } +} + +export function getAniListTitle( + title: MediaTitle, + locale: LocaleString, + origin: string | null | undefined, + titleLanguage: AniListSearchTitleLanguage +) { + switch (titleLanguage) { + case AniListSearchTitleLanguage.English: + return title.english ?? title.romaji ?? title.native!; + case AniListSearchTitleLanguage.Romaji: + return title.romaji ?? title.english ?? title.native!; + case AniListSearchTitleLanguage.Native: + return title.native ?? title.romaji ?? title.english!; + default: + return shouldUseNative(locale, origin ?? 'JP') + ? (title.native ?? title.english ?? title.romaji!) + : (title.english ?? title.romaji ?? title.native!); + } +} + +function shouldUseNative(locale: LocaleString, origin: string) { + if (locale === 'ja') return origin === 'JP'; + if (locale === 'zh-CN') return origin === 'CN'; + if (locale === 'ko') return origin === 'KR'; + return false; +} + +type SupportedInteraction = Interactions.ChatInputCommand | Interactions.MessageComponentButton; + +export interface HandlerOptions { + interaction: SupportedInteraction; + result: Result | null, FetchError>; + kind: Kind; + hideDescription: boolean | null | undefined; + hide: boolean | null | undefined; +} diff --git a/src/lib/apis/anilist/anilist-types.ts b/src/lib/utilities/anilist/types.ts similarity index 98% rename from src/lib/apis/anilist/anilist-types.ts rename to src/lib/utilities/anilist/types.ts index 685462f5..83a0a904 100644 --- a/src/lib/apis/anilist/anilist-types.ts +++ b/src/lib/utilities/anilist/types.ts @@ -38,7 +38,7 @@ export interface AnilistEntryManga extends AnilistEntryBase { } /** An external link to another site related to the media */ -interface MediaExternalLink { +export interface MediaExternalLink { /** The url of the external link */ readonly url: string; /** The site location of the external link */ diff --git a/src/lib/apis/anilist/anilist-utilities.ts b/src/lib/utilities/anilist/utilities.ts similarity index 99% rename from src/lib/apis/anilist/anilist-utilities.ts rename to src/lib/utilities/anilist/utilities.ts index faf79b53..88ae4058 100644 --- a/src/lib/apis/anilist/anilist-utilities.ts +++ b/src/lib/utilities/anilist/utilities.ts @@ -1,4 +1,4 @@ -import type { AnilistEntryAnime, AnilistEntryManga } from '#lib/apis/anilist/anilist-types'; +import type { AnilistEntryAnime, AnilistEntryManga } from '#lib/utilities/anilist/types'; import { TemporaryCollection } from '#lib/utilities/temporary-collection'; import { Result, ok } from '@sapphire/result'; import { Time } from '@sapphire/time-utilities'; diff --git a/src/lib/utilities/discord-utilities.ts b/src/lib/utilities/discord-utilities.ts index 9e1a896f..a1136a4e 100644 --- a/src/lib/utilities/discord-utilities.ts +++ b/src/lib/utilities/discord-utilities.ts @@ -1,5 +1,29 @@ -import type { APIChannel } from 'discord-api-types/v10'; +import { + ButtonStyle, + ComponentType, + type APIActionRowComponent, + type APIButtonComponentWithCustomId, + type APIButtonComponentWithURL, + type APIChannel, + type APIMessageActionRowComponent +} from 'discord-api-types/v10'; export function isNsfwChannel(channel: Partial): boolean { return 'nsfw' in channel ? (channel.nsfw ?? false) : false; } + +export function makeActionRow( + components: Component[] +): APIActionRowComponent { + return { type: ComponentType.ActionRow, components }; +} + +export type LinkButtonOptions = Omit; +export function makeLinkButton(options: LinkButtonOptions): APIButtonComponentWithURL { + return { type: ComponentType.Button, style: ButtonStyle.Link, ...options }; +} + +export type ActionButtonOptions = Omit; +export function makeActionButton(options: ActionButtonOptions): APIButtonComponentWithCustomId { + return { type: ComponentType.Button, ...options }; +} diff --git a/src/locales/en-US/commands/anilist.json b/src/locales/en-US/commands/anilist.json index a6a55cc4..819313ae 100644 --- a/src/locales/en-US/commands/anilist.json +++ b/src/locales/en-US/commands/anilist.json @@ -28,5 +28,7 @@ "countryChina": "China", "countryJapan": "Japan", "countryKorea": "South Korea", - "countryTaiwan": "Taiwan" + "countryTaiwan": "Taiwan", + "buttonSource": "Source", + "buttonShare": "Share" } diff --git a/src/tsconfig.json b/src/tsconfig.json index 7bc35938..9cd73ebe 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -6,9 +6,7 @@ "outDir": "../dist", "composite": true, "tsBuildInfoFile": "../dist/tsconfig.tsbuildinfo", - "paths": { - "#lib/*": ["lib/*"] - } + "resolvePackageJsonImports": true }, "include": ["."] }