diff --git a/docs/docs/guide/commands/overview.md b/docs/docs/guide/commands/overview.md index d4ea37950..824437d4e 100644 --- a/docs/docs/guide/commands/overview.md +++ b/docs/docs/guide/commands/overview.md @@ -50,6 +50,8 @@ |[キュー内を検索](./searchqueue.md)|キュー内を検索します。引数にキーワードを指定します。| |[サウンドクラウドを検索](./searchsoundcloud.md)|曲をSoundCloudで検索します。| |[シーク](./seek.md)|楽曲をシークします。| +|[設定>現在再生中](./setting>nowplaying.md)|現在再生中パネルの表示モードの設定をします。何も指定しないと現在の設定を確認できます。| +|[設定>スキップ投票](./setting>skipvote.md)|スキップ投票の有効・無効の設定をします。何も指定しないと現在の設定を確認できます。| |[シャッフル](./shuffle.md)|キューの内容をシャッフルします。| |[スキップ](./skip.md)|状況に応じて現在再生中の曲をスキップするか、スキップ投票を開始します。| |[サムネイル](./thumbnail.md)|現在再生中のサムネイルを表示します。検索パネルが開いていて検索パネル中の番号が指定された場合にはその曲のサムネイルを表示します。| diff --git a/docs/docs/guide/commands/setting_nowplaying.md b/docs/docs/guide/commands/setting_nowplaying.md new file mode 100644 index 000000000..99cc9e8e7 --- /dev/null +++ b/docs/docs/guide/commands/setting_nowplaying.md @@ -0,0 +1,21 @@ +--- +sidebar_label: 設定>現在再生中 +--- +# `設定>現在再生中`コマンド +現在再生中パネルの表示モードの設定をします。何も指定しないと現在の設定を確認できます。 + +スラッシュコマンドでは、`/setting nowplaying`を使用してください。 + +## 別名 +`設定>現在再生中`以外にも以下の別名を使用できます。 + +- setting>nowplaying + + + + +## 実行に必要な権限 +同じボイスチャンネルに接続していてかつDJロールを保持していること, ボイスチャンネルの唯一のユーザーであること, サーバーの管理権限を持っていることのいずれか + +※管理者権限や、サーバーの管理権限、チャンネルの管理権限、および管理者権限を持つユーザーはこの権限を満たしていなくてもいつでもこのコマンドを実行できます。 + diff --git a/docs/docs/guide/commands/setting_skipvote.md b/docs/docs/guide/commands/setting_skipvote.md new file mode 100644 index 000000000..39905a89f --- /dev/null +++ b/docs/docs/guide/commands/setting_skipvote.md @@ -0,0 +1,21 @@ +--- +sidebar_label: 設定>スキップ投票 +--- +# `設定>スキップ投票`コマンド +スキップ投票の有効・無効の設定をします。何も指定しないと現在の設定を確認できます。 + +スラッシュコマンドでは、`/setting skipvote`を使用してください。 + +## 別名 +`設定>スキップ投票`以外にも以下の別名を使用できます。 + +- setting>skipvote + + + + +## 実行に必要な権限 +同じボイスチャンネルに接続していてかつDJロールを保持していること, ボイスチャンネルの唯一のユーザーであること, サーバーの管理権限を持っていることのいずれか + +※管理者権限や、サーバーの管理権限、チャンネルの管理権限、および管理者権限を持つユーザーはこの権限を満たしていなくてもいつでもこのコマンドを実行できます。 + diff --git a/locales/ja/commands.json b/locales/ja/commands.json index 124b9dbd5..27cf30b55 100644 --- a/locales/ja/commands.json +++ b/locales/ja/commands.json @@ -49,7 +49,8 @@ "player": "音楽プレイヤー制御系", "playlist": "プレイリスト操作系", "utility": "ユーティリティ系", - "bot": "ボット操作全般" + "bot": "ボット操作全般", + "settings": "設定" }, "commandList": "コマンド一覧", "toLearnMoreMessage": "`{{prefix}}コマンド 再生`のように、コマンド名を引数につけて、そのコマンドの詳細を表示できます。", @@ -489,6 +490,38 @@ "success": "シークしました", "failed": "シークに失敗しました" }, + "setting": { + "name": "設定", + "description": "ボットの設定を変更します" + }, + "setting>skipvote": { + "name": "設定>スキップ投票", + "description": "スキップ投票の有効・無効の設定をします。何も指定しないと現在の設定を確認できます。", + "args": { + "enabled": { + "description": "スキップ投票を有効にするかどうか。" + } + }, + "currentState": "スキップ投票は現在 **{{status}}** の設定になっています。", + "changed": "スキップ投票を **{{status}}** にしました。" + }, + "setting>nowplaying": { + "name": "設定>現在再生中", + "description": "現在再生中パネルの表示モードの設定をします。何も指定しないと現在の設定を確認できます。", + "args": { + "level": { + "description": "表示モードを指定します。", + "choices": { + "normal": "通常", + "silent": "サイレント", + "disabled": "無効" + } + } + }, + "invalidLevel": "指定されたモードが正しくありません。normal, silent, disabledのいずれかを指定してください。", + "changed": "現在再生中パネルの表示モードを **{{level}}** に設定しました。", + "currentState": "現在再生中パネルの表示モードは現在 **{{level}}** に設定されています。" + }, "shuffle": { "name": "シャッフル", "description": "キューの内容をシャッフルします。", diff --git a/locales/ja/default.json b/locales/ja/default.json index c5c920ab6..c20e39b21 100644 --- a/locales/ja/default.json +++ b/locales/ja/default.json @@ -94,5 +94,7 @@ "attachmentNotFound": "添付ファイルが見つかりません", "invalidUrl": "有効なURLを指定してください。キーワードで再生する場合は`検索`コマンドを使用してください。" }, - "system": "システム" + "system": "システム", + "enabled": "有効", + "disabled": "無効" } diff --git a/src/AudioSource/audiosource.ts b/src/AudioSource/audiosource.ts index 85ab3a9bb..b838aee19 100644 --- a/src/AudioSource/audiosource.ts +++ b/src/AudioSource/audiosource.ts @@ -18,7 +18,6 @@ import type { YouTube } from "./youtube"; import type { LoggerObject } from "../logger"; -import type { i18n } from "i18next"; import type { EmbedField } from "oceanic.js"; import type { Readable } from "stream"; @@ -140,13 +139,13 @@ export abstract class AudioSource>; + abstract init(url: string, prefetched: U | null): Promise>; /** 再生するためのストリームをフェッチします。 */ - abstract fetch(url?: boolean, t?: i18n["t"]): Promise; + abstract fetch(url?: boolean): Promise; /** 現在再生中の曲に関する追加データを生成します。 */ - abstract npAdditional(t: i18n["t"]): string; + abstract npAdditional(): string; /** データをプレーンなオブジェクトにエクスポートします。 */ abstract exportData(): U; diff --git a/src/AudioSource/bestdori.ts b/src/AudioSource/bestdori.ts index 98d7a9334..c5747323b 100644 --- a/src/AudioSource/bestdori.ts +++ b/src/AudioSource/bestdori.ts @@ -19,9 +19,9 @@ import type { AudioSourceBasicJsonFormat, UrlStreamInfo } from "."; import candyget from "candyget"; -import { i18n } from "i18next"; import { AudioSource } from "./audiosource"; +import { getCommandExecutionContext } from "../Commands"; export class BestdoriS extends AudioSource { protected artist = ""; @@ -31,7 +31,9 @@ export class BestdoriS extends AudioSource { protected arranger: string; private id: number; - async init(url: string, prefetched: BestdoriJsonFormat, t: i18n["t"]){ + async init(url: string, prefetched: BestdoriJsonFormat){ + const { t } = getCommandExecutionContext(); + this.url = url; const id = BestdoriApi.instance.getAudioId(url); if(!id) throw new Error("Invalid streamable url"); @@ -68,11 +70,14 @@ export class BestdoriS extends AudioSource { }; } - toField(_: boolean, t: i18n["t"]){ + toField(_: boolean){ + const { t } = getCommandExecutionContext(); + const typeMap = { anime: "カバー", normal: "アニメ", }; + return [ { name: "バンド名", diff --git a/src/AudioSource/custom.ts b/src/AudioSource/custom.ts index 400161461..44e27821b 100644 --- a/src/AudioSource/custom.ts +++ b/src/AudioSource/custom.ts @@ -17,9 +17,9 @@ */ import type { AudioSourceBasicJsonFormat, StreamInfo } from "."; -import type { i18n } from "i18next"; import { AudioSource } from "./audiosource"; +import { getCommandExecutionContext } from "../Commands"; import { createFragmentalDownloadStream, downloadAsReadable, isAvailableRawAudioURL, requestHead, retrieveRemoteAudioInfo } from "../Util"; export class CustomStream extends AudioSource { @@ -27,7 +27,9 @@ export class CustomStream extends AudioSource { @@ -30,7 +30,9 @@ export class FsStream extends AudioSource { super({ isCacheable: false }); } - async init(url: string, _: AudioSourceBasicJsonFormat | null, t: i18n["t"]){ + async init(url: string, _: AudioSourceBasicJsonFormat | null){ + const { t } = getCommandExecutionContext(); + this.url = url; const info = await retrieveRemoteAudioInfo(url); this.title = info.displayTitle || t("audioSources.customStream"); @@ -46,7 +48,9 @@ export class FsStream extends AudioSource { }; } - toField(_: boolean, t: i18n["t"]){ + toField(_: boolean){ + const { t } = getCommandExecutionContext(); + return [ { name: `:asterisk:${t("moreInfo")}`, diff --git a/src/AudioSource/googledrive.ts b/src/AudioSource/googledrive.ts index 473f5b997..1f87acad8 100644 --- a/src/AudioSource/googledrive.ts +++ b/src/AudioSource/googledrive.ts @@ -17,12 +17,12 @@ */ import type { AudioSourceBasicJsonFormat, UrlStreamInfo } from "."; -import type { i18n } from "i18next"; import candyget from "candyget"; import * as htmlEntities from "html-entities"; import { AudioSource } from "./audiosource"; +import { getCommandExecutionContext } from "../Commands"; import { retrieveHttpStatusCode, retrieveRemoteAudioInfo } from "../Util"; export class GoogleDrive extends AudioSource { @@ -30,7 +30,9 @@ export class GoogleDrive extends AudioSource super({ isCacheable: false }); } - async init(url: string, prefetched: AudioSourceBasicJsonFormat | null, t: i18n["t"]){ + async init(url: string, prefetched: AudioSourceBasicJsonFormat | null){ + const { t } = getCommandExecutionContext(); + if(prefetched){ this.title = prefetched.title || t("audioSources.driveStream"); this.url = url; @@ -56,7 +58,9 @@ export class GoogleDrive extends AudioSource }; } - toField(_: boolean, t: i18n["t"]){ + toField(_: boolean){ + const { t } = getCommandExecutionContext(); + return [ { name: `:asterisk:${t("moreInfo")}`, diff --git a/src/AudioSource/niconico.ts b/src/AudioSource/niconico.ts index d4ee13e19..02937cee3 100644 --- a/src/AudioSource/niconico.ts +++ b/src/AudioSource/niconico.ts @@ -17,13 +17,13 @@ */ import type { AudioSourceBasicJsonFormat, ReadableStreamInfo } from "."; -import type { i18n } from "i18next"; import type { Readable } from "stream"; import { convert as htmlToText } from "html-to-text"; import NiconicoDL, { isValidURL } from "niconico-dl.js"; import { AudioSource } from "./audiosource"; +import { getCommandExecutionContext } from "../Commands"; import { createPassThrough } from "../Util"; export class NicoNicoS extends AudioSource { @@ -35,7 +35,9 @@ export class NicoNicoS extends AudioSource { super({ isSeekable: false }); } - async init(url: string, prefetched: NiconicoJsonFormat, t: i18n["t"]){ + async init(url: string, prefetched: NiconicoJsonFormat){ + const { t } = getCommandExecutionContext(); + this.url = url; this.nico = new NiconicoDL(url, /* quality */ "high"); if(prefetched){ @@ -75,7 +77,9 @@ export class NicoNicoS extends AudioSource { }; } - toField(verbose: boolean, t: i18n["t"]){ + toField(verbose: boolean){ + const { t } = getCommandExecutionContext(); + return [ { name: `:cinema:${t("audioSources.videoAuthor")}`, @@ -97,7 +101,9 @@ export class NicoNicoS extends AudioSource { ]; } - npAdditional(t: i18n["t"]){ + npAdditional(){ + const { t } = getCommandExecutionContext(); + return `${t("audioSources.videoAuthor")}: ` + this.author; } diff --git a/src/AudioSource/resolver.ts b/src/AudioSource/resolver.ts index fda365782..1ec4469e5 100644 --- a/src/AudioSource/resolver.ts +++ b/src/AudioSource/resolver.ts @@ -18,11 +18,10 @@ import type { KnownAudioSourceIdentifer } from "../Component/queueManager"; import type { SourceCache } from "../Component/sourceCache"; -import type { i18n } from "i18next"; import * as AudioSource from "."; import { isAvailableRawAudioURL } from "../Util"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { getLogger } from "../logger"; type AudioSourceBasicInfo = { @@ -32,10 +31,10 @@ type AudioSourceBasicInfo = { forceCache: boolean, }; -const { isDisabledSource } = useConfig(); +const { isDisabledSource } = getConfig(); const logger = getLogger("Resolver"); -export async function resolve(info: AudioSourceBasicInfo, cacheManager: SourceCache, preventSourceCache: boolean, t: i18n["t"]){ +export async function resolve(info: AudioSourceBasicInfo, cacheManager: SourceCache, preventSourceCache: boolean){ let basicInfo: AudioSource.AudioSource | null = null; const type = info.type; @@ -66,29 +65,29 @@ export async function resolve(info: AudioSourceBasicInfo, cacheManager: SourceCa basicInfo = await AudioSource.initYouTube(url, gotData as AudioSource.YouTubeJsonFormat, cache); }else if(!isDisabledSource("custom") && (type === "custom" || type === "unknown" && isAvailableRawAudioURL(url))){ // カスタムストリーム - basicInfo = await new AudioSource.CustomStream().init(url, info.knownData, t); + basicInfo = await new AudioSource.CustomStream().init(url, info.knownData); }else if(!isDisabledSource("soundcloud") && (type === "soundcloud" || AudioSource.SoundCloudS.validateUrl(url))){ // soundcloud - basicInfo = await new AudioSource.SoundCloudS().init(url, gotData as AudioSource.SoundcloudJsonFormat, t); + basicInfo = await new AudioSource.SoundCloudS().init(url, gotData as AudioSource.SoundcloudJsonFormat); }else if(!isDisabledSource("spotify") && (type === "spotify" || AudioSource.Spotify.validateTrackUrl(url)) && AudioSource.Spotify.available){ // spotify basicInfo = await new AudioSource.Spotify().init(url, gotData as AudioSource.SpotifyJsonFormat); }else if(type === "unknown"){ // google drive if(!isDisabledSource("googledrive") && AudioSource.GoogleDrive.validateUrl(url)){ - basicInfo = await new AudioSource.GoogleDrive().init(url, info.knownData, t); + basicInfo = await new AudioSource.GoogleDrive().init(url, info.knownData); }else if(!isDisabledSource("streamable") && AudioSource.StreamableApi.getVideoId(url)){ // Streamable basicInfo = await new AudioSource.Streamable().init(url, gotData as AudioSource.StreamableJsonFormat); }else if(process.env.BD_ENABLE && AudioSource.BestdoriApi.instance.getAudioId(url)){ // Bestdori - basicInfo = await new AudioSource.BestdoriS().init(url, gotData as AudioSource.BestdoriJsonFormat, t); + basicInfo = await new AudioSource.BestdoriS().init(url, gotData as AudioSource.BestdoriJsonFormat); }else if(!isDisabledSource("niconico") && AudioSource.NicoNicoS.validateUrl(url)){ // NicoNico - basicInfo = await new AudioSource.NicoNicoS().init(url, gotData as AudioSource.NiconicoJsonFormat, t); + basicInfo = await new AudioSource.NicoNicoS().init(url, gotData as AudioSource.NiconicoJsonFormat); }else if(!isDisabledSource("twitter") && AudioSource.Twitter.validateUrl(url)){ // Twitter - basicInfo = await new AudioSource.Twitter().init(url, gotData as AudioSource.TwitterJsonFormat, t); + basicInfo = await new AudioSource.Twitter().init(url, gotData as AudioSource.TwitterJsonFormat); } } diff --git a/src/AudioSource/soundcloud.ts b/src/AudioSource/soundcloud.ts index 1ddca7f93..9702055af 100644 --- a/src/AudioSource/soundcloud.ts +++ b/src/AudioSource/soundcloud.ts @@ -17,13 +17,13 @@ */ import type { AudioSourceBasicJsonFormat, ReadableStreamInfo } from "."; -import type { i18n } from "i18next"; import type { SoundcloudTrackV2 } from "soundcloud.ts"; import type { Readable } from "stream"; import SoundCloud from "soundcloud.ts"; import { AudioSource } from "./audiosource"; +import { getCommandExecutionContext } from "../Commands"; import { createPassThrough } from "../Util"; let soundCloudClient = new SoundCloud(); @@ -35,7 +35,9 @@ export class SoundCloudS extends AudioSource { super({ isSeekable: false }); } - async init(url: string, prefetched: SoundcloudJsonFormat | null, t: i18n["t"]){ + async init(url: string, prefetched: SoundcloudJsonFormat | null){ + const { t } = getCommandExecutionContext(); + this.url = url; if(prefetched){ this.title = prefetched.title; @@ -70,7 +72,9 @@ export class SoundCloudS extends AudioSource { }; } - toField(verbose: boolean, t: i18n["t"]){ + toField(verbose: boolean){ + const { t } = getCommandExecutionContext(); + return [ { name: `:musical_note:${t("user")}`, @@ -85,7 +89,9 @@ export class SoundCloudS extends AudioSource { ]; } - npAdditional(t: i18n["t"]){ + npAdditional(){ + const { t } = getCommandExecutionContext(); + return `${t("audioSources.artist")}: \`${this.author}\``; } diff --git a/src/AudioSource/streamable.ts b/src/AudioSource/streamable.ts index 78f474c58..e9eadfd60 100644 --- a/src/AudioSource/streamable.ts +++ b/src/AudioSource/streamable.ts @@ -17,11 +17,11 @@ */ import type { AudioSourceBasicJsonFormat, UrlStreamInfo } from "."; -import type { i18n } from "i18next"; import candyget from "candyget"; import { AudioSource } from "./audiosource"; +import { getCommandExecutionContext } from "../Commands"; export class Streamable extends AudioSource { protected streamUrl = ""; @@ -57,7 +57,9 @@ export class Streamable extends AudioSource { }; } - toField(_: boolean, t: i18n["t"]){ + toField(){ + const { t } = getCommandExecutionContext(); + return [ { name: ":link:URL", diff --git a/src/AudioSource/twitter.ts b/src/AudioSource/twitter.ts index 8acbf74de..fa6e914e8 100644 --- a/src/AudioSource/twitter.ts +++ b/src/AudioSource/twitter.ts @@ -17,18 +17,20 @@ */ import type { AudioSourceBasicJsonFormat, UrlStreamInfo } from "."; -import type { i18n } from "i18next"; import candyget from "candyget"; import * as htmlEntities from "html-entities"; import { AudioSource } from "./audiosource"; +import { getCommandExecutionContext } from "../Commands"; import { retrieveRemoteAudioInfo } from "../Util"; export class Twitter extends AudioSource { private streamUrl = ""; - async init(url: string, prefetched: TwitterJsonFormat | null, t: i18n["t"]){ + async init(url: string, prefetched: TwitterJsonFormat | null){ + const { t } = getCommandExecutionContext(); + this.url = url; if(!Twitter.validateUrl(url)) throw new Error("Invalid Twitter url."); if(prefetched){ @@ -59,7 +61,9 @@ export class Twitter extends AudioSource { }; } - toField(_: boolean, t: i18n["t"]){ + toField(){ + const { t } = getCommandExecutionContext(); + return [ { name: ":link:URL", diff --git a/src/AudioSource/youtube/index.ts b/src/AudioSource/youtube/index.ts index 86204ab9f..9fc39a173 100644 --- a/src/AudioSource/youtube/index.ts +++ b/src/AudioSource/youtube/index.ts @@ -18,7 +18,6 @@ import type { Cache } from "./strategies/base"; import type { StreamInfo } from ".."; -import type { i18n } from "i18next"; import type { EmbedField } from "oceanic.js"; import type { InfoData } from "play-dl"; @@ -27,6 +26,7 @@ import * as ytdl from "ytdl-core"; import { attemptGetInfoForStrategies, attemptFetchForStrategies } from "./strategies"; import { playDl } from "./strategies/play-dl"; import { ytdlCore } from "./strategies/ytdl-core"; +import { getCommandExecutionContext } from "../../Commands"; import { SecondaryUserAgent } from "../../definition"; import { timeLoggedMethod } from "../../logger"; import { AudioSource } from "../audiosource"; @@ -79,7 +79,7 @@ export class YouTube extends AudioSource { } @timeLoggedMethod - async init(url: string, prefetched: YouTubeJsonFormat | null, _: i18n["t"] | null = null, forceCache?: boolean){ + async init(url: string, prefetched: YouTubeJsonFormat | null, forceCache?: boolean){ this.url = url = YouTube.normalizeUrl(url); if(prefetched){ this.importData(prefetched); @@ -195,7 +195,8 @@ export class YouTube extends AudioSource { } } - toField(verbose: boolean, t: i18n["t"]){ + toField(verbose: boolean){ + const { t } = getCommandExecutionContext(); const fields = [] as EmbedField[]; fields.push({ name: `:cinema:${t("channelName")}`, @@ -211,7 +212,8 @@ export class YouTube extends AudioSource { return fields; } - npAdditional(t: i18n["t"]){ + npAdditional(){ + const { t } = getCommandExecutionContext(); return `${t("channelName")}:\`${this.channelName}\``; } @@ -265,7 +267,6 @@ export class YouTube extends AudioSource { resolve(); } - const waitTime = Math.max(new Date(startTime!).getTime() - Date.now(), 20 * 1000); this.logger.info(`Retrying after ${waitTime}ms`); @@ -273,7 +274,7 @@ export class YouTube extends AudioSource { if(signal.aborted) return; tick(); this.purgeCache(); - await this.init(this.url, null, null, false); + await this.init(this.url, null, false); checkForLive(); }, waitTime).unref(); }; diff --git a/src/AudioSource/youtube/playlist.ts b/src/AudioSource/youtube/playlist.ts index 25036c3a0..afb1aa3c7 100644 --- a/src/AudioSource/youtube/playlist.ts +++ b/src/AudioSource/youtube/playlist.ts @@ -19,10 +19,10 @@ import ytpl from "ytpl"; import { requireIfAny } from "../../Util"; -import { useConfig } from "../../config"; +import { getConfig } from "../../config"; const dYtpl = requireIfAny("ytpl") as typeof import("@distube/ytpl"); -const config = useConfig(); +const config = getConfig(); const playlistSearchOptions = { gl: config.country, hl: config.defaultLanguage, diff --git a/src/AudioSource/youtube/strategies/index.ts b/src/AudioSource/youtube/strategies/index.ts index 9b0c171f8..5b745cb25 100644 --- a/src/AudioSource/youtube/strategies/index.ts +++ b/src/AudioSource/youtube/strategies/index.ts @@ -23,7 +23,7 @@ import type { ytDlPStrategy } from "./yt-dlp"; import type { ytdlCoreStrategy } from "./ytdl-core"; import type { YtDlPatchedYoutubeDl } from "./ytdl-patched_youtube-dl"; -import { useConfig } from "../../../config"; +import { getConfig } from "../../../config"; import { getLogger } from "../../../logger"; type strategies = @@ -35,7 +35,7 @@ type strategies = ; const logger = getLogger("Strategies"); -const config = useConfig(); +const config = getConfig(); export const strategies: strategies[] = [ () => require("./ytdl-core"), diff --git a/src/AudioSource/youtube/strategies/ytdl-core.ts b/src/AudioSource/youtube/strategies/ytdl-core.ts index 3ceb13426..d4c7082c4 100644 --- a/src/AudioSource/youtube/strategies/ytdl-core.ts +++ b/src/AudioSource/youtube/strategies/ytdl-core.ts @@ -25,14 +25,14 @@ import * as ytdl from "ytdl-core"; import { Strategy } from "./base"; import { YouTubeJsonFormat } from ".."; -import { useConfig } from "../../../config"; +import { getConfig } from "../../../config"; import { SecondaryUserAgent } from "../../../definition"; import { createChunkedYTStream, createRefreshableYTLiveStream } from "../stream"; type ytdlCore = "ytdlCore"; export const ytdlCore: ytdlCore = "ytdlCore"; -const config = useConfig(); +const config = getConfig(); type ytdlCoreCache = Cache; diff --git a/src/AudioSource/youtube/worker.ts b/src/AudioSource/youtube/worker.ts index b32d42c12..f97123db4 100644 --- a/src/AudioSource/youtube/worker.ts +++ b/src/AudioSource/youtube/worker.ts @@ -24,7 +24,7 @@ import ytsr from "ytsr"; import { YouTube } from "."; import { requireIfAny, stringifyObject } from "../../Util"; -import { useConfig } from "../../config"; +import { getConfig } from "../../config"; if(!parentPort){ throw new Error("This file should be run in worker thread."); @@ -32,7 +32,7 @@ if(!parentPort){ const dYtsr = requireIfAny("@distube/ytsr") as typeof import("@distube/ytsr"); -const config = useConfig(); +const config = getConfig(); const searchOptions = { limit: 12, gl: config.country, @@ -52,7 +52,7 @@ function onMessage(message: WithId){ if(message.type === "init"){ const { id, url, prefetched, forceCache } = message; const youtube = new YouTube(); - youtube.init(url, prefetched, null, forceCache) + youtube.init(url, prefetched, forceCache) .then(() => { const data = Object.assign({}, youtube); // @ts-expect-error diff --git a/src/Commands/bdmbd.ts b/src/Commands/bdmbd.ts index 80e195be2..ce4b45788 100644 --- a/src/Commands/bdmbd.ts +++ b/src/Commands/bdmbd.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,12 +32,12 @@ export default class BgmBd extends BaseCommand { }); } - protected async run(message: CommandMessage, context: Readonly, t: i18n["t"]): Promise { + protected async run(message: CommandMessage, context: Readonly): Promise { context.server.updateBoundChannel(message); - await context.server.joinVoiceChannel(message, { replyOnFail: true }, t); + await context.server.joinVoiceChannel(message, { replyOnFail: true }); const bgmPlaylistUrl = context.rawArgs.length === 0 ? "aHR0cHM6Ly93d3cueW91dHViZS5jb20vcGxheWxpc3Q/bGlzdD1QTExmZmhjQXBzbzl4UFhLUG5YbEZ3czlxWUNkMDltTFA0" : "aHR0cHM6Ly93d3cueW91dHViZS5jb20vcGxheWxpc3Q/bGlzdD1QTExmZmhjQXBzbzl4WnpYZ0RFdEdsQk5wNUtYZjNPY1Zx"; - await context.server.playFromURL(message, Buffer.from(bgmPlaylistUrl, "base64").toString(), {}, t); + await context.server.playFromURL(message, Buffer.from(bgmPlaylistUrl, "base64").toString(), {}); } } diff --git a/src/Commands/bgm.ts b/src/Commands/bgm.ts index 9d1cd2e57..dfa7f42c7 100644 --- a/src/Commands/bgm.ts +++ b/src/Commands/bgm.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,12 +32,14 @@ export default class Bgm extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + // update bound channel context.server.updateBoundChannel(message); // attempt to join - if(!await context.server.joinVoiceChannel(message, { replyOnFail: true }, t)) return; + if(!await context.server.joinVoiceChannel(message, { replyOnFail: true })) return; // check existing search panel if(context.server.searchPanel.has(message.member.id)){ @@ -49,7 +50,6 @@ export default class Bgm extends BaseCommand { const searchPanel = context.server.searchPanel.create( message, t("commands:bgm.listOfPresetBGM"), - t, true ); if(!searchPanel){ @@ -69,7 +69,6 @@ export default class Bgm extends BaseCommand { thumbnail: item.thumbnails[0].url, url: item.url, })), - t ); } } diff --git a/src/Commands/bulk_delete.ts b/src/Commands/bulk_delete.ts index 4d4ff2161..2d35a4408 100644 --- a/src/Commands/bulk_delete.ts +++ b/src/Commands/bulk_delete.ts @@ -19,7 +19,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; import type { ResponseMessage } from "../Component/commandResolver/ResponseMessage"; -import type { i18n } from "i18next"; import type { AnyTextableGuildChannel, Message } from "oceanic.js"; import { MessageActionRowBuilder, MessageButtonBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -32,7 +31,7 @@ export default class BulkDelete extends BaseCommand { alias: ["bulk_delete", "bulk-delete", "bulkdelete"], unlist: false, category: "utility", - argument: [ + args: [ { type: "integer" as const, name: "count", @@ -45,7 +44,9 @@ export default class BulkDelete extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + const count = Number(context.args[0]); if(isNaN(count)){ message.reply(`:warning:${t("commands:bulk_delete.invalidMessageCount")}`).catch(this.logger.error); diff --git a/src/Commands/cancel.ts b/src/Commands/cancel.ts index 230f2af83..a228f3485 100644 --- a/src/Commands/cancel.ts +++ b/src/Commands/cancel.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class Cancel extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); const result = context.server.cancelAll(); if(result){ diff --git a/src/Commands/command.ts b/src/Commands/command.ts index ff93d83fa..07acf1167 100644 --- a/src/Commands/command.ts +++ b/src/Commands/command.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import type { EmbedField } from "oceanic.js"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -26,11 +25,11 @@ import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/hel import { BaseCommand } from "."; import { CommandManager } from "../Component/commandManager"; import { getColor } from "../Util/color"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; -const config = useConfig(); +const config = getConfig(); -export const categoriesList = ["voice", "player", "playlist", "utility", "bot"] as const; +export const categoriesList = ["voice", "player", "playlist", "utility", "bot", "settings"] as const; export default class Commands extends BaseCommand { constructor(){ @@ -38,7 +37,7 @@ export default class Commands extends BaseCommand { unlist: false, alias: ["command", "commands", "cmd"], category: "bot", - argument: [ + args: [ { type: "string", name: "command", @@ -53,7 +52,9 @@ export default class Commands extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + if(context.rawArgs === ""){ // 引数がない場合は全コマンドの一覧を表示 const embed = [] as MessageEmbedBuilder[]; diff --git a/src/Commands/disconnect.ts b/src/Commands/disconnect.ts index 5e68b7e1b..fc477d9bf 100644 --- a/src/Commands/disconnect.ts +++ b/src/Commands/disconnect.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,9 @@ export default class Dc extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + context.server.updateBoundChannel(message); // そもそも再生状態じゃないよ... if(!context.server.player.isConnecting){ diff --git a/src/Commands/effect.ts b/src/Commands/effect.ts index b0df23053..06b266cee 100644 --- a/src/Commands/effect.ts +++ b/src/Commands/effect.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import type { MessageActionRow } from "oceanic.js"; import { MessageActionRowBuilder, MessageButtonBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -37,7 +36,9 @@ export default class Effect extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + context.server.updateBoundChannel(message); try{ const { collector, customIdMap } = context.server.bot.collectors.create() @@ -74,12 +75,12 @@ export default class Effect extends BaseCommand { }; const reply = await message.reply({ - embeds: [context.server.audioEffects.createEmbed(message.member.avatarURL(), t)], + embeds: [context.server.audioEffects.createEmbed(message.member.avatarURL())], components: createActionRow(), }); const updateEffectEmbed = (emptyrow = false) => { reply.edit({ - embeds: [context.server.audioEffects.createEmbed(message.member.avatarURL(), t)], + embeds: [context.server.audioEffects.createEmbed(message.member.avatarURL())], components: emptyrow ? [] : createActionRow(), }).catch(this.logger.error); }; diff --git a/src/Commands/end.ts b/src/Commands/end.ts index afb095918..232548bf3 100644 --- a/src/Commands/end.ts +++ b/src/Commands/end.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,9 @@ export default class End extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + context.server.updateBoundChannel(message); if(!context.server.player.isPlaying){ message.reply(t("errorOccurred")).catch(this.logger.error); diff --git a/src/Commands/equalplayback.ts b/src/Commands/equalplayback.ts index 0b41655dd..7cf155dc9 100644 --- a/src/Commands/equalplayback.ts +++ b/src/Commands/equalplayback.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -36,20 +35,22 @@ export default class EquallyPlayback extends BaseCommand { }); } - async run(context: CommandMessage, options: CommandArgs, t: i18n["t"]){ - options.server.updateBoundChannel(context); - if(options.server.equallyPlayback){ - options.server.equallyPlayback = false; - context.reply(`❌${t("commands:equalplayback.disabled")}`).catch(this.logger.error); + async run(command: CommandMessage, context: CommandArgs){ + const { t } = context; + + context.server.updateBoundChannel(command); + if(context.server.preferences.equallyPlayback){ + context.server.preferences.equallyPlayback = false; + command.reply(`❌${t("commands:equalplayback.disabled")}`).catch(this.logger.error); }else{ - options.server.equallyPlayback = true; + context.server.preferences.equallyPlayback = true; const embed = new MessageEmbedBuilder() .setTitle(`⭕${t("commands:equalplayback.enabled")}`) .setDescription(t("commands:equalplayback.featureDescription")) .setColor(getColor("EQUALLY")) .toOceanic() ; - context.reply({ embeds: [embed] }).catch(this.logger.error); + command.reply({ embeds: [embed] }).catch(this.logger.error); } } } diff --git a/src/Commands/export.ts b/src/Commands/export.ts index c052c7595..6d64e4dbd 100644 --- a/src/Commands/export.ts +++ b/src/Commands/export.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; import { YmxVersion } from "../Structure"; @@ -34,7 +33,9 @@ export default class Export extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + context.server.updateBoundChannel(message); if(context.server.queue.publicLength === 0){ message.reply(t("commands:export.queueEmpty")).catch(this.logger.error); diff --git a/src/Commands/frame.ts b/src/Commands/frame.ts index eb1786485..2555922ec 100644 --- a/src/Commands/frame.ts +++ b/src/Commands/frame.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { FFmpeg } from "prism-media"; import * as ytdl from "ytdl-core"; @@ -34,7 +33,7 @@ export default class Frame extends BaseCommand { alias: ["frame", "キャプチャ", "capture"], unlist: false, category: "player", - argument: [{ + args: [{ type: "string", name: "time", required: false, @@ -46,7 +45,9 @@ export default class Frame extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + context.server.updateBoundChannel(message); const server = context.server; diff --git a/src/Commands/help.ts b/src/Commands/help.ts index 4485532b6..3987a905f 100644 --- a/src/Commands/help.ts +++ b/src/Commands/help.ts @@ -18,16 +18,15 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; import { BaseCommand } from "."; import { Spotify } from "../AudioSource"; import { getColor } from "../Util/color"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; -const config = useConfig(); +const config = getConfig(); export default class Help extends BaseCommand { constructor(){ @@ -40,7 +39,9 @@ export default class Help extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + const developerId = "593758391395155978"; const cachedUser = context.client.users.get(developerId); const developer: string | null = cachedUser diff --git a/src/Commands/import.ts b/src/Commands/import.ts index 404e2d988..c0f93a3bb 100644 --- a/src/Commands/import.ts +++ b/src/Commands/import.ts @@ -19,7 +19,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; import type { YmxFormat } from "../Structure"; -import type { i18n } from "i18next"; import type { AnyTextableGuildChannel, Message } from "oceanic.js"; import candyget from "candyget"; @@ -28,9 +27,9 @@ import { ApplicationCommandTypes } from "oceanic.js"; import { BaseCommand } from "."; import { TaskCancellationManager } from "../Component/taskCancellationManager"; import { YmxVersion } from "../Structure"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; -const config = useConfig(); +const config = getConfig(); export default class Import extends BaseCommand { constructor(){ @@ -38,7 +37,7 @@ export default class Import extends BaseCommand { alias: ["import"], unlist: false, category: "playlist", - argument: [{ + args: [{ type: "string", name: "url", required: true, @@ -51,7 +50,9 @@ export default class Import extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + context.server.updateBoundChannel(message); const statusMessage = await message.reply(`🔍${t("commands:import.loadingMessage")}...`); diff --git a/src/Commands/index.ts b/src/Commands/index.ts index 6c22b8393..e089c3c46 100644 --- a/src/Commands/index.ts +++ b/src/Commands/index.ts @@ -18,10 +18,12 @@ import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; import type { GuildDataContainer } from "../Structure"; -import type { ListCommandInitializeOptions, UnlistCommandOptions, ListCommandWithArgsOptions, CommandArgs, CommandPermission, LocalizedSlashCommandArgument } from "../Structure/Command"; import type { LoggerObject } from "../logger"; +import type { ListCommandInitializeOptions, UnlistCommandOptions, ListCommandWithArgsOptions, CommandArgs, CommandPermission, LocalizedSlashCommandArgument } from "../types/Command"; import type { AnyTextableGuildChannel, ApplicationCommandOptionsBoolean, ApplicationCommandOptionsChoice, ApplicationCommandOptionsInteger, ApplicationCommandOptionsString, CreateApplicationCommandOptions, LocaleMap, ModalSubmitInteraction, PermissionName } from "oceanic.js"; +import { AsyncLocalStorage } from "async_hooks"; + import i18next from "i18next"; import { InteractionTypes, Permissions, TypedEmitter, ApplicationCommandTypes } from "oceanic.js"; @@ -30,18 +32,25 @@ import { discordUtil } from "../Util"; import { availableLanguages } from "../i18n"; import { getLogger } from "../logger"; -export { CommandArgs } from "../Structure/Command"; +export { CommandArgs } from "../types/Command"; interface CommandEvents { run: [Readonly]; } +export const commandExecutionContext = new AsyncLocalStorage(); +export function getCommandExecutionContext(): CommandArgs | Pick { + return commandExecutionContext.getStore() ?? { + t: i18next.t, + }; +} + /** * すべてのコマンドハンドラーの基底クラスです */ export abstract class BaseCommand extends TypedEmitter { /** ボットを実行します */ - protected abstract run(message: CommandMessage, context: Readonly, t: (typeof i18next)["t"]): Promise; + protected abstract run(message: CommandMessage, context: Readonly): Promise; // eslint-disable-next-line unused-imports/no-unused-vars handleAutoComplete(argname: string, input: string | number, otherOptions: { name: string, value: string | number }[]): string[] { @@ -129,12 +138,12 @@ export abstract class BaseCommand extends TypedEmitter { /** スラッシュコマンドの名称として登録できる旧基準を満たしたコマンド名を取得します */ get asciiName(){ - return this.alias.filter(c => c.match(/^[\w-]{2,32}$/))[0]; + return this.alias.filter(c => c.match(/^[>\w-]{2,32}$/))[0]; } protected readonly logger: LoggerObject; - constructor(opts: ListCommandInitializeOptions|UnlistCommandOptions){ + constructor(opts: ListCommandInitializeOptions | UnlistCommandOptions){ super(); this._messageCommand = "messageCommand" in opts && opts.messageCommand || false; this._interactionOnly = "interactionOnly" in opts && opts.interactionOnly || false; @@ -156,7 +165,7 @@ export abstract class BaseCommand extends TypedEmitter { examples, usage, category, - argument, + args, requiredPermissionsOr, defaultMemberPermission, } = opts as ListCommandWithArgsOptions; @@ -188,7 +197,7 @@ export abstract class BaseCommand extends TypedEmitter { this._category = category; - this._argument = argument ? argument.map(arg => { + this._argument = args ? args.map(arg => { const result: LocalizedSlashCommandArgument = { type: arg.type, name: arg.name, @@ -308,12 +317,16 @@ export abstract class BaseCommand extends TypedEmitter { } this.emit("run", context); - await this.run(message, context, i18next.getFixedT(context.locale)); + + await commandExecutionContext.run(context, () => this.run(message, context)); } /** アプリケーションコマンドとして登録できるオブジェクトを生成します */ toApplicationCommandStructure(): CreateApplicationCommandOptions[] { - if(this.unlist) throw new Error("This command cannot be listed due to private command!"); + if(this.unlist){ + throw new Error("This command cannot be listed due to private command!"); + } + const result: CreateApplicationCommandOptions[] = []; const defaultMemberPermissions = this.defaultMemberPermission === "NONE" ? null diff --git a/src/Commands/invoke.ts b/src/Commands/invoke.ts index 10e890ac0..10676dcc2 100644 --- a/src/Commands/invoke.ts +++ b/src/Commands/invoke.ts @@ -17,12 +17,11 @@ */ import type { CommandArgs } from "."; -import type { i18n } from "i18next"; import { BaseCommand } from "."; import { CommandManager } from "../Component/commandManager"; import { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { getLogs } from "../logger"; export default class Invoke extends BaseCommand { @@ -31,7 +30,7 @@ export default class Invoke extends BaseCommand { alias: ["invoke"], unlist: false, category: "utility", - argument: [{ + args: [{ name: "command", type: "string", required: true, @@ -43,10 +42,12 @@ export default class Invoke extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + // handle special commands - if(context.rawArgs.startsWith("sp;") && useConfig().isBotAdmin(message.member.id)){ - this.evaluateSpecialCommands(context.rawArgs.substring(3), message, context, t) + if(context.rawArgs.startsWith("sp;") && getConfig().isBotAdmin(message.member.id)){ + this.evaluateSpecialCommands(context.rawArgs.substring(3), message, context) .then(result => message.reply(result)) .catch(this.logger.error) ; @@ -74,7 +75,7 @@ export default class Invoke extends BaseCommand { } } - private async evaluateSpecialCommands(specialCommand: string, message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + private async evaluateSpecialCommands(specialCommand: string, message: CommandMessage, context: CommandArgs){ switch(specialCommand){ case "cleanupsc": await CommandManager.instance.sync(context.client, true); @@ -102,8 +103,8 @@ export default class Invoke extends BaseCommand { }).catch(this.logger.error); break; default: - return t("commands:invoke.specialCommandNotFound"); + return context.t("commands:invoke.specialCommandNotFound"); } - return t("commands:invoke.executed"); + return context.t("commands:invoke.executed"); } } diff --git a/src/Commands/join.ts b/src/Commands/join.ts index dce16f357..35f2ee299 100644 --- a/src/Commands/join.ts +++ b/src/Commands/join.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,12 +32,14 @@ export default class Join extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); + if(message.member.voiceState?.channel?.voiceMembers.has(context.client.user.id) && context.server.connection){ message.reply(`✘${t("commands:join.alreadyConnected")}`).catch(this.logger.error); }else{ - await context.server.joinVoiceChannel(message, { reply: true }, t); + await context.server.joinVoiceChannel(message, { reply: true }); } } } diff --git a/src/Commands/leaveclean.ts b/src/Commands/leaveclean.ts index 8a6f6d216..86e6a36df 100644 --- a/src/Commands/leaveclean.ts +++ b/src/Commands/leaveclean.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class LeaveClean extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); if(!context.server.player.isConnecting){ diff --git a/src/Commands/log.ts b/src/Commands/log.ts index d945d9b06..ed07f937e 100644 --- a/src/Commands/log.ts +++ b/src/Commands/log.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import type { EmbedOptions } from "oceanic.js"; import * as os from "os"; @@ -30,10 +29,10 @@ import { BaseCommand } from "."; import * as Util from "../Util"; import { getColor } from "../Util/color"; import { getMBytes } from "../Util/system"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { getLogs } from "../logger"; -const config = useConfig(); +const config = getConfig(); export default class SystemInfo extends BaseCommand { constructor(){ @@ -41,7 +40,7 @@ export default class SystemInfo extends BaseCommand { alias: ["ログ", "log", "systeminfo", "sysinfo"], unlist: false, category: "utility", - argument: [{ + args: [{ type: "string", name: "content", required: false, @@ -59,7 +58,8 @@ export default class SystemInfo extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); // Run default logger context.bot.logGeneralInfo(); diff --git a/src/Commands/loop.ts b/src/Commands/loop.ts index 2c7a9d45a..a74b2caa7 100644 --- a/src/Commands/loop.ts +++ b/src/Commands/loop.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class Loop extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); if(context.server.queue.loopEnabled){ context.server.queue.loopEnabled = false; diff --git a/src/Commands/looponce.ts b/src/Commands/looponce.ts index 925f01058..885914e2e 100644 --- a/src/Commands/looponce.ts +++ b/src/Commands/looponce.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class OnceLoop extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); if(context.server.queue.onceLoopEnabled){ context.server.queue.onceLoopEnabled = false; diff --git a/src/Commands/loopqueue.ts b/src/Commands/loopqueue.ts index f7ea75b30..811a38cea 100644 --- a/src/Commands/loopqueue.ts +++ b/src/Commands/loopqueue.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class QueueLoop extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); if(context.server.queue.queueLoopEnabled){ context.server.queue.queueLoopEnabled = false; diff --git a/src/Commands/lyrics.ts b/src/Commands/lyrics.ts index da9e31149..4b79d558c 100644 --- a/src/Commands/lyrics.ts +++ b/src/Commands/lyrics.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; import candyget from "candyget"; @@ -36,7 +35,7 @@ export default class Lyrics extends BaseCommand { alias: ["lyrics", "l", "lyric"], unlist: false, category: "utility", - argument: [ + args: [ { type: "string", name: "keyword", @@ -50,11 +49,12 @@ export default class Lyrics extends BaseCommand { }); } - async run(message: CommandMessage, options: CommandArgs, t: i18n["t"]){ - options.server.updateBoundChannel(message); + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; + context.server.updateBoundChannel(message); const msg = await message.reply("🔍検索中..."); try{ - const songInfo = await getLyrics(options.rawArgs); + const songInfo = await getLyrics(context.rawArgs); const embeds = [] as MessageEmbedBuilder[]; if(!songInfo.lyric) throw new Error("取得した歌詞が空でした"); const chunkLength = Math.ceil(songInfo.lyric.length / 4000); diff --git a/src/Commands/move.ts b/src/Commands/move.ts index 5c3a0f4c7..41ba0d375 100644 --- a/src/Commands/move.ts +++ b/src/Commands/move.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -28,7 +27,7 @@ export default class Mv extends BaseCommand { alias: ["move", "mv"], unlist: false, category: "playlist", - argument: [ + args: [ { type: "integer", name: "from", @@ -47,7 +46,8 @@ export default class Mv extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); if(context.args.length !== 2){ diff --git a/src/Commands/movelastsongtofirst.ts b/src/Commands/movelastsongtofirst.ts index b46f380e4..03913c9cd 100644 --- a/src/Commands/movelastsongtofirst.ts +++ b/src/Commands/movelastsongtofirst.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class Mltf extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); if(context.server.queue.length <= 2){ message.reply(t("commands:movelastsongtofirst.usableWhen3orMoreQueue")).catch(this.logger.error); diff --git a/src/Commands/news.ts b/src/Commands/news.ts index 383befe9c..089e7f7d7 100644 --- a/src/Commands/news.ts +++ b/src/Commands/news.ts @@ -18,16 +18,15 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { MessageActionRowBuilder, MessageButtonBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; import { BaseCommand } from "."; import { Playlist } from "../AudioSource/youtube/playlist"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { DefaultAudioThumbnailURL } from "../definition"; -const config = useConfig(); +const config = getConfig(); export default class News extends BaseCommand { constructor(){ @@ -40,9 +39,10 @@ export default class News extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); - context.server.joinVoiceChannel(message, {}, t).catch(this.logger.error); + context.server.joinVoiceChannel(message, {}).catch(this.logger.error); // change news according to locale let url: string = null!; switch(context.locale){ @@ -122,7 +122,7 @@ export default class News extends BaseCommand { } return; } - const searchPanel = context.server.searchPanel.create(message, t("commands:news.newsTopics"), t, true); + const searchPanel = context.server.searchPanel.create(message, t("commands:news.newsTopics"), true); if(!searchPanel) return; await searchPanel.consumeSearchResult( Playlist(url, { @@ -136,7 +136,6 @@ export default class News extends BaseCommand { duration: item.durationText, description: `${t("length")}: ${item.duration}, ${t("channelName")}: ${item.author}`, })), - t ); } } diff --git a/src/Commands/nowplaying.ts b/src/Commands/nowplaying.ts index 4b8342321..6e3f1651c 100644 --- a/src/Commands/nowplaying.ts +++ b/src/Commands/nowplaying.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -32,7 +31,7 @@ export default class NowPlaying extends BaseCommand { alias: ["今の曲", "nowplaying", "np"], unlist: false, category: "player", - argument: [{ + args: [{ type: "bool" as const, name: "detailed", required: false, @@ -42,7 +41,8 @@ export default class NowPlaying extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); // そもそも再生状態じゃないよ... if(!context.server.player.isPlaying){ @@ -82,12 +82,7 @@ export default class NowPlaying extends BaseCommand { : ` \`${min}:${sec}/${totalDurationSeconds === 0 ? `(${t("unknown")})` : `${tmin}:${tsec}\``}` }` ) - .setFields( - ...info.toField( - ["long", "l", "verbose", "l", "true"].some(arg => context.args[0] === arg), - t - ) - ) + .setFields(...info.toField(["long", "l", "verbose", "l", "true"].some(arg => context.args[0] === arg))) .addField(":link:URL", info.url); if(typeof info.thumbnail === "string"){ diff --git a/src/Commands/pause.ts b/src/Commands/pause.ts index 391811d7c..cee70692e 100644 --- a/src/Commands/pause.ts +++ b/src/Commands/pause.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class Pause extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); // そもそも再生状態じゃないよ... if(!context.server.player.isPlaying){ diff --git a/src/Commands/ping.ts b/src/Commands/ping.ts index 441a61018..8dfea341a 100644 --- a/src/Commands/ping.ts +++ b/src/Commands/ping.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { getVoiceConnection } from "@discordjs/voice"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -38,7 +37,8 @@ export default class Uptime extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; const now = Date.now(); const embed = new MessageEmbedBuilder() .setColor(getColor("UPTIME")) diff --git a/src/Commands/play.ts b/src/Commands/play.ts index 6ba1f4f66..8f83b8825 100644 --- a/src/Commands/play.ts +++ b/src/Commands/play.ts @@ -19,7 +19,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; import type * as dYtsr from "@distube/ytsr"; -import type { i18n } from "i18next"; import type * as ytsr from "ytsr"; import { ApplicationCommandTypes } from "oceanic.js"; @@ -33,7 +32,7 @@ export default class Play extends BaseCommand { alias: ["play", "p", "resume", "re"], unlist: false, category: "player", - argument: [ + args: [ { type: "string" as const, name: "keyword", @@ -51,7 +50,8 @@ export default class Play extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); const server = context.server; const firstAttachment = Array.isArray(message.attachments) ? message.attachments[0] : message.attachments.first(); @@ -71,7 +71,7 @@ export default class Play extends BaseCommand { const wasConnected = server.player.isConnecting; //VCに入れない - if(!await context.server.joinVoiceChannel(message, { replyOnFail: true }, t)){ + if(!await context.server.joinVoiceChannel(message, { replyOnFail: true })){ return; } @@ -92,7 +92,7 @@ export default class Play extends BaseCommand { // 引数ついてたらそれ優先して再生する if(context.rawArgs.startsWith("http://") || context.rawArgs.startsWith("https://")){ // ついていた引数がURLなら - await context.server.playFromURL(message, context.args as string[], { first: !wasConnected }, t); + await context.server.playFromURL(message, context.args as string[], { first: !wasConnected }); }else{ // URLでないならキーワードとして検索 const msg = await message.channel.createMessage({ @@ -118,7 +118,7 @@ export default class Play extends BaseCommand { return; } await Promise.allSettled([ - context.server.playFromURL(message, videos[0].url, { first: !wasConnected, cancellable: context.server.queue.length >= 1 }, t), + context.server.playFromURL(message, videos[0].url, { first: !wasConnected, cancellable: context.server.queue.length >= 1 }), msg.delete(), ]); } @@ -134,21 +134,20 @@ export default class Play extends BaseCommand { message, firstAttachment.url, { first: !wasConnected }, - t ); }else if(message["_message"]?.referencedMessage){ // 返信先のメッセージを確認 const messageReference = message["_message"].referencedMessage; if(messageReference.inCachedGuildChannel()){ context.server - .playFromMessage(message, messageReference, context, { first: !wasConnected }, t) + .playFromMessage(message, messageReference, context, { first: !wasConnected }) .catch(this.logger.error); } }else if(message["_interaction"] && "type" in message["_interaction"].data && message["_interaction"].data.type === ApplicationCommandTypes.MESSAGE){ const messageReference = message["_interaction"].data.resolved.messages.first(); if(messageReference?.inCachedGuildChannel()){ context.server - .playFromMessage(message, messageReference, context, { first: !wasConnected }, t) + .playFromMessage(message, messageReference, context, { first: !wasConnected }) .catch(this.logger.error); } }else if(server.queue.length >= 1){ diff --git a/src/Commands/play_private.ts b/src/Commands/play_private.ts index 265ec30f2..9082c317f 100644 --- a/src/Commands/play_private.ts +++ b/src/Commands/play_private.ts @@ -18,10 +18,8 @@ import type { CommandArgs } from "."; import type { GuildDataContainer } from "../Structure"; -import type { i18n } from "i18next"; import type { AnyTextableGuildChannel, ModalSubmitInteraction } from "oceanic.js"; -import i18next from "i18next"; import { ComponentTypes, InteractionTypes, TextInputStyles } from "oceanic.js"; import { BaseCommand } from "."; @@ -40,7 +38,9 @@ export default class PlayPrivate extends BaseCommand { }); } - protected override async run(message: CommandMessage, context: Readonly, t: i18n["t"]){ + protected override async run(message: CommandMessage, context: Readonly){ + const { t } = context; + if(message["isMessage"] || !message["_interaction"]){ await message.reply(`:x: ${t("commands:play_private.noInteraction")}`).catch(this.logger.error); return; @@ -74,17 +74,14 @@ export default class PlayPrivate extends BaseCommand { if(value){ const message = CommandMessage.createFromInteraction(interaction, "play_private", [value], value); - // ここの型注釈を外すとTSサーバーがとんでもなく重くなる - const fixedT: i18n["t"] = i18next.getFixedT(interaction.locale); const items = await server.playFromURL( message, value, { privateSource: true, first: false }, - fixedT, ); - if(items.length <= 0 || !await server.joinVoiceChannel(message, {}, fixedT)){ + if(items.length <= 0 || !await server.joinVoiceChannel(message, {})){ return; } diff --git a/src/Commands/queue.ts b/src/Commands/queue.ts index 876ea41eb..5818b4a75 100644 --- a/src/Commands/queue.ts +++ b/src/Commands/queue.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -32,7 +31,7 @@ export default class Queue extends BaseCommand { alias: ["キューを表示", "再生待ち", "queue", "q"], unlist: false, category: "playlist", - argument: [{ + args: [{ type: "integer" as const, name: "page", required: false, @@ -42,7 +41,8 @@ export default class Queue extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); const queue = context.server.queue; if(queue.length === 0){ @@ -84,7 +84,7 @@ export default class Queue extends BaseCommand { : `${min}:${sec}` } \``, `${t("components:nowplaying.requestedBy")}: \`${q.additionalInfo.addedBy.displayName}\` `, - q.basicInfo.npAdditional(t), + q.basicInfo.npAdditional(), ].join("\r\n"), }); } @@ -103,8 +103,8 @@ export default class Queue extends BaseCommand { `${t("commands:queue.total")}: ${thour}:${tmin}:${tsec}`, `${t("components:queue.trackloop")}:${queue.loopEnabled ? "⭕" : "❌"}`, `${t("components:queue.queueloop")}:${queue.queueLoopEnabled ? "⭕" : "❌"}`, - `${t("components:queue.autoplayRelated")}:${context.server.addRelated ? "⭕" : "❌"}`, - `${t("components:queue.equallyplayback")}:${context.server.equallyPlayback ? "⭕" : "❌"}`, + `${t("components:queue.autoplayRelated")}:${context.server.preferences.addRelated ? "⭕" : "❌"}`, + `${t("components:queue.equallyplayback")}:${context.server.preferences.equallyPlayback ? "⭕" : "❌"}`, ].join(" | "), }) .setThumbnail(message.guild.iconURL()!) diff --git a/src/Commands/radio.ts b/src/Commands/radio.ts index b43c11a39..28049ea0d 100644 --- a/src/Commands/radio.ts +++ b/src/Commands/radio.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import ytdl from "ytdl-core"; @@ -32,7 +31,7 @@ export default class Radio extends BaseCommand { category: "playlist", requiredPermissionsOr: ["admin", "sameVc", "noConnection"], shouldDefer: true, - argument: [ + args: [ { type: "string" as const, name: "url", @@ -42,7 +41,8 @@ export default class Radio extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); try{ @@ -57,7 +57,7 @@ export default class Radio extends BaseCommand { // if url specified, enable the feature if(context.rawArgs !== "" || (!context.server.queue.mixPlaylistEnabled && context.server.player.isPlaying)){ // first, attempt to join to the vc - const joinResult = await context.server.joinVoiceChannel(message, { reply: false, replyOnFail: true }, t); + const joinResult = await context.server.joinVoiceChannel(message, { reply: false, replyOnFail: true }); if(!joinResult){ return; } diff --git a/src/Commands/related.ts b/src/Commands/related.ts index 981a3bb82..92f3377b8 100644 --- a/src/Commands/related.ts +++ b/src/Commands/related.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -36,13 +35,14 @@ export default class Related extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); - if(context.server.addRelated){ - context.server.addRelated = false; + if(context.server.preferences.addRelated){ + context.server.preferences.addRelated = false; message.reply(`❌${t("commands:related.disabled")}`).catch(this.logger.error); }else{ - context.server.addRelated = true; + context.server.preferences.addRelated = true; const embed = new MessageEmbedBuilder() .setTitle(`⭕${t("commands:related.enabled")}`) .setDescription(`${t("commands:related.featureDescription")}\r\n${t("commands:related.featureNote")}`) diff --git a/src/Commands/remove.ts b/src/Commands/remove.ts index c91f94d08..ad1bf2551 100644 --- a/src/Commands/remove.ts +++ b/src/Commands/remove.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; import { discordUtil } from "../Util"; @@ -29,7 +28,7 @@ export default class Rm extends BaseCommand { alias: ["消去", "remove", "rm", "del", "delete"], unlist: false, category: "playlist", - argument: [{ + args: [{ type: "string", name: "index", required: true, @@ -41,7 +40,8 @@ export default class Rm extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; if(context.args.length === 0){ message.reply(t("commands:remove.noArgument")).catch(this.logger.error); return; diff --git a/src/Commands/removeall.ts b/src/Commands/removeall.ts index 908aa7b09..488c716f1 100644 --- a/src/Commands/removeall.ts +++ b/src/Commands/removeall.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class Rmall extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); await context.server.player.disconnect().catch(this.logger.error); context.server.queue.removeAll(); diff --git a/src/Commands/removedupes.ts b/src/Commands/removedupes.ts index 3c0d26e04..de22c2ea2 100644 --- a/src/Commands/removedupes.ts +++ b/src/Commands/removedupes.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class RmDuplicated extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); // 削除するアイテムのリストを作成 diff --git a/src/Commands/reset.ts b/src/Commands/reset.ts index 4ef9a7d49..fbb93250c 100644 --- a/src/Commands/reset.ts +++ b/src/Commands/reset.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { Routes } from "oceanic.js"; @@ -33,7 +32,7 @@ export default class Reset extends BaseCommand { category: "utility", requiredPermissionsOr: ["manageGuild"], shouldDefer: false, - argument: [ + args: [ { type: "bool" as const, name: "preservequeue", @@ -43,7 +42,8 @@ export default class Reset extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; // VC接続中なら切断 await context.server.player.disconnect().catch(this.logger.error); diff --git a/src/Commands/rewind.ts b/src/Commands/rewind.ts index f237011a0..d89411c91 100644 --- a/src/Commands/rewind.ts +++ b/src/Commands/rewind.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class Rewind extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); if(!context.server.player.isPlaying){ message.reply(t("notPlaying")).catch(this.logger.error); diff --git a/src/Commands/search.ts b/src/Commands/search.ts index e180d7ceb..433c8356a 100644 --- a/src/Commands/search.ts +++ b/src/Commands/search.ts @@ -20,33 +20,33 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; import type { SongInfo } from "../Component/searchPanel"; import type * as dYtsr from "@distube/ytsr"; -import type { i18n } from "i18next"; import type * as ytsr from "ytsr"; import { MessageActionRowBuilder, MessageButtonBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; -import { BaseCommand } from "."; +import { BaseCommand, getCommandExecutionContext } from "."; import { searchYouTube } from "../AudioSource"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { DefaultAudioThumbnailURL } from "../definition"; export abstract class SearchBase extends BaseCommand { - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); // URLが渡されたら、そのままキューに追加を試みる if(this.urlCheck(context.rawArgs)){ - const joinResult = await context.server.joinVoiceChannel(message, { replyOnFail: true }, t); + const joinResult = await context.server.joinVoiceChannel(message, { replyOnFail: true }); if(!joinResult){ return; } - await context.server.playFromURL(message, context.args as string[], { first: !context.server.player.isConnecting }, t); + await context.server.playFromURL(message, context.args as string[], { first: !context.server.player.isConnecting }); return; } // ボイスチャンネルへの参加の試みをしておく - context.server.joinVoiceChannel(message, {}, t).catch(this.logger.error); + context.server.joinVoiceChannel(message, {}).catch(this.logger.error); // 検索パネルがすでにあるなら if(context.server.searchPanel.has(message.member.id)){ @@ -92,11 +92,11 @@ export abstract class SearchBase extends BaseCommand { // 検索を実行する if(context.rawArgs !== ""){ - const searchPanel = context.server.searchPanel.create(message, context.rawArgs, t); + const searchPanel = context.server.searchPanel.create(message, context.rawArgs); if(!searchPanel){ return; } - await searchPanel.consumeSearchResult(this.searchContent(context.rawArgs, context, t), this.consumer.bind(this), t); + await searchPanel.consumeSearchResult(this.searchContent(context.rawArgs, context), this.consumer.bind(this)); }else{ await message.reply(t("commands:search.noArgument")).catch(this.logger.error); } @@ -106,10 +106,10 @@ export abstract class SearchBase extends BaseCommand { * 検索を実行する関数 * 検索時にクエリーの変換を行う場合は、変換後のクエリをtransfomedQueryとして返す必要があります。 */ - protected abstract searchContent(query: string, context: CommandArgs, t: i18n["t"]): Promise; + protected abstract searchContent(query: string, context: CommandArgs): Promise; /** 検索結果を検索パネルで使用できるデータに変換する関数 */ - protected abstract consumer(result: T, t: i18n["t"]): SongInfo[]; + protected abstract consumer(result: T): SongInfo[]; /** この検索が対象とするURLかを判断する関数 */ // eslint-disable-next-line unused-imports/no-unused-vars @@ -118,7 +118,7 @@ export abstract class SearchBase extends BaseCommand { } } -const config = useConfig(); +const config = getConfig(); export default class Search extends SearchBase { constructor(){ @@ -126,7 +126,7 @@ export default class Search extends SearchBase { alias: ["search", "se"], unlist: false, category: "playlist", - argument: [{ + args: [{ type: "string", name: "keyword", required: true, @@ -148,7 +148,9 @@ export default class Search extends SearchBase { }); } - protected override consumer(items: ytsr.Video[] | dYtsr.Video[], t: i18n["t"]){ + protected override consumer(items: ytsr.Video[] | dYtsr.Video[]){ + const { t } = getCommandExecutionContext(); + return items.map(item => ({ url: item.url, title: "title" in item ? item.title : `*${item.name}`, diff --git a/src/Commands/searchnico.ts b/src/Commands/searchnico.ts index f090da6fe..6842e7b6c 100644 --- a/src/Commands/searchnico.ts +++ b/src/Commands/searchnico.ts @@ -17,16 +17,16 @@ */ import type { SongInfo } from "../Component/searchPanel"; -import type { i18n } from "i18next"; import candyget from "candyget"; +import { getCommandExecutionContext } from "."; import { SearchBase } from "./search"; import { time } from "../Util"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; -const config = useConfig(); +const config = getConfig(); export default class SearchN extends SearchBase { constructor() { @@ -34,7 +34,7 @@ export default class SearchN extends SearchBase { alias: ["ニコニコを検索", "ニコ動を検索", "searchnico", "searchniconico", "searchn"], unlist: false, category: "playlist", - argument: [ + args: [ { type: "string", name: "keyword", @@ -53,7 +53,9 @@ export default class SearchN extends SearchBase { return searchNicoNico(query); } - protected override consumer(result: Datum[], t: i18n["t"]): SongInfo[] { + protected override consumer(result: Datum[]): SongInfo[] { + const { t } = getCommandExecutionContext(); + return result.map(item => { const [min, sec] = time.calcMinSec(Math.floor(item.lengthSeconds)); return { diff --git a/src/Commands/searchqueue.ts b/src/Commands/searchqueue.ts index 380e92ac3..dbf2ce56a 100644 --- a/src/Commands/searchqueue.ts +++ b/src/Commands/searchqueue.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import type { EmbedField } from "oceanic.js"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -34,7 +33,7 @@ export default class Searchq extends BaseCommand { alias: ["searchqueue", "searchq", "seq", "sq"], unlist: false, category: "playlist", - argument: [{ + args: [{ type: "string", name: "keyword", required: true, @@ -46,7 +45,8 @@ export default class Searchq extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); if(context.server.queue.length === 0){ diff --git a/src/Commands/searchsoundcloud.ts b/src/Commands/searchsoundcloud.ts index f5c3114cc..d8db5eab6 100644 --- a/src/Commands/searchsoundcloud.ts +++ b/src/Commands/searchsoundcloud.ts @@ -17,18 +17,18 @@ */ import type { SoundCloudTrackCollection } from "../AudioSource"; -import type { i18n } from "i18next"; import type { SoundcloudTrackV2 } from "soundcloud.ts"; import candyget from "candyget"; import Soundcloud from "soundcloud.ts"; +import { getCommandExecutionContext } from "."; import { SearchBase } from "./search"; import * as Util from "../Util"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { DefaultUserAgent } from "../definition"; -const config = useConfig(); +const config = getConfig(); export default class Searchs extends SearchBase { private readonly soundcloud = new Soundcloud(); @@ -38,7 +38,7 @@ export default class Searchs extends SearchBase { alias: ["soundcloudを検索", "searchsoundcloud", "searchs", "ses", "ss", "sc", "soundcloud"], unlist: false, category: "playlist", - argument: [{ + args: [{ type: "string", name: "keyword", required: true, @@ -88,7 +88,9 @@ export default class Searchs extends SearchBase { }; } - protected override consumer(result: SoundcloudTrackV2[], t: i18n["t"]){ + protected override consumer(result: SoundcloudTrackV2[]){ + const { t } = getCommandExecutionContext(); + return result.map(item => { const [min, sec] = Util.time.calcMinSec(Math.floor(item.duration / 1000)); return { diff --git a/src/Commands/seek.ts b/src/Commands/seek.ts index aa05753fd..78bd02adb 100644 --- a/src/Commands/seek.ts +++ b/src/Commands/seek.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -28,7 +27,7 @@ export default class Seek extends BaseCommand { alias: ["seek"], unlist: false, category: "player", - argument: [{ + args: [{ type: "string", name: "keyword", required: true, @@ -40,7 +39,8 @@ export default class Seek extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); const server = context.server; diff --git a/src/Commands/setting_nowplaying.ts b/src/Commands/setting_nowplaying.ts new file mode 100644 index 000000000..d280cd298 --- /dev/null +++ b/src/Commands/setting_nowplaying.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2021-2023 mtripg6666tdr + * + * This file is part of mtripg6666tdr/Discord-SimpleMusicBot. + * (npm package name: 'discord-music-bot' / repository url: ) + * + * mtripg6666tdr/Discord-SimpleMusicBot is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * mtripg6666tdr/Discord-SimpleMusicBot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with mtripg6666tdr/Discord-SimpleMusicBot. + * If not, see . + */ + +import type { CommandArgs } from "."; +import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; + +import { BaseCommand } from "."; +import { NowPlayingNotificationLevel } from "../types/GuildPreferences"; + +export default class SettingNowPlayingNotification extends BaseCommand { + constructor(){ + super({ + alias: ["setting>nowplaying"], + unlist: false, + category: "bot", + requiredPermissionsOr: ["admin", "dj", "onlyListener", "manageGuild"], + shouldDefer: false, + examples: false, + usage: false, + args: [ + { + type: "string", + name: "level", + required: false, + choices: [ + "normal", + "silent", + "disabled", + ], + }, + ], + }); + } + + async run(message: CommandMessage, context: CommandArgs){ + if(context.rawArgs){ + const level = this.resolveLevel(context.rawArgs); + + if(level === null){ + await message.reply(context.t("commands:setting>nowplaying.invalidLevel")); + return; + } + + context.server.preferences.nowPlayingNotificationLevel = level; + + await message.reply(context.t("commands:setting>nowplaying.changed", { + level: context.t(`commands:setting>nowplaying.args.level.choices.${this.levelToString(level)}`), + })); + }else{ + await message.reply(context.t("commands:setting>nowplaying.currentState", { + level: context.t(`commands:setting>nowplaying.args.level.choices.${this.levelToString(context.server.preferences.nowPlayingNotificationLevel)}`), + })); + } + } + + resolveLevel(level: string): NowPlayingNotificationLevel | null { + switch(level.toLowerCase()){ + case "normal": + case "true": + return NowPlayingNotificationLevel.Normal; + case "silent": + return NowPlayingNotificationLevel.Silent; + case "disabled": + case "false": + return NowPlayingNotificationLevel.Disable; + default: + return null; + } + } + + levelToString(level: NowPlayingNotificationLevel) { + switch(level){ + case NowPlayingNotificationLevel.Normal: + return "normal" as const; + case NowPlayingNotificationLevel.Silent: + return "silent" as const; + case NowPlayingNotificationLevel.Disable: + return "disabled" as const; + } + } +} diff --git a/src/Commands/setting_skipvote.ts b/src/Commands/setting_skipvote.ts new file mode 100644 index 000000000..c15c4e8fe --- /dev/null +++ b/src/Commands/setting_skipvote.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2021-2023 mtripg6666tdr + * + * This file is part of mtripg6666tdr/Discord-SimpleMusicBot. + * (npm package name: 'discord-music-bot' / repository url: ) + * + * mtripg6666tdr/Discord-SimpleMusicBot is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * mtripg6666tdr/Discord-SimpleMusicBot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with mtripg6666tdr/Discord-SimpleMusicBot. + * If not, see . + */ + +import type { CommandArgs } from "."; +import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; + +import { BaseCommand } from "."; + +export default class SettingSkipvote extends BaseCommand { + constructor(){ + super({ + alias: ["setting>skipvote"], + unlist: false, + category: "bot", + requiredPermissionsOr: ["admin", "dj", "onlyListener", "manageGuild"], + shouldDefer: false, + examples: false, + usage: false, + args: [ + { + type: "bool", + name: "enabled", + required: false, + }, + ], + }); + } + + async run(message: CommandMessage, context: CommandArgs){ + if(context.rawArgs){ + const newDisabledStatus = context.server.preferences.disableSkipSession = !(context.args[0] === "enable" || context.args[0] === "true"); + + await message.reply(context.t("commands:setting>skipvote.changed", { + status: newDisabledStatus ? context.t("disabled") : context.t("enabled"), + })); + }else{ + await message.reply(context.t("commands:setting>skipvote.currentState", { + status: context.server.preferences.disableSkipSession ? context.t("disabled") : context.t("enabled"), + })); + } + } +} diff --git a/src/Commands/shuffle.ts b/src/Commands/shuffle.ts index 71933d6ec..b21d89015 100644 --- a/src/Commands/shuffle.ts +++ b/src/Commands/shuffle.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -33,7 +32,8 @@ export default class Shuffle extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); if(context.server.queue.length === 0){ message.reply(t("commands:shuffle.queueEmpty")).catch(this.logger.error); diff --git a/src/Commands/skip.ts b/src/Commands/skip.ts index 3aed97dbe..f7cc3d504 100644 --- a/src/Commands/skip.ts +++ b/src/Commands/skip.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; import { discordUtil } from "../Util"; @@ -34,7 +33,8 @@ export default class Skip extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; const server = context.server; // そもそも再生状態じゃない if(server.player.preparing){ @@ -59,6 +59,7 @@ export default class Skip extends BaseCommand { && !discordUtil.users.isDJ(message.member, context) && !discordUtil.users.isPrivileged(message.member) && members && members.size > 3 + && !context.server.preferences.disableSkipSession ){ // 投票パネルを作成する if(!server.skipSession){ diff --git a/src/Commands/thumbnail.ts b/src/Commands/thumbnail.ts index 953e345cb..601cb46c0 100644 --- a/src/Commands/thumbnail.ts +++ b/src/Commands/thumbnail.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -32,7 +31,7 @@ export default class Thumbnail extends BaseCommand { alias: ["サムネ", "thumbnail", "thumb", "t"], unlist: false, category: "player", - argument: [{ + args: [{ type: "integer", name: "index", required: false, @@ -44,7 +43,8 @@ export default class Thumbnail extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); const embed = new MessageEmbedBuilder(); diff --git a/src/Commands/uptime.ts b/src/Commands/uptime.ts index d501fd354..2eabe6c96 100644 --- a/src/Commands/uptime.ts +++ b/src/Commands/uptime.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; @@ -38,7 +37,8 @@ export default class Uptime extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; const now = Date.now(); const insta = Util.time.calcTime(now - context.bot.instantiatedTime!.getTime()); const ready = Util.time.calcTime(context.client.uptime); diff --git a/src/Commands/volume.ts b/src/Commands/volume.ts index b3abc3499..ef7c58709 100644 --- a/src/Commands/volume.ts +++ b/src/Commands/volume.ts @@ -18,7 +18,6 @@ import type { CommandArgs } from "."; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; -import type { i18n } from "i18next"; import { BaseCommand } from "."; @@ -28,7 +27,7 @@ export default class Volume extends BaseCommand { alias: ["volume", "vol"], unlist: false, category: "voice", - argument: [{ + args: [{ type: "integer", name: "volume", required: false, @@ -40,7 +39,8 @@ export default class Volume extends BaseCommand { }); } - async run(message: CommandMessage, context: CommandArgs, t: i18n["t"]){ + async run(message: CommandMessage, context: CommandArgs){ + const { t } = context; context.server.updateBoundChannel(message); if(context.rawArgs === ""){ await message.reply(`:loud_sound:${t("commands:volume.currentVolume", { volume: context.server.player.volume })}`) diff --git a/src/Component/audioEffectManager.ts b/src/Component/audioEffectManager.ts index 6676f817a..7547d515e 100644 --- a/src/Component/audioEffectManager.ts +++ b/src/Component/audioEffectManager.ts @@ -17,10 +17,10 @@ */ import type { GuildDataContainer } from "../Structure"; -import type { i18n } from "i18next"; import { MessageButtonBuilder, MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; +import { getCommandExecutionContext } from "../Commands"; import { ServerManagerBase } from "../Structure"; import { getColor } from "../Util/color"; @@ -68,7 +68,7 @@ const audioEffects = { arg: "asetrate=48000*1.2,aresample=48000,bass=g=5", shouldDisableVbr: false, }, -} satisfies Record; +} as const satisfies Record; export type ExportedAudioEffect = { args: string[], @@ -108,7 +108,9 @@ export class AudioEffectManager extends ServerManagerBase { return { args, shouldDisableVbr }; } - createEmbed(avatarUrl: string, t: i18n["t"]){ + createEmbed(avatarUrl: string){ + const { t } = getCommandExecutionContext(); + const embed = new MessageEmbedBuilder() .setTitle(`:cd:${t("commands:effect.effectControllPanel.title")}:microphone:`) .setDescription(t("commands:effect.effectControllPanel.description")) diff --git a/src/Component/backupper/httpBased.ts b/src/Component/backupper/httpBased.ts index ec0e04604..583050483 100644 --- a/src/Component/backupper/httpBased.ts +++ b/src/Component/backupper/httpBased.ts @@ -16,9 +16,9 @@ * If not, see . */ -import type { exportableStatuses } from "."; import type { YmxFormat } from "../../Structure"; import type { DataType, MusicBotBase } from "../../botBase"; +import type { JSONStatuses } from "../../types/GuildStatuses"; import candyget from "candyget"; @@ -56,7 +56,7 @@ export class HttpBackupper extends IntervalBackupper { this.logger.info("Backing up modified status..."); const statuses: { [guildId: string]: string } = {}; - const originalStatuses: { [guildId: string]: exportableStatuses } = {}; + const originalStatuses: { [guildId: string]: JSONStatuses } = {}; statusModifiedGuildIds.forEach(id => { const status = this.data.get(id)?.exportStatus(); if(!status) return; @@ -69,6 +69,8 @@ export class HttpBackupper extends IntervalBackupper { status.addRelatedSongs ? "1" : "0", status.equallyPlayback ? "1" : "0", status.volume, + status.disableSkipSession, + status.nowPlayingNotificationLevel, ].join(":"); originalStatuses[id] = status; }); @@ -159,7 +161,7 @@ export class HttpBackupper extends IntervalBackupper { ); if(result.status === 200){ const frozenGuildStatuses = result.data; - const map = new Map(); + const map = new Map(); Object.keys(frozenGuildStatuses).forEach(key => { const [ voiceChannelId, @@ -169,6 +171,8 @@ export class HttpBackupper extends IntervalBackupper { addRelatedSongs, equallyPlayback, volume, + disableSkipSession, + nowPlayingNotificationLevel, ] = frozenGuildStatuses[key].split(":"); const numVolume = Number(volume) || 100; const b = (v: string) => v === "1"; @@ -180,6 +184,8 @@ export class HttpBackupper extends IntervalBackupper { addRelatedSongs: b(addRelatedSongs), equallyPlayback: b(equallyPlayback), volume: numVolume >= 1 && numVolume <= 200 ? numVolume : 100, + disableSkipSession: b(disableSkipSession), + nowPlayingNotificationLevel: Number(nowPlayingNotificationLevel) || 0, }); }); return map; diff --git a/src/Component/backupper/index.ts b/src/Component/backupper/index.ts index 7b9aebbb9..4d928b4fa 100644 --- a/src/Component/backupper/index.ts +++ b/src/Component/backupper/index.ts @@ -22,16 +22,7 @@ import type { DataType, MusicBotBase } from "../../botBase"; import { isDeepStrictEqual } from "util"; import { LogEmitter } from "../../Structure"; - -export type exportableStatuses = { - voiceChannelId: string, - boundChannelId: string, - loopEnabled: boolean, - queueLoopEnabled: boolean, - addRelatedSongs: boolean, - equallyPlayback: boolean, - volume: number, -}; +import { JSONStatuses } from "../../types/GuildStatuses"; // eslint-disable-next-line @typescript-eslint/ban-types export abstract class Backupper extends LogEmitter<{}> { @@ -48,7 +39,7 @@ export abstract class Backupper extends LogEmitter<{}> { /** * バックアップ済みの接続ステータス等を取得します */ - abstract getStatusFromBackup(guildIds: string[]): Promise | null>; + abstract getStatusFromBackup(guildIds: string[]): Promise | null>; /** * バックアップ済みのキューのデータを取得します */ @@ -90,7 +81,7 @@ export abstract class IntervalBackupper extends Backupper { await this.backupQueue(); } - protected updateStatusCache(guildId: string, status: exportableStatuses){ + protected updateStatusCache(guildId: string, status: JSONStatuses){ this.previousStatusCache.set(guildId, JSON.stringify(status)); } diff --git a/src/Component/backupper/mongodb.ts b/src/Component/backupper/mongodb.ts index cbf0f145e..6d6a7b641 100644 --- a/src/Component/backupper/mongodb.ts +++ b/src/Component/backupper/mongodb.ts @@ -16,10 +16,10 @@ * If not, see . */ -import type { exportableStatuses } from "."; import type { BaseCommand, CommandArgs } from "../../Commands"; import type { GuildDataContainer, YmxFormat } from "../../Structure"; import type { DataType, MusicBotBase } from "../../botBase"; +import type { JSONStatuses } from "../../types/GuildStatuses"; import type * as mongo from "mongodb"; import { Backupper } from "."; @@ -46,7 +46,7 @@ export class MongoBackupper extends Backupper { private dbConnectionReady = false; private dbError: Error | null = null; collections: { - status: mongo.Collection>, + status: mongo.Collection>, queue: mongo.Collection>, analytics: mongo.Collection, } = null!; @@ -69,7 +69,7 @@ export class MongoBackupper extends Backupper { this.logger.info("Database connection ready"); const db = this.client.db(process.env.DB_TOKEN || "discord_music_bot_backup"); this.collections = { - status: db.collection>("Status"), + status: db.collection>("Status"), queue: db.collection>("Queue"), analytics: db.collection("Analytics"), }; @@ -91,6 +91,7 @@ export class MongoBackupper extends Backupper { container.queue.eitherOn(["change", "changeWithoutCurrent"], backupQueueFuncFactory(container.getGuildId())); container.queue.on("settingsChanged", backupStatusFuncFactory(container.getGuildId())); container.player.on("all", backupStatusFuncFactory(container.getGuildId())); + container.preferences.on("updateSettings", backupStatusFuncFactory(container.getGuildId())); container.player.on("reportPlaybackDuration", this.addPlayerAnalyticsEvent.bind(this, container.getGuildId())); }; @@ -210,7 +211,7 @@ export class MongoBackupper extends Backupper { guildId: id, })), }); - const result = new Map(); + const result = new Map(); for await(const doc of dbResult){ result.set(doc.guildId, doc); } diff --git a/src/Component/backupper/replit.ts b/src/Component/backupper/replit.ts index c12742992..922e4d124 100644 --- a/src/Component/backupper/replit.ts +++ b/src/Component/backupper/replit.ts @@ -16,15 +16,13 @@ * If not, see . */ -import type { exportableStatuses } from "."; import type { YmxFormat } from "../../Structure"; import type { DataType, MusicBotBase } from "../../botBase"; - -import candyget from "candyget"; -import PQueue from "p-queue"; +import type { JSONStatuses } from "../../types/GuildStatuses"; import { IntervalBackupper } from "."; import { timeLoggedMethod } from "../../logger"; +import { ReplitClient } from "../replitDatabaseClient"; export class ReplitBackupper extends IntervalBackupper { protected readonly db: ReplitClient = null!; @@ -113,11 +111,11 @@ export class ReplitBackupper extends IntervalBackupper { @timeLoggedMethod override async getStatusFromBackup(guildIds: string[]) { - const result = new Map(); + const result = new Map(); try{ await Promise.allSettled( guildIds.map(async id => { - const status = await this.db.get(this.getDbKey("status", id)); + const status = await this.db.get(this.getDbKey("status", id)); if(status){ result.set(id, status); this.updateStatusCache(id, status); @@ -140,65 +138,3 @@ export class ReplitBackupper extends IntervalBackupper { override destroy(){ } } - -class ReplitClient { - protected baseUrl: string; - protected queue: PQueue; - - constructor(baseUrl: string){ - this.baseUrl = baseUrl; - - if(!this.baseUrl){ - throw new Error("No URL found"); - } - - this.queue = new PQueue({ - concurrency: 3, - timeout: 10e3, - throwOnTimeout: true, - intervalCap: 4, - interval: 10, - }); - } - - get(key: string, options: { raw: true }): Promise; - get(key: string, options?: { raw: false }): Promise; - get(key: string, options?: { raw: boolean }){ - return this.queue.add(async () => { - const shouldRaw = options?.raw || false; - const { body } = await candyget(`${this.baseUrl}/${key}`, "string"); - if(!body){ - return null; - }else if(shouldRaw){ - return body; - }else{ - return JSON.parse(body); - } - }); - } - - set(key: string, value: any){ - return this.queue.add(async () => { - const textData = JSON.stringify(value); - - const { statusCode } = await candyget.post(this.baseUrl, "empty", { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - }, - }, `${encodeURIComponent(key)}=${encodeURIComponent(textData)}`); - - if(statusCode >= 200 && statusCode <= 299){ - return this; - }else{ - throw new Error(`Status code: ${statusCode}`); - } - }); - } - - delete(key: string){ - return this.queue.add(async () => { - await candyget.delete(`${this.baseUrl}/${key}`, "empty"); - return this; - }); - } -} diff --git a/src/Component/commandManager.ts b/src/Component/commandManager.ts index 3a25d5525..78783d9ff 100644 --- a/src/Component/commandManager.ts +++ b/src/Component/commandManager.ts @@ -18,14 +18,17 @@ import type { BaseCommand } from "../Commands"; import type { CommandOptionsTypes } from "../Structure"; -import type { AnyApplicationCommand, ChatInputApplicationCommand, Client, CreateApplicationCommandOptions, MessageApplicationCommand } from "oceanic.js"; +import type { AnyApplicationCommand, ApplicationCommandOptions, ApplicationCommandOptionsSubCommand, ApplicationCommandOptionsWithValue, ChatInputApplicationCommand, Client, CreateApplicationCommandOptions, CreateChatInputApplicationCommandOptions, Locale, MessageApplicationCommand } from "oceanic.js"; import util from "util"; +import i18next from "i18next"; import { ApplicationCommandOptionTypes, ApplicationCommandTypes } from "oceanic.js"; import { LogEmitter } from "../Structure"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; +import { subCommandSeparator } from "../definition"; +import { availableLanguages } from "../i18n"; import { timeLoggedMethod } from "../logger"; // const commandSeparator = "_"; @@ -45,13 +48,17 @@ export class CommandManager extends LogEmitter<{}> { } private readonly _commands: BaseCommand[]; - /** - * コマンドを返します - */ + /** コマンドを返します */ get commands(): Readonly{ return this._commands; } + private _subCommandNames: Set; + /** サブコマンドがあるコマンドの一覧を返します */ + get subCommandNames(): Readonly>{ + return this._subCommandNames; + } + private commandMap: Map; private constructor(){ @@ -60,25 +67,38 @@ export class CommandManager extends LogEmitter<{}> { this._commands = (require("../Commands/_index") as typeof import("../Commands/_index")).default.filter(n => !n.disabled); - this.initializeMap({ reportDupes: useConfig().debug }); + this.initializeMap({ reportDupes: getConfig().debug }); + this.initializeSubcommandNames(); + this.logger.info("Initialized"); } private initializeMap({ reportDupes }: { reportDupes: boolean }){ const sets = new Map(); + const setCommand = (name: string, command: BaseCommand) => { if(sets.has(name) && reportDupes && !command.interactionOnly){ this.logger.warn(`Detected command ${command.name} the duplicated key ${name} with ${sets.get(name)!.name}; overwriting`); } sets.set(name, command); }; + this.commands.forEach(command => { setCommand(command.name, command); command.alias.forEach(name => setCommand(name, command)); }); + this.commandMap = sets; } + private initializeSubcommandNames(){ + this._subCommandNames = new Set( + this.commands + .filter(command => command.asciiName.includes(">")) + .map(command => command.asciiName.split(">")[0]) + ); + } + /** * コマンド名でコマンドを解決します * @param command コマンド名 @@ -105,10 +125,12 @@ export class CommandManager extends LogEmitter<{}> { this.logger.info("Start syncing application commands"); // format local commands into the api-compatible well-formatted ones - const apiCompatibleCommands: CreateApplicationCommandOptions[] = this.commands - .filter(command => !command.unlist) - .flatMap(command => command.toApplicationCommandStructure()) - .map(command => this.apiToApplicationCommand(command as unknown as AnyApplicationCommand) as CreateApplicationCommandOptions); + const apiCompatibleCommands: CreateApplicationCommandOptions[] = this.groupSubcommands( + this.commands + .filter(command => !command.unlist) + .flatMap(command => command.toApplicationCommandStructure()) + .map(command => this.apiToApplicationCommand(command as unknown as AnyApplicationCommand) as CreateApplicationCommandOptions) + ); // Get registered commands const registeredAppCommands = await client.application.getGlobalCommands({ withLocalizations: true }); @@ -169,6 +191,7 @@ export class CommandManager extends LogEmitter<{}> { const actual = registeredAppCommands.find( command => command.type === ApplicationCommandTypes.CHAT_INPUT && command.name === expected.name ) as ChatInputApplicationCommand; + if(!actual){ return false; }else{ @@ -178,6 +201,7 @@ export class CommandManager extends LogEmitter<{}> { const actual = registeredAppCommands.find( command => command.type === ApplicationCommandTypes.MESSAGE && command.name === expected.name ) as MessageApplicationCommand; + if(!actual){ return false; }else{ @@ -251,38 +275,52 @@ export class CommandManager extends LogEmitter<{}> { defaultMemberPermissions, }; }else if(apiCommand.options){ + const optionMapper = (option: ApplicationCommandOptions) => { + if("choices" in option && option.choices){ + return { + type: option.type, + name: option.name, + description: option.description, + descriptionLocalizations: option.descriptionLocalizations, + required: !!option.required, + choices: option.choices.map(choice => ({ + name: choice.name, + value: choice.value, + // @ts-expect-error + nameLocalizations: choice.nameLocalizations || choice.name_localizations || null, + // @ts-expect-error + name_localizations: choice.nameLocalizations || choice.name_localizations || null, + })), + }; + }else{ + return { + type: option.type, + name: option.name, + description: option.description, + descriptionLocalizations: option.descriptionLocalizations, + required: !!option.required, + autocomplete: "autocomplete" in option && option.autocomplete || false, + }; + } + }; + return { type: apiCommand.type, name: apiCommand.name, description: apiCommand.description, - descriptionLocalizations: apiCommand.descriptionLocalizations, + descriptionLocalizations: apiCommand.descriptionLocalizations || {}, defaultMemberPermissions, options: apiCommand.options.map(option => { - if("choices" in option && option.choices){ + if(option.type === ApplicationCommandOptionTypes.SUB_COMMAND){ return { - type: option.type, - name: option.name, description: option.description, - descriptionLocalizations: option.descriptionLocalizations, - required: !!option.required, - choices: option.choices.map(choice => ({ - name: choice.name, - value: choice.value, - // @ts-expect-error - nameLocalizations: choice.nameLocalizations || choice.name_localizations || null, - // @ts-expect-error - name_localizations: choice.nameLocalizations || choice.name_localizations || null, - })), - }; - }else{ - return { - type: option.type, + descriptionLocalizations: option.descriptionLocalizations || {}, name: option.name, - description: option.description, - descriptionLocalizations: option.descriptionLocalizations, - required: !!option.required, - autocomplete: "autocomplete" in option && option.autocomplete || false, + type: option.type, + options: option.options?.map(optionMapper), }; + }else{ + return optionMapper(option); } }), }; @@ -309,4 +347,65 @@ export class CommandManager extends LogEmitter<{}> { return ApplicationCommandOptionTypes.ATTACHMENT; } } + + /**サブコマンドをグループ化します。現時点では、ネストは一段階のみ対応しています。 */ + protected groupSubcommands(commands: CreateApplicationCommandOptions[]): CreateApplicationCommandOptions[] { + const subcommandGroups = new Map(); + + const normalCommands = commands.filter(c => { + if(!c.name.includes(subCommandSeparator) || c.type !== ApplicationCommandTypes.CHAT_INPUT){ + return true; + } + + const baseName = c.name.split(subCommandSeparator)[0]; + + if(subcommandGroups.has(baseName)){ + subcommandGroups.get(baseName)!.from.push(c); + }else{ + subcommandGroups.set(baseName, { from: [c], to: null }); + } + + return false; + }); + + for(const key of subcommandGroups.keys()){ + const group = subcommandGroups.get(key)!; + + if(group.from.some(c => c.name === key)){ + throw new Error("Top level command that has subcommands cannot be command itself."); + } + + if(!group.to){ + group.to = { + type: ApplicationCommandTypes.CHAT_INPUT, + name: key, + description: i18next.t(`commands:${key}.description` as any), + defaultMemberPermissions: group.from[0].defaultMemberPermissions, + descriptionLocalizations: {}, + options: [], + }; + + availableLanguages().forEach(language => { + if(i18next.language === language) return; + const localized: string = i18next.t(`commands:${key}.description` as any, { lng: language }).substring(0, 100); + if(localized === group.to!.description) return; + group.to!.descriptionLocalizations![language as Locale] = localized.trim(); + }); + } + + group.from.forEach(command => { + const subcommand: ApplicationCommandOptionsSubCommand = { + type: ApplicationCommandOptionTypes.SUB_COMMAND, + name: command.name.substring(key.length + 1), + description: command.description, + descriptionLocalizations: command.descriptionLocalizations || {}, + options: command.options as ApplicationCommandOptionsWithValue[], + }; + + group.to!.options!.push(subcommand); + }); + } + + return [...normalCommands, ...[...subcommandGroups.values()].map(d => d.to!)]; + } } diff --git a/src/Component/commandResolver/CommandMessage.ts b/src/Component/commandResolver/CommandMessage.ts index 44bc01ba0..c3df2ad1e 100644 --- a/src/Component/commandResolver/CommandMessage.ts +++ b/src/Component/commandResolver/CommandMessage.ts @@ -17,10 +17,17 @@ */ import { CommandMessage as LibCommandMessage } from "@mtripg6666tdr/oceanic-command-resolver"; +import { defaultConfig } from "@mtripg6666tdr/oceanic-command-resolver"; +import { AnyTextableGuildChannel, Message } from "oceanic.js"; import { normalizeText } from "../../Util"; +import { subCommandSeparator } from "../../definition"; +import { CommandManager } from "../commandManager"; + +defaultConfig.subCommandSeparator = subCommandSeparator; export class CommandMessage extends LibCommandMessage { + // 超省略形を解釈するために、基底クラスをオーバーライドします protected static override parseCommand(content: string, prefixLength: number){ const resolved = super.parseCommand(content, prefixLength, normalizeText); // 超省略形を捕捉 @@ -32,9 +39,19 @@ export class CommandMessage extends LibCommandMessage { return resolved; } - static override resolveCommandMessage(content: string, prefixLength: number = 1){ - const resolved = CommandMessage.parseCommand(content, prefixLength); - resolved.command = resolved.command.toLowerCase(); - return resolved; + // サブコマンドを解決するために、基底クラスをオーバーライドします + static override createFromMessage(message: Message, prefixLength?: number | undefined) { + const resolved = this.parseCommand(message.content, prefixLength || 1); + + if(CommandManager.instance.subCommandNames.has(resolved.command)){ + const subCommand = resolved.options.shift(); + + if(subCommand){ + resolved.command = `${resolved.command}${subCommandSeparator}${subCommand}`; + resolved.rawOptions = resolved.options.join(" "); + } + } + + return CommandMessage.createFromMessageWithParsed(message, resolved.command, resolved.options, resolved.rawOptions); } } diff --git a/src/Component/playManager.ts b/src/Component/playManager.ts index fc74632b5..3f7836341 100644 --- a/src/Component/playManager.ts +++ b/src/Component/playManager.ts @@ -18,7 +18,6 @@ import type { GuildDataContainer } from "../Structure"; import type { AudioPlayer } from "@discordjs/voice"; -import type { Member, Message, TextChannel } from "oceanic.js"; import type { Readable } from "stream"; @@ -26,6 +25,7 @@ import { NoSubscriberBehavior } from "@discordjs/voice"; import { AudioPlayerStatus, createAudioResource, createAudioPlayer, entersState, StreamType, VoiceConnectionStatus } from "@discordjs/voice"; import { MessageActionRowBuilder, MessageButtonBuilder, MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; import i18next from "i18next"; +import { MessageFlags, type Member, type Message, type TextChannel } from "oceanic.js"; import { FixedAudioResource } from "./audioResource"; import { resolveStreamToPlayable } from "./streams"; @@ -35,8 +35,9 @@ import { type AudioSource } from "../AudioSource"; import { ServerManagerBase } from "../Structure"; import * as Util from "../Util"; import { getColor } from "../Util/color"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { timeLoggedMethod } from "../logger"; +import { NowPlayingNotificationLevel } from "../types/GuildPreferences"; interface PlayManagerEvents { volumeChanged: [volume:string]; @@ -63,11 +64,11 @@ interface PlayManagerEvents { export type PlayManagerPlayOptions = Record & { /** シークする際に、シーク先の秒数を指定します */ time?: number, - /** エラーが発生した際にエラーを紐づけテキストチャンネルに送信するかどうかを指定します */ - quietOnError?: boolean, + /** エテキストチャンネルにメッセージを送信するかどうかを指定します */ + quiet?: boolean, }; -const config = useConfig(); +const config = getConfig(); /** * サーバーごとの再生を管理するマネージャー。 @@ -163,7 +164,7 @@ export class PlayManager extends ServerManagerBase { // コンストラクタ constructor(parent: GuildDataContainer){ super("PlayManager", parent); - this.logger.info("PlayManager instantiated"); + this.logger.info("PlayManager instantiated."); } setVolume(val: number){ @@ -181,7 +182,7 @@ export class PlayManager extends ServerManagerBase { @timeLoggedMethod async play(options: PlayManagerPlayOptions = {}): Promise{ let time = options.time || 0; - const quietOnError = options.quietOnError || false; + const quiet = options.quiet || false; this.emit("playCalled", time); @@ -197,58 +198,67 @@ export class PlayManager extends ServerManagerBase { let mes: Message | null = null; this._currentAudioInfo = this.server.queue.get(0).basicInfo; - // 通知メッセージを送信する(可能なら) - if(this.getNoticeNeeded() && !quietOnError){ - const [min, sec] = Util.time.calcMinSec(this.currentAudioInfo!.lengthSeconds); - const isYT = this.currentAudioInfo!.isYouTube(); - const isLive = isYT && this.currentAudioInfo.isLiveStream; - - if(isYT && this.currentAudioInfo.availableAfter){ - const waitTarget = this.currentAudioInfo; - // まだ始まっていないライブを待機する - mes = await this.server.bot.client.rest.channels.createMessage( - this.server.boundTextChannel, - { - content: `:stopwatch:${i18next.t("components:play.waitingForLiveStream", { - lng: this.server.locale, - title: this.currentAudioInfo.title, - })}`, - } - ); - this.preparing = false; - const abortController = this._waitForLiveAbortController = new AbortController(); - this.once("stop", () => { + const [min, sec] = Util.time.calcMinSec(this.currentAudioInfo!.lengthSeconds); + const isYT = this.currentAudioInfo!.isYouTube(); + const isLive = isYT && this.currentAudioInfo.isLiveStream; + + if(isYT && this.currentAudioInfo.availableAfter){ + const waitTarget = this.currentAudioInfo; + // まだ始まっていないライブを待機する + mes = this.getNoticeNeeded() && !quiet && await this.server.bot.client.rest.channels.createMessage( + this.server.boundTextChannel, + { + content: `:stopwatch:${i18next.t("components:play.waitingForLiveStream", { + lng: this.server.locale, + title: this.currentAudioInfo.title, + })}`, + } + ) || null; + this.preparing = false; + const abortController = this._waitForLiveAbortController = new AbortController(); + this.once("stop", () => { + abortController.abort(); + }); + await waitTarget.waitForLive(abortController.signal, () => { + if(waitTarget !== this._currentAudioInfo){ abortController.abort(); - }); - await waitTarget.waitForLive(abortController.signal, () => { - if(waitTarget !== this._currentAudioInfo){ - abortController.abort(); - } - }); - if(abortController.signal.aborted){ - this._waitForLiveAbortController = null; - await mes.edit({ - content: `:white_check_mark:${i18next.t("components:play.waitingForLiveCanceled", { lng: this.server.locale })}`, - }); - return this; } + }); + if(abortController.signal.aborted){ this._waitForLiveAbortController = null; - this.preparing = true; - }else{ - mes = await this.server.bot.client.rest.channels.createMessage( - this.server.boundTextChannel, - { - content: `:hourglass_flowing_sand:${ - i18next.t("components:play.preparing", { - title: `\`${this.currentAudioInfo!.title}\` \`(${ - isLive ? i18next.t("liveStream", { lng: this.server.locale }) : `${min}:${sec}` - })\``, - lng: this.server.locale, - }) - }...`, - } - ); + const content = `:white_check_mark:${i18next.t("components:play.waitingForLiveCanceled", { lng: this.server.locale })}`; + + if(mes){ + mes.edit({ content }).catch(this.logger.error); + }else{ + this.server.bot.client.rest.channels.createMessage( + this.server.boundTextChannel, + { content } + ).catch(this.logger.error); + } + + return this; } + this._waitForLiveAbortController = null; + this.preparing = true; + }else if(this.getNoticeNeeded() && !quiet && this.server.preferences.nowPlayingNotificationLevel !== NowPlayingNotificationLevel.Disable){ + // 通知メッセージを送信する(可能なら) + mes = await this.server.bot.client.rest.channels.createMessage( + this.server.boundTextChannel, + { + content: `:hourglass_flowing_sand:${ + i18next.t("components:play.preparing", { + title: `\`${this.currentAudioInfo!.title}\` \`(${ + isLive ? i18next.t("liveStream", { lng: this.server.locale }) : `${min}:${sec}` + })\``, + lng: this.server.locale, + }) + }...`, + flags: this.server.preferences.nowPlayingNotificationLevel === NowPlayingNotificationLevel.Silent + ? MessageFlags.SUPPRESS_NOTIFICATIONS + : 0, + } + ); } try{ @@ -316,13 +326,13 @@ export class PlayManager extends ServerManagerBase { this.setVolume(this.volume); // wait for player entering the playing state - const waitSuccess = await entersState(this._player!, AudioPlayerStatus.Playing, 10e3) + const waitingSucceeded = await entersState(this._player!, AudioPlayerStatus.Playing, 10e3) .then(() => true) .catch(() => false); // when occurring one or more error(s) while waiting for player, // the error(s) should be also emitted from AudioPlayer and handled by PlayManager#handleError // so simply ignore the error(s) here. - if(!waitSuccess){ + if(!waitingSucceeded){ return this; } @@ -332,7 +342,7 @@ export class PlayManager extends ServerManagerBase { this.logger.info("Playback started successfully"); - if(mes && !quietOnError){ + if(mes && !quiet){ // 再生開始メッセージ const messageContent = this.createNowPlayingMessage(); @@ -792,7 +802,7 @@ export class PlayManager extends ServerManagerBase { if(this.server.queue.length === 1 && this.server.queue.queueLoopEnabled) this.server.queue.queueLoopEnabled = false; await this.server.queue.next(); } - await this.play({ quietOnError: quiet }); + await this.play({ quiet: quiet }); } override emit(event: U, ...args: PlayManagerEvents[U]): boolean { diff --git a/src/Component/playManagerWithBgm.ts b/src/Component/playManagerWithBgm.ts index 178cb1aa6..8a42f76bd 100644 --- a/src/Component/playManagerWithBgm.ts +++ b/src/Component/playManagerWithBgm.ts @@ -109,7 +109,7 @@ export class PlayManagerWithBgm extends PlayManager { this.logger.info("Queue empty"); await this.disconnect(); }else{ - await this.play({ quietOnError: true, bgm: true }); + await this.play({ quiet: true, bgm: true }); } }else{ return super.onStreamFinished(); diff --git a/src/Component/preferencesManager.ts b/src/Component/preferencesManager.ts new file mode 100644 index 000000000..ec6ef4347 --- /dev/null +++ b/src/Component/preferencesManager.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2021-2023 mtripg6666tdr + * + * This file is part of mtripg6666tdr/Discord-SimpleMusicBot. + * (npm package name: 'discord-music-bot' / repository url: ) + * + * mtripg6666tdr/Discord-SimpleMusicBot is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * mtripg6666tdr/Discord-SimpleMusicBot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with mtripg6666tdr/Discord-SimpleMusicBot. + * If not, see . + */ + +import { GuildDataContainer } from "../Structure/GuildDataContainer"; +import { ServerManagerBase } from "../Structure/ServerManagerBase"; +import { JSONGuildPreferences, NowPlayingNotificationLevel } from "../types/GuildPreferences"; + +interface GuildPreferencesEvents { + updateSettings: []; +} + +export class GuildPreferencesManager extends ServerManagerBase { + constructor(parent: GuildDataContainer){ + super("GuildPreferencesManager", parent); + this.logger.info("GuildPreferencesManager initialized."); + + this.init(); + } + + protected init(){ + this.addRelated = false; + this.equallyPlayback = false; + this.disableSkipSession = false; + this.nowPlayingNotificationLevel = NowPlayingNotificationLevel.Normal; + } + + exportPreferences(): JSONGuildPreferences { + return { + addRelatedSongs: this.addRelated, + equallyPlayback: this.equallyPlayback, + disableSkipSession: this.disableSkipSession, + nowPlayingNotificationLevel: this.nowPlayingNotificationLevel, + }; + } + + importPreferences(preferences: JSONGuildPreferences){ + this.addRelated = preferences.addRelatedSongs; + this.equallyPlayback = preferences.equallyPlayback; + this.disableSkipSession = preferences.disableSkipSession; + this.nowPlayingNotificationLevel = preferences.nowPlayingNotificationLevel; + } + + + protected _addRelated: boolean; + /** 関連動画自動追加が有効 */ + get addRelated(){ + return this._addRelated; + } + set addRelated(value: boolean){ + if(this._addRelated !== value){ + this.emit("updateSettings"); + } + + this._addRelated = value; + } + + + protected _equallyPlayback: boolean; + /** 均等再生が有効 */ + get equallyPlayback(){ + return this._equallyPlayback; + } + set equallyPlayback(value: boolean){ + if(this._equallyPlayback !== value){ + this.emit("updateSettings"); + } + + this._equallyPlayback = value; + } + + + protected _disableSkipSession: boolean; + /** スキップ投票を無効にするか */ + get disableSkipSession(){ + return this._disableSkipSession; + } + set disableSkipSession(value: boolean){ + if(this._disableSkipSession !== value){ + this.emit("updateSettings"); + } + + this._disableSkipSession = value; + } + + + protected _nowPlayingNotificationLevel: NowPlayingNotificationLevel; + get nowPlayingNotificationLevel(){ + return this._nowPlayingNotificationLevel; + } + set nowPlayingNotificationLevel(value: NowPlayingNotificationLevel){ + if(this._nowPlayingNotificationLevel !== value){ + this.emit("updateSettings"); + } + + this._nowPlayingNotificationLevel = value; + } +} diff --git a/src/Component/queueManager.ts b/src/Component/queueManager.ts index cb7dce444..bb30a84f0 100644 --- a/src/Component/queueManager.ts +++ b/src/Component/queueManager.ts @@ -21,7 +21,7 @@ import type { ResponseMessage } from "./commandResolver/ResponseMessage"; import type { TaskCancellationManager } from "./taskCancellationManager"; import type { AudioSourceBasicJsonFormat } from "../AudioSource"; import type { GuildDataContainer } from "../Structure"; -import type { AddedBy, QueueContent } from "../Structure/QueueContent"; +import type { AddedBy, QueueContent } from "../types/QueueContent"; import type { AnyTextableGuildChannel, EditMessageOptions, Message, MessageActionRow } from "oceanic.js"; import { lock, LockObj } from "@mtripg6666tdr/async-lock"; @@ -32,11 +32,12 @@ import ytmpl from "yt-mix-playlist"; import ytdl from "ytdl-core"; import * as AudioSource from "../AudioSource"; +import { getCommandExecutionContext } from "../Commands"; import { ServerManagerBase } from "../Structure"; import * as Util from "../Util"; import { getColor } from "../Util/color"; import { bindThis } from "../Util/decorators"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { timeLoggedMethod } from "../logger"; export type KnownAudioSourceIdentifer = "youtube"|"custom"|"soundcloud"|"spotify"|"unknown"; @@ -49,7 +50,7 @@ interface QueueManagerEvents { mixPlaylistEnabledChanged: [enabled: boolean]; } -const config = useConfig(); +const config = getConfig(); /** * サーバーごとのキューを管理するマネージャー。 @@ -158,7 +159,7 @@ export class QueueManager extends ServerManagerBase { constructor(parent: GuildDataContainer){ super("QueueManager", parent); - this.logger.info("QueueManager instantiated"); + this.logger.info("QueueManager initialized."); } /** @@ -247,7 +248,6 @@ export class QueueManager extends ServerManagerBase { }, this.server.bot.cache, preventSourceCache, - i18next.getFixedT(this.server.locale) ), additionalInfo: { addedBy: { @@ -258,7 +258,7 @@ export class QueueManager extends ServerManagerBase { } as QueueContent; if(result.basicInfo){ this._default[method](result); - if(this.server.equallyPlayback) this.sortWithAddedBy(); + if(this.server.preferences.equallyPlayback) this.sortByAddedBy(); this.emit(method === "push" ? "changeWithoutCurrent" : "change"); this.emit("add", result); const index = this._default.findIndex(q => q === result); @@ -299,6 +299,7 @@ export class QueueManager extends ServerManagerBase { ): Promise { this.logger.info("AutoAddQueue Called"); let uiMessage: Message | ResponseMessage | null = null; + const { t } = getCommandExecutionContext(); try{ // UI表示するためのメッセージを特定する作業 @@ -310,8 +311,8 @@ export class QueueManager extends ServerManagerBase { content: "", embeds: [ new MessageEmbedBuilder() - .setTitle(i18next.t("pleaseWait", { lng: this.server.locale })) - .setDescription(`${i18next.t("loadingInfo", { lng: this.server.locale })}...`) + .setTitle(t("pleaseWait")) + .setDescription(`${t("loadingInfo")}...`) .toOceanic(), ], allowedMentions: { @@ -327,7 +328,7 @@ export class QueueManager extends ServerManagerBase { // まだないの場合(新しくUI用のメッセージを生成する) this.logger.info("AutoAddQueue will make a message that will be used to report statuses"); uiMessage = await options.channel.createMessage({ - content: i18next.t("loadingInfoPleaseWait", { lng: this.server.locale }), + content: t("loadingInfoPleaseWait"), }); } @@ -335,8 +336,8 @@ export class QueueManager extends ServerManagerBase { if(this.server.queue.length > 999){ // キュー上限 this.logger.warn("AutoAddQueue failed due to too long queue"); - // eslint-disable-next-line @typescript-eslint/no-throw-literal - throw i18next.t("components:queue.tooManyQueueItems", { lng: this.server.locale }); + + throw new Error(t("components:queue.tooManyQueueItems")); } // キューへの追加を実行 @@ -370,40 +371,40 @@ export class QueueManager extends ServerManagerBase { // 埋め込みの作成 const embed = new MessageEmbedBuilder() .setColor(getColor("SONG_ADDED")) - .setTitle(`✅${i18next.t("components:queue.songAdded", { lng: this.server.locale })}`) + .setTitle(`✅${t("components:queue.songAdded")}`) .setDescription(info.basicInfo.isPrivateSource ? info.basicInfo.title : `[${info.basicInfo.title}](${info.basicInfo.url})`) .addField( - i18next.t("length", { lng: this.server.locale }), + t("length"), info.basicInfo.isYouTube() && info.basicInfo.isLiveStream - ? i18next.t("liveStream", { lng: this.server.locale }) + ? t("liveStream") : _t !== 0 ? min + ":" + sec - : i18next.t("unknown", { lng: this.server.locale }), + : t("unknown"), true ) .addField( - i18next.t("components:nowplaying.requestedBy", { lng: this.server.locale }), - options.addedBy?.displayName || i18next.t("unknown", { lng: this.server.locale }), + t("components:nowplaying.requestedBy"), + options.addedBy?.displayName || t("unknown"), true ) .addField( - i18next.t("components:queue.positionInQueue", { lng: this.server.locale }), + t("components:queue.positionInQueue"), index === "0" ? `${ - i18next.t("components:nowplaying.nowplayingItemName", { lng: this.server.locale }) + t("components:nowplaying.nowplayingItemName") }/${ - i18next.t("components:nowplaying.waitForPlayingItemName", { lng: this.server.locale }) + t("components:nowplaying.waitForPlayingItemName") }` : index, true ) .addField( - i18next.t("components:queue.etaToPlay", { lng: this.server.locale }), + t("components:queue.etaToPlay"), index === "0" ? "-" : timeFragments[2].includes("-") - ? i18next.t("unknown", { lng: this.server.locale }) - : Util.time.HourMinSecToString(timeFragments, i18next.getFixedT(this.server.locale)), + ? t("unknown") + : Util.time.HourMinSecToString(timeFragments, t), true ) ; @@ -411,16 +412,16 @@ export class QueueManager extends ServerManagerBase { if(info.basicInfo.isYouTube()){ if(info.basicInfo.isFallbacked){ embed.addField( - `:warning:${i18next.t("attention", { lng: this.server.locale })}`, - i18next.t("components:queue.fallbackNotice", { lng: this.server.locale }) + `:warning:${t("attention")}`, + t("components:queue.fallbackNotice") ); }else if(info.basicInfo.strategyId === 1){ embed.setTitle(`${embed.title}*`); } }else if(info.basicInfo instanceof AudioSource.Spotify){ embed.addField( - `:warning:${i18next.t("attention", { lng: this.server.locale })}`, - i18next.t("components:queue.spotifyNotice", { lng: this.server.locale }) + `:warning:${t("attention")}`, + t("components:queue.spotifyNotice") ); } @@ -444,7 +445,7 @@ export class QueueManager extends ServerManagerBase { .addComponents( new MessageButtonBuilder() .setCustomId(collectorCreateResult.customIdMap.cancelLast) - .setLabel(i18next.t("cancel", { lng: this.server.locale })) + .setLabel(t("cancel")) .setStyle("DANGER") ) .toOceanic() @@ -455,13 +456,13 @@ export class QueueManager extends ServerManagerBase { const item = this.get(this.length - 1); this.removeAt(this.length - 1); interaction.createFollowup({ - content: `🚮${i18next.t("components:queue.cancelAdded", { title: item.basicInfo.title, lng: this.server.locale })}`, + content: `🚮${t("components:queue.cancelAdded", { title: item.basicInfo.title })}`, }).catch(this.logger.error); } catch(er){ this.logger.error(er); interaction.createFollowup({ - content: i18next.t("errorOccurred"), + content: t("errorOccurred"), }).catch(this.logger.error); } }); @@ -507,7 +508,9 @@ export class QueueManager extends ServerManagerBase { this.logger.error("AutoAddQueue failed", e); if(uiMessage){ uiMessage.edit({ - content: `:weary:${i18next.t("components:queue.failedToAdd", { lng: this.server.locale })}${typeof e === "object" && "message" in e ? `(${e.message})` : ""}`, + content: `:weary:${t("components:queue.failedToAdd")}${ + typeof e === "object" && "message" in e ? `(${e.message})` : "" + }`, embeds: [], }) .catch(this.logger.error) @@ -593,7 +596,7 @@ export class QueueManager extends ServerManagerBase { if(this.queueLoopEnabled){ this._default.push(this.default[0]); - }else if(this.server.addRelated && this.server.player.currentAudioInfo instanceof AudioSource.YouTube){ + }else if(this.server.preferences.addRelated && this.server.player.currentAudioInfo instanceof AudioSource.YouTube){ const relatedVideos = this.server.player.currentAudioInfo.relatedVideos; if(relatedVideos.length >= 1){ const video = relatedVideos[0]; @@ -734,8 +737,8 @@ export class QueueManager extends ServerManagerBase { this._default.sort(() => Math.random() - 0.5); this.emit("change"); } - if(this.server.equallyPlayback){ - this.sortWithAddedBy(addedByOrder); + if(this.server.preferences.equallyPlayback){ + this.sortByAddedBy(addedByOrder); } } @@ -784,7 +787,7 @@ export class QueueManager extends ServerManagerBase { /** * 追加者によってできるだけ交互になるようにソートします */ - sortWithAddedBy(addedByUsers?: string[]){ + sortByAddedBy(addedByUsers?: string[]){ // 追加者の一覧とマップを作成 const generateUserOrder = !addedByUsers; addedByUsers = addedByUsers || []; diff --git a/src/Component/queueManagerWithBGM.ts b/src/Component/queueManagerWithBGM.ts index e4f08b5f2..122012266 100644 --- a/src/Component/queueManagerWithBGM.ts +++ b/src/Component/queueManagerWithBGM.ts @@ -18,13 +18,12 @@ import type { KnownAudioSourceIdentifer } from "./queueManager"; import type { GuildDataContainerWithBgm } from "../Structure/GuildDataContainerWithBgm"; -import type { AddedBy, QueueContent } from "../Structure/QueueContent"; +import type { AddedBy, QueueContent } from "../types/QueueContent"; import type { Member } from "oceanic.js"; import * as fs from "fs"; import * as path from "path"; -import i18next from "i18next"; import { QueueManager } from "./queueManager"; import * as AudioSource from "../AudioSource"; @@ -91,18 +90,24 @@ export class QueueManagerWithBgm extends QueueManager { && !url.startsWith("https://") && fs.existsSync(path.join(__dirname, global.BUNDLED ? "../" : "../../", url)) ){ - const result = { - basicInfo: await new AudioSource.FsStream().init(url, null, i18next.getFixedT(this.server.locale)), + const result: QueueContent = { + basicInfo: await new AudioSource.FsStream().init(url, null), additionalInfo: { addedBy: { userId: addedBy ? this.getUserIdFromMember(addedBy) : "0", displayName: addedBy?.displayName || "unknown", }, }, - } as QueueContent; + }; + this._default[method](result); - if(this.server.equallyPlayback) this.sortWithAddedBy(); + + if(this.server.preferences.equallyPlayback){ + this.sortByAddedBy(); + } + const index = this._default.findIndex(q => q === result); + return { ...result, index }; } return super.addQueueOnly({ url, addedBy, method, sourceType, gotData, preventCache }); diff --git a/src/Component/replitDatabaseClient.ts b/src/Component/replitDatabaseClient.ts new file mode 100644 index 000000000..1925591f0 --- /dev/null +++ b/src/Component/replitDatabaseClient.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2021-2023 mtripg6666tdr + * + * This file is part of mtripg6666tdr/Discord-SimpleMusicBot. + * (npm package name: 'discord-music-bot' / repository url: ) + * + * mtripg6666tdr/Discord-SimpleMusicBot is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * mtripg6666tdr/Discord-SimpleMusicBot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with mtripg6666tdr/Discord-SimpleMusicBot. + * If not, see . + */ + +import candyget from "candyget"; +import PQueue from "p-queue"; + +export class ReplitClient { + protected baseUrl: string; + protected queue: PQueue; + + constructor(baseUrl: string){ + this.baseUrl = baseUrl; + + if(!this.baseUrl){ + throw new Error("No URL found"); + } + + this.queue = new PQueue({ + concurrency: 3, + timeout: 10e3, + throwOnTimeout: true, + intervalCap: 4, + interval: 10, + }); + } + + get(key: string, options: { raw: true }): Promise; + get(key: string, options?: { raw: false }): Promise; + get(key: string, options?: { raw: boolean }){ + return this.queue.add(async () => { + const shouldRaw = options?.raw || false; + const { body } = await candyget(`${this.baseUrl}/${key}`, "string"); + if(!body){ + return null; + }else if(shouldRaw){ + return body; + }else{ + return JSON.parse(body); + } + }); + } + + set(key: string, value: any){ + return this.queue.add(async () => { + const textData = JSON.stringify(value); + + const { statusCode } = await candyget.post(this.baseUrl, "empty", { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }, `${encodeURIComponent(key)}=${encodeURIComponent(textData)}`); + + if(statusCode >= 200 && statusCode <= 299){ + return this; + }else{ + throw new Error(`Status code: ${statusCode}`); + } + }); + } + + delete(key: string){ + return this.queue.add(async () => { + await candyget.delete(`${this.baseUrl}/${key}`, "empty"); + return this; + }); + } +} diff --git a/src/Component/searchPanel.ts b/src/Component/searchPanel.ts index e40f1ef37..612105e6d 100644 --- a/src/Component/searchPanel.ts +++ b/src/Component/searchPanel.ts @@ -23,9 +23,10 @@ import type { SelectOption } from "oceanic.js"; import { MessageActionRowBuilder, MessageEmbedBuilder, MessageStringSelectMenuBuilder } from "@mtripg6666tdr/oceanic-command-resolver/helper"; +import { getCommandExecutionContext } from "../Commands"; import { LogEmitter } from "../Structure"; import { getColor } from "../Util/color"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { timeLoggedMethod } from "../logger"; type status = "init"|"consumed"|"destroyed"; @@ -35,7 +36,7 @@ interface SearchPanelEvents { open: [reply: ResponseMessage]; } -const config = useConfig(); +const config = getConfig(); export class SearchPanel extends LogEmitter { protected _status: status = "init"; @@ -75,7 +76,12 @@ export class SearchPanel extends LogEmitter { } @timeLoggedMethod - async consumeSearchResult(searchPromise: Promise, consumer: (result: T, t: i18n["t"]) => SongInfo[], t: i18n["t"]){ + async consumeSearchResult( + searchPromise: Promise, + consumer: (result: T, t: i18n["t"]) => SongInfo[] + ){ + const { t } = getCommandExecutionContext(); + if(this.status !== "init"){ return false; } diff --git a/src/Component/searchPanelManager.ts b/src/Component/searchPanelManager.ts index 07909405b..a56e9561d 100644 --- a/src/Component/searchPanelManager.ts +++ b/src/Component/searchPanelManager.ts @@ -18,9 +18,9 @@ import type { CommandMessage } from "./commandResolver/CommandMessage"; import type { GuildDataContainer } from "../Structure"; -import type { i18n } from "i18next"; import { SearchPanel } from "./searchPanel"; +import { getCommandExecutionContext } from "../Commands"; import { ServerManagerBase } from "../Structure"; interface SearchPanelManagerEvents { @@ -40,7 +40,9 @@ export class SearchPanelManager extends ServerManagerBase= 3){ _commandMessage.reply(`:cry:${t("components:search.maximumSearch")}`) .catch(this.logger.error); diff --git a/src/Component/skipSession.ts b/src/Component/skipSession.ts index 41b55220e..80b6aa108 100644 --- a/src/Component/skipSession.ts +++ b/src/Component/skipSession.ts @@ -20,7 +20,7 @@ import type { InteractionCollector } from "./collectors/InteractionCollector"; import type { CommandMessage } from "./commandResolver/CommandMessage"; import type { ResponseMessage } from "./commandResolver/ResponseMessage"; import type { GuildDataContainer } from "../Structure"; -import type { QueueContent } from "../Structure/QueueContent"; +import type { QueueContent } from "../types/QueueContent"; import type { Member } from "oceanic.js"; import { lock, LockObj } from "@mtripg6666tdr/async-lock"; diff --git a/src/Component/sourceCache.ts b/src/Component/sourceCache.ts index d8d20597c..7c57271a2 100644 --- a/src/Component/sourceCache.ts +++ b/src/Component/sourceCache.ts @@ -31,7 +31,7 @@ import { lock, LockObj } from "@mtripg6666tdr/async-lock"; import { LogEmitter } from "../Structure"; import { getMBytes } from "../Util/system"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { timeLoggedMethod } from "../logger"; interface CacheEvents { @@ -41,7 +41,7 @@ interface CacheEvents { persistentCacheNotFound: []; } -const config = useConfig(); +const config = getConfig(); export class SourceCache extends LogEmitter { private readonly _sourceCache: Map>; diff --git a/src/Structure/GuildDataContainer.ts b/src/Structure/GuildDataContainer.ts index f23d55952..59d7d533d 100644 --- a/src/Structure/GuildDataContainer.ts +++ b/src/Structure/GuildDataContainer.ts @@ -16,16 +16,15 @@ * If not, see . */ -import type { CommandArgs } from "./Command"; -import type { QueueContent } from "./QueueContent"; -import type { YmxFormat } from "./YmxFormat"; import type { AudioSourceBasicJsonFormat, SpotifyJsonFormat } from "../AudioSource"; -import type { exportableStatuses } from "../Component/backupper"; import type { CommandMessage } from "../Component/commandResolver/CommandMessage"; import type { SearchPanel } from "../Component/searchPanel"; import type { MusicBotBase } from "../botBase"; +import type { CommandArgs } from "../types/Command"; +import type { JSONStatuses } from "../types/GuildStatuses"; +import type { QueueContent } from "../types/QueueContent"; +import type { YmxFormat } from "../types/YmxFormat"; import type { VoiceConnection } from "@discordjs/voice"; -import type { i18n } from "i18next"; import type { AnyTextableGuildChannel, Message, StageChannel, VoiceChannel } from "oceanic.js"; import type { TextChannel } from "oceanic.js"; import type { Playlist } from "spotify-url-info"; @@ -36,26 +35,28 @@ import { MessageEmbedBuilder } from "@mtripg6666tdr/oceanic-command-resolver/hel import Soundcloud from "soundcloud.ts"; import { LogEmitter } from "./LogEmitter"; -import { YmxVersion } from "./YmxFormat"; import { Spotify } from "../AudioSource"; import { SoundCloudS } from "../AudioSource"; import { Playlist as ytpl } from "../AudioSource/youtube/playlist"; +import { getCommandExecutionContext } from "../Commands"; import { AudioEffectManager } from "../Component/audioEffectManager"; import { PlayManager } from "../Component/playManager"; +import { GuildPreferencesManager } from "../Component/preferencesManager"; import { QueueManager } from "../Component/queueManager"; import { SearchPanelManager } from "../Component/searchPanelManager"; import { SkipSession } from "../Component/skipSession"; import { TaskCancellationManager } from "../Component/taskCancellationManager"; import * as Util from "../Util"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import { discordLanguages } from "../i18n"; import { getLogger } from "../logger"; +import { YmxVersion } from "../types/YmxFormat"; interface GuildDataContainerEvents { updateBoundChannel: [string]; } -const config = useConfig(); +const config = getConfig(); /** * サーバーごとデータを保存するコンテナ @@ -69,6 +70,7 @@ export class GuildDataContainer extends LogEmitter { /** プレフィックス */ prefix: string; + // キューマネージャー protected _queue: QueueManager; /** キューマネジャ */ @@ -76,6 +78,7 @@ export class GuildDataContainer extends LogEmitter { return this._queue; } + // プレーマネージャー protected _player: PlayManager; /** 再生マネジャ */ @@ -83,6 +86,7 @@ export class GuildDataContainer extends LogEmitter { return this._player; } + // 検索パネルマネージャー protected _searchPanel: SearchPanelManager; /** 検索パネルマネジャ */ @@ -90,19 +94,27 @@ export class GuildDataContainer extends LogEmitter { return this._searchPanel; } + protected _audioEffects: AudioEffectManager; /** オーディオエフェクトマネジャ */ get audioEffects(){ return this._audioEffects; } - // スキップセッション + protected _skipSession: SkipSession | null = null; /** スキップセッション */ get skipSession(){ return this._skipSession; } + + protected _preferences: GuildPreferencesManager; + /** 設定 */ + get preferences(){ + return this._preferences; + } + private _boundTextChannel: string; /** 紐づけテキストチャンネルを取得します */ get boundTextChannel(){ @@ -115,10 +127,7 @@ export class GuildDataContainer extends LogEmitter { /** メインボット */ readonly bot: MusicBotBase; - /** 関連動画自動追加が有効 */ - addRelated: boolean; - /** 均等再生が有効 */ - equallyPlayback: boolean; + /** VCへの接続 */ connection: VoiceConnection | null; /** VC */ @@ -152,14 +161,13 @@ export class GuildDataContainer extends LogEmitter { throw new Error("Invalid bound textchannel id was given"); } this.bot = bot; - this.addRelated = false; this.prefix = ">"; - this.equallyPlayback = false; this.connection = null; this.initPlayManager(); this.initQueueManager(); this.initSearchPanelManager(); - this.initAudioEffectManager(); + this.initAudioEffects(); + this.initPreferences(); } // 子クラスでオーバーライドされる可能性があるので必要 @@ -178,10 +186,14 @@ export class GuildDataContainer extends LogEmitter { } // 同上 - protected initAudioEffectManager(){ + protected initAudioEffects(){ this._audioEffects = new AudioEffectManager(this); } + protected initPreferences(){ + this._preferences = new GuildPreferencesManager(this); + } + /** * 状況に応じてバインドチャンネルを更新します * @param message 更新元となるメッセージ @@ -245,16 +257,15 @@ export class GuildDataContainer extends LogEmitter { * ステータスをエクスポートします * @returns ステータスのオブジェクト */ - exportStatus(): exportableStatuses{ + exportStatus(): JSONStatuses { // VCのID:バインドチャンネルのID:ループ:キューループ:関連曲 return { voiceChannelId: this.player.isPlaying && !this.player.isPaused ? this.connectingVoiceChannel!.id : "0", boundChannelId: this.boundTextChannel, loopEnabled: this.queue.loopEnabled, queueLoopEnabled: this.queue.queueLoopEnabled, - addRelatedSongs: this.addRelated, - equallyPlayback: this.equallyPlayback, volume: this.player.volume, + ...this.preferences.exportPreferences(), }; } @@ -262,12 +273,11 @@ export class GuildDataContainer extends LogEmitter { * ステータスをオブジェクトからインポートします。 * @param param0 読み取り元のオブジェクト */ - importStatus(statuses: exportableStatuses){ + importStatus(statuses: JSONStatuses): void { //VCのID:バインドチャンネルのID:ループ:キューループ:関連曲 - this.queue.loopEnabled = statuses.loopEnabled; - this.queue.queueLoopEnabled = statuses.queueLoopEnabled; - this.addRelated = statuses.addRelatedSongs; - this.equallyPlayback = statuses.equallyPlayback; + this.queue.loopEnabled = !!statuses.loopEnabled; + this.queue.queueLoopEnabled = !!statuses.queueLoopEnabled; + this.preferences.importPreferences(statuses); this.player.setVolume(statuses.volume); if(statuses.voiceChannelId !== "0"){ this.joinVoiceChannelOnly(statuses.voiceChannelId) @@ -370,10 +380,11 @@ export class GuildDataContainer extends LogEmitter { */ async joinVoiceChannel( message: CommandMessage, - { reply = false, replyOnFail = false }: { reply?: boolean, replyOnFail?: boolean }, - t: i18n["t"] + { reply = false, replyOnFail = false }: { reply?: boolean, replyOnFail?: boolean } ): Promise{ return lock(this.joinVoiceChannelLocker, async () => { + const { t } = getCommandExecutionContext(); + if(message.member.voiceState?.channelID){ const targetVC = this.bot.client.getChannel(message.member.voiceState.channelID)!; @@ -457,9 +468,10 @@ export class GuildDataContainer extends LogEmitter { first?: boolean, cancellable?: boolean, privateSource?: boolean, - }, - t: i18n["t"] + } ): Promise { + const { t } = getCommandExecutionContext(); + if(Array.isArray(rawArg)){ const [firstUrl, ...restUrls] = rawArg .flatMap(fragment => Util.normalizeText(fragment).split(" ")) @@ -470,7 +482,7 @@ export class GuildDataContainer extends LogEmitter { // eslint-disable-next-line prefer-spread results.push.apply( results, - await this.playFromURL(message, firstUrl, { first, cancellable: false }, t) + await this.playFromURL(message, firstUrl, { first, cancellable: false }) ); if(restUrls){ @@ -786,24 +798,24 @@ export class GuildDataContainer extends LogEmitter { message: Message, context: CommandArgs, morePrefs: { first?: boolean, cancellable?: boolean }, - t: i18n["t"], ){ + const { t } = getCommandExecutionContext(); const prefixLength = context.server.prefix.length; if(message.content.startsWith("http://") || message.content.startsWith("https://")){ // URLのみのメッセージか? - await context.server.playFromURL(commandMessage, message.content, morePrefs, t); + await context.server.playFromURL(commandMessage, message.content, morePrefs); return; }else if( message.content.substring(prefixLength).startsWith("http://") || message.content.substring(prefixLength).startsWith("https://") ){ // プレフィックス+URLのメッセージか? - await context.server.playFromURL(commandMessage, message.content.substring(prefixLength), morePrefs, t); + await context.server.playFromURL(commandMessage, message.content.substring(prefixLength), morePrefs); return; }else if(message.attachments.size > 0){ // 添付ファイル付きか? - await context.server.playFromURL(commandMessage, message.attachments.first()!.url, morePrefs, t); + await context.server.playFromURL(commandMessage, message.attachments.first()!.url, morePrefs); return; }else if(message.author.id === context.client.user.id || config.isWhiteListedBot(message.author.id)){ // ボットのメッセージなら @@ -819,7 +831,7 @@ export class GuildDataContainer extends LogEmitter { const url = embed.description?.match(/^\[.+\]\((?https?.+)\)/)?.groups!.url; if(url){ - await context.server.playFromURL(commandMessage, url, morePrefs, t); + await context.server.playFromURL(commandMessage, url, morePrefs); return; } } @@ -834,7 +846,7 @@ export class GuildDataContainer extends LogEmitter { * @param nums インデックス番号の配列 * @param message */ - async playFromSearchPanelOptions(nums: string[], panel: SearchPanel, t: i18n["t"]){ + async playFromSearchPanelOptions(nums: string[], panel: SearchPanel){ const includingNums = panel.filterOnlyIncludes(nums.map(n => Number(n)).filter(n => !isNaN(n))); const { @@ -854,7 +866,7 @@ export class GuildDataContainer extends LogEmitter { // 現在の状態を確認してVCに接続中なら接続試行 if(panel.commandMessage.member.voiceState?.channelID){ - await this.joinVoiceChannel(panel.commandMessage, {}, t); + await this.joinVoiceChannel(panel.commandMessage, {}); } // 接続中なら再生を開始 diff --git a/src/Structure/GuildDataContainerWithBgm.ts b/src/Structure/GuildDataContainerWithBgm.ts index f7fcc607e..2727bfcfc 100644 --- a/src/Structure/GuildDataContainerWithBgm.ts +++ b/src/Structure/GuildDataContainerWithBgm.ts @@ -86,7 +86,7 @@ export class GuildDataContainerWithBgm extends GuildDataContainer { this.queue.resetBgmTracks(); } return this.joinVoiceChannelOnly(this.bgmConfig.voiceChannelId) - .then(() => this.player.play({ quietOnError: true, bgm: true })) + .then(() => this.player.play({ quiet: true, bgm: true })) .catch(this.logger.error); } } diff --git a/src/Structure/ServerManagerBase.ts b/src/Structure/ServerManagerBase.ts index 6202775e0..a837b6f65 100644 --- a/src/Structure/ServerManagerBase.ts +++ b/src/Structure/ServerManagerBase.ts @@ -26,7 +26,7 @@ import { LogEmitter } from "."; */ export abstract class ServerManagerBase extends LogEmitter { protected server: GuildDataContainer; - + constructor(tag: string, parent: GuildDataContainer){ super(tag, parent.getGuildId()); this.logger.info("Set data of guild id " + parent.getGuildId()); diff --git a/src/Structure/index.ts b/src/Structure/index.ts index 931b15ba4..8ba9cd406 100644 --- a/src/Structure/index.ts +++ b/src/Structure/index.ts @@ -18,6 +18,6 @@ export * from "./LogEmitter"; export * from "./ServerManagerBase"; -export * from "./Command"; +export * from "../types/Command"; export * from "./GuildDataContainer"; -export * from "./YmxFormat"; +export * from "../types/YmxFormat"; diff --git a/src/Util/discord.ts b/src/Util/discord.ts index c128818a2..9390ba18d 100644 --- a/src/Util/discord.ts +++ b/src/Util/discord.ts @@ -19,9 +19,9 @@ import type { CommandArgs } from "../Commands"; import type { AnyTextableGuildChannel, Member, PermissionName } from "oceanic.js"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; -const config = useConfig(); +const config = getConfig(); export const users = { isDJ(member: Member, options: CommandArgs){ diff --git a/src/Util/index.ts b/src/Util/index.ts index c22aa7238..01c3cc155 100644 --- a/src/Util/index.ts +++ b/src/Util/index.ts @@ -301,6 +301,7 @@ const normalizeTemplate = [ { from: /y/g, to: "y" } as const, { from: /z/g, to: "z" } as const, { from: />/g, to: ">" } as const, + { from: /</g, to: "<" } as const, ] as const; /** diff --git a/src/bot.ts b/src/bot.ts index 3bde29122..369069ea4 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -18,15 +18,16 @@ import type { CommandArgs } from "./Structure"; +import i18next from "i18next"; import * as discord from "oceanic.js"; import { Telemetry } from "./Component/telemetry"; import { requireIfAny } from "./Util"; import { MusicBotBase } from "./botBase"; -import { useConfig } from "./config"; +import { getConfig } from "./config"; import * as eventHandlers from "./events"; -const config = useConfig(); +const config = getConfig(); /** * 音楽ボットの本体 @@ -149,6 +150,7 @@ export class MusicBot extends MusicBotBase { initData: this.initData.bind(this), includeMention: false, locale, + t: i18next.getFixedT(locale), }; } } diff --git a/src/botBase.ts b/src/botBase.ts index 1748cf549..fd499cf72 100644 --- a/src/botBase.ts +++ b/src/botBase.ts @@ -35,7 +35,7 @@ import { GuildDataContainer } from "./Structure"; import { LogEmitter } from "./Structure"; import { GuildDataContainerWithBgm } from "./Structure/GuildDataContainerWithBgm"; import * as Util from "./Util"; -import { useConfig } from "./config"; +import { getConfig } from "./config"; export type DataType = Map; @@ -175,7 +175,7 @@ export abstract class MusicBotBase extends LogEmitter { } this.logger.info(`Version: ${this._versionInfo}`); this.initializeBackupper(); - const config = useConfig(); + const config = getConfig(); this._cacheManger = new SourceCache(this, config.cacheLevel === "persistent"); } @@ -231,7 +231,7 @@ export abstract class MusicBotBase extends LogEmitter { protected initData(guildid: string, boundChannelId: string){ const prev = this.guildData.get(guildid); if(!prev){ - const config = useConfig(); + const config = getConfig(); const server = config.bgm[guildid] ? new GuildDataContainerWithBgm(guildid, boundChannelId, this, config.bgm[guildid]) : new GuildDataContainer(guildid, boundChannelId, this); diff --git a/src/config/index.ts b/src/config/index.ts index 784b817e4..b37601873 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -26,7 +26,7 @@ import { TypeCompiler } from "@sinclair/typebox/compiler"; import { Value } from "@sinclair/typebox/value"; import CJSON from "comment-json"; -import { ConfigSchema } from "./type"; +import { ConfigSchema } from "./schema"; const DEVELOPMENT_PHASE = false; @@ -130,8 +130,8 @@ class ConfigLoader { } } -export function useConfig(){ +export function getConfig(){ return ConfigLoader.instance.config; } -export { GuildBGMContainerType } from "./type"; +export { GuildBGMContainerType } from "./schema"; diff --git a/src/config/type.ts b/src/config/schema.ts similarity index 100% rename from src/config/type.ts rename to src/config/schema.ts diff --git a/src/definition.ts b/src/definition.ts index 27f9939cf..2d755bc87 100644 --- a/src/definition.ts +++ b/src/definition.ts @@ -19,3 +19,4 @@ export const DefaultAudioThumbnailURL = "https://cdn.discordapp.com/attachments/757824315294220329/846737267951271946/Audio_icon-icons.com_71845.png"; export const DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36"; export const SecondaryUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Safari/537.36"; +export const subCommandSeparator = ">"; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 344957009..301ba49fc 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -21,10 +21,10 @@ import type { MusicBot } from "../bot"; import * as discord from "oceanic.js"; import { InteractionTypes } from "oceanic.js"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; import * as handlers from "../handlers"; -const config = useConfig(); +const config = getConfig(); export async function onInteractionCreate(this: MusicBot, interaction: discord.AnyInteractionGateway){ // コマンドインタラクションおよびコンポーネントインタラクション以外は処理せず終了 diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index d88ca06ad..fad76a3e2 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -25,9 +25,9 @@ import { CommandManager } from "../Component/commandManager"; import { CommandMessage } from "../Component/commandResolver/CommandMessage"; import { GuildDataContainerWithBgm } from "../Structure/GuildDataContainerWithBgm"; import { discordUtil, normalizeText } from "../Util"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; -const config = useConfig(); +const config = getConfig(); export async function onMessageCreate(this: MusicBot, message: discord.Message){ if(this.maintenance && !config.isBotAdmin(message.author.id)){ @@ -139,7 +139,7 @@ export async function onMessageCreate(this: MusicBot, message: discord.Message){ else if(content.match(/^([0-9]\s?)+$/)){ // メッセージ送信者が検索者と一致するかを確認 const nums = content.split(" "); - await server.playFromSearchPanelOptions(nums, panel, i18next.getFixedT(server.locale)); + await server.playFromSearchPanelOptions(nums, panel); } }else if( message.content === "キャンセル" diff --git a/src/events/ready.ts b/src/events/ready.ts index 3301fa570..aca39fbf8 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -22,9 +22,9 @@ import i18next from "i18next"; import * as discord from "oceanic.js"; import { CommandManager } from "../Component/commandManager"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; -const config = useConfig(); +const config = getConfig(); export async function onReady(this: MusicBot){ const client = this._client; diff --git a/src/events/voiceChannelLeave.ts b/src/events/voiceChannelLeave.ts index 72710de04..6dc94644e 100644 --- a/src/events/voiceChannelLeave.ts +++ b/src/events/voiceChannelLeave.ts @@ -22,9 +22,9 @@ import type * as discord from "oceanic.js"; import i18next from "i18next"; import { QueueManagerWithBgm } from "../Component/queueManagerWithBGM"; -import { useConfig } from "../config"; +import { getConfig } from "../config"; -const config = useConfig(); +const config = getConfig(); export async function onVoiceChannelLeave( this: MusicBot, diff --git a/src/handlers/commandInteraction.ts b/src/handlers/commandInteraction.ts index a89bf9d52..7f3435ab4 100644 --- a/src/handlers/commandInteraction.ts +++ b/src/handlers/commandInteraction.ts @@ -53,8 +53,10 @@ export async function handleCommandInteraction(this: MusicBot, server: GuildData return; } + // メッセージライクに解決してコマンドメッセージに + const commandMessage = CommandMessage.createFromInteraction(interaction); // コマンドを解決 - const command = CommandManager.instance.resolve(interaction.data.name); + const command = CommandManager.instance.resolve(commandMessage.command); if(!command){ await interaction.createMessage({ content: `${i18next.t("commandNotFound", { lng: interaction.locale })}:sob:`, @@ -67,8 +69,6 @@ export async function handleCommandInteraction(this: MusicBot, server: GuildData return; } - // メッセージライクに解決してコマンドメッセージに - const commandMessage = CommandMessage.createFromInteraction(interaction); // プレフィックス更新 server.updatePrefix(commandMessage); // コマンドを実行 diff --git a/src/handlers/selectMenuInteraction.ts b/src/handlers/selectMenuInteraction.ts index 3890b5cd9..6e3af98c8 100644 --- a/src/handlers/selectMenuInteraction.ts +++ b/src/handlers/selectMenuInteraction.ts @@ -21,7 +21,6 @@ import type { MusicBot } from "../bot"; import type * as discord from "oceanic.js"; import type { ComponentTypes } from "oceanic.js"; -import i18next from "i18next"; export async function handleSelectMenuInteraction( this: MusicBot, @@ -46,7 +45,7 @@ export async function handleSelectMenuInteraction( if(interaction.data.values.getStrings().includes("cancel")){ await panel.destroy(); }else{ - await server.playFromSearchPanelOptions(interaction.data.values.getStrings(), panel, i18next.getFixedT(interaction.locale)); + await server.playFromSearchPanelOptions(interaction.data.values.getStrings(), panel); } } } diff --git a/src/i18n.ts b/src/i18n.ts index c243d2514..1ff994f80 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -18,11 +18,9 @@ import type { Locale } from "oceanic.js"; - import fs from "fs"; import path from "path"; - import i18next from "i18next"; import Backend from "i18next-fs-backend"; diff --git a/src/index.ts b/src/index.ts index b5265fb33..6bcc709ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,12 +25,12 @@ import log4js from "log4js"; import { stringifyObject } from "./Util"; import { MusicBot } from "./bot"; -import { useConfig } from "./config"; +import { getConfig } from "./config"; import { initLocalization } from "./i18n"; import { createServer } from "./server"; const logger = log4js.getLogger("Entry"); -const config = useConfig(); +const config = getConfig(); logger.info("Discord-SimpleMusicBot by mtripg6666tdr"); logger.info("This application was originally built by mtripg6666tdr and is licensed under GPLv3 or later."); diff --git a/src/logger.ts b/src/logger.ts index 0c968274c..049b1a5d0 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -25,9 +25,9 @@ import { isMainThread } from "worker_threads"; import log4js from "log4js"; import { stringifyObject } from "./Util"; -import { useConfig } from "./config"; +import { getConfig } from "./config"; -const { debug, maxLogFiles } = useConfig(); +const { debug, maxLogFiles } = getConfig(); const tokens = { diff --git a/src/Structure/Command.ts b/src/types/Command.ts similarity index 93% rename from src/Structure/Command.ts rename to src/types/Command.ts index 182d24de7..7d2987d52 100644 --- a/src/Structure/Command.ts +++ b/src/types/Command.ts @@ -21,6 +21,8 @@ import type { GuildDataContainer } from "../Structure"; import type { MusicBot } from "../bot"; import type { Client, LocaleMap, PermissionName } from "oceanic.js"; +import { i18n } from "i18next"; + export type CommandPermission = | "admin" | "dj" @@ -53,15 +55,14 @@ type CommandHelp = { }; export type ListCommandWithArgsOptions = BaseListCommand & CommandHelp & { - argument: SlashCommandArgument[], + args: readonly SlashCommandArgument[], }; export type ListCommandWithoutArgsOptions = BaseListCommand & Partial; export type ListCommandInitializeOptions = | ListCommandWithArgsOptions - | ListCommandWithoutArgsOptions -; + | ListCommandWithoutArgsOptions; export type UnlistCommandOptions = BaseCommandInitializeOptions & { unlist: true, @@ -74,7 +75,7 @@ export type UnlistCommandOptions = BaseCommandInitializeOptions & { requiredPermissionsOr?: CommandPermission[], }; -export type CommandOptionsTypes = "bool"|"integer"|"string"|"file"; +export type CommandOptionsTypes = "bool" | "integer" | "string" | "file"; /** * スラッシュコマンドの引数として取れるものを定義するインターフェースです @@ -135,4 +136,8 @@ export interface CommandArgs { * ユーザーのロケール */ locale: string; + /** + * ロケールに適したt関数 + */ + t: i18n["t"]; } diff --git a/src/types/GuildPreferences.ts b/src/types/GuildPreferences.ts new file mode 100644 index 000000000..ec88b3fb6 --- /dev/null +++ b/src/types/GuildPreferences.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021-2023 mtripg6666tdr + * + * This file is part of mtripg6666tdr/Discord-SimpleMusicBot. + * (npm package name: 'discord-music-bot' / repository url: ) + * + * mtripg6666tdr/Discord-SimpleMusicBot is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * mtripg6666tdr/Discord-SimpleMusicBot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with mtripg6666tdr/Discord-SimpleMusicBot. + * If not, see . + */ + +export interface JSONGuildPreferences { + addRelatedSongs: boolean; + equallyPlayback: boolean; + disableSkipSession: boolean; + nowPlayingNotificationLevel: NowPlayingNotificationLevel; +} + +export enum NowPlayingNotificationLevel { + Normal = 0, + Silent = 1, + Disable = 2 +} diff --git a/src/types/GuildStatuses.ts b/src/types/GuildStatuses.ts new file mode 100644 index 000000000..01137aaca --- /dev/null +++ b/src/types/GuildStatuses.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021-2023 mtripg6666tdr + * + * This file is part of mtripg6666tdr/Discord-SimpleMusicBot. + * (npm package name: 'discord-music-bot' / repository url: ) + * + * mtripg6666tdr/Discord-SimpleMusicBot is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * mtripg6666tdr/Discord-SimpleMusicBot is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with mtripg6666tdr/Discord-SimpleMusicBot. + * If not, see . + */ + +import { JSONGuildPreferences } from "./GuildPreferences"; + +export type JSONStatuses = { + voiceChannelId: string, + boundChannelId: string, + loopEnabled: boolean, + queueLoopEnabled: boolean, + volume: number, +} & JSONGuildPreferences; diff --git a/src/Structure/QueueContent.ts b/src/types/QueueContent.ts similarity index 100% rename from src/Structure/QueueContent.ts rename to src/types/QueueContent.ts diff --git a/src/Structure/YmxFormat.ts b/src/types/YmxFormat.ts similarity index 100% rename from src/Structure/YmxFormat.ts rename to src/types/YmxFormat.ts diff --git a/src/@types/dytsr.d.ts b/src/types/modules/dytsr.d.ts similarity index 100% rename from src/@types/dytsr.d.ts rename to src/types/modules/dytsr.d.ts diff --git a/src/@types/global.d.ts b/src/types/modules/global.d.ts similarity index 100% rename from src/@types/global.d.ts rename to src/types/modules/global.d.ts diff --git a/src/@types/node.d.ts b/src/types/modules/node.d.ts similarity index 100% rename from src/@types/node.d.ts rename to src/types/modules/node.d.ts diff --git a/src/@types/spotify-url-info.d.ts b/src/types/modules/spotify-url-info.d.ts similarity index 100% rename from src/@types/spotify-url-info.d.ts rename to src/types/modules/spotify-url-info.d.ts diff --git a/test/load-config.test.js b/test/load-config.test.js index b20833e20..e2a1de4ab 100644 --- a/test/load-config.test.js +++ b/test/load-config.test.js @@ -58,7 +58,7 @@ describe("Config", function() { }); describe("#CheckValues", function(){ - const config = require(configLoaderPath).useConfig(); + const config = require(configLoaderPath).getConfig(); /** @type {[string, string|boolean|{}][]} */ const tests = [ ["adminId", "123456"], diff --git a/util/generateCommandList.js b/util/generateCommandList.js index c2793f37b..c501c1392 100644 --- a/util/generateCommandList.js +++ b/util/generateCommandList.js @@ -19,7 +19,7 @@ // @ts-check const fs = require("fs"); const path = require("path"); -const config = require("../dist/config").useConfig(); +const config = require("../dist/config").getConfig(); let overview = `# コマンド一覧 この節では、ボットで使用できるコマンドのほとんどが解説されています。 @@ -57,7 +57,7 @@ require("../dist/i18n").initLocalization(false, config.defaultLanguage).then(() for(let i = 0; i < commands.length; i++){ const cmd = commands[i]; - const filename = `${cmd.asciiName}.md`; + const filename = `${cmd.asciiName.replaceAll(">", "_")}.md`; if(existingFiles.has(filename)){ existingFiles.delete(filename); } @@ -67,7 +67,7 @@ sidebar_label: ${cmd.name} # \`${cmd.name}\`コマンド ${cmd.description} -スラッシュコマンドでは、\`/${cmd.asciiName}\`を使用してください。 +スラッシュコマンドでは、\`/${cmd.asciiName.replaceAll(">", " ")}\`を使用してください。 ## 別名 \`${cmd.name}\`以外にも以下の別名を使用できます。