mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
Merge pull request #198 from kveld9/feat/DRP-enhancement
feat: discord rich presence enhancement
This commit is contained in:
+48
-37
@@ -216,6 +216,16 @@ fn try_connect() -> Option<DiscordIpcClient> {
|
||||
Some(client)
|
||||
}
|
||||
|
||||
/// Apply a template string, replacing placeholders with actual values.
|
||||
/// Supported placeholders: {title}, {artist}, {album}
|
||||
fn apply_template(template: &str, title: &str, artist: &str, album: Option<&str>) -> String {
|
||||
let album_text = album.unwrap_or("");
|
||||
template
|
||||
.replace("{title}", title)
|
||||
.replace("{artist}", artist)
|
||||
.replace("{album}", album_text)
|
||||
}
|
||||
|
||||
/// Update the Discord Rich Presence activity.
|
||||
///
|
||||
/// - `is_playing`: true = playing (timer shown), false = paused (no timer, state shows "Paused").
|
||||
@@ -225,6 +235,12 @@ fn try_connect() -> Option<DiscordIpcClient> {
|
||||
/// - `fetch_itunes_covers`: if true, fetch artwork from the iTunes Search API when no
|
||||
/// `cover_art_url` is provided. If false (default), fall back to the Psysonic app icon
|
||||
/// without making any external request — required for privacy opt-in.
|
||||
/// - `details_template`: template string for the "details" field. Default: "{artist} - {title}".
|
||||
/// Supported placeholders: {title}, {artist}, {album}
|
||||
/// - `state_template`: template string for the "state" field. Default: "{album}".
|
||||
/// Supported placeholders: {title}, {artist}, {album}
|
||||
/// - `large_text_template`: template string for the large image tooltip. Default: "{album}".
|
||||
/// Supported placeholders: {title}, {artist}, {album}
|
||||
#[tauri::command]
|
||||
pub async fn discord_update_presence(
|
||||
state: tauri::State<'_, DiscordState>,
|
||||
@@ -235,6 +251,9 @@ pub async fn discord_update_presence(
|
||||
elapsed_secs: Option<f64>,
|
||||
cover_art_url: Option<String>,
|
||||
fetch_itunes_covers: bool,
|
||||
details_template: Option<String>,
|
||||
state_template: Option<String>,
|
||||
large_text_template: Option<String>,
|
||||
) -> Result<(), String> {
|
||||
// Resolve artwork on a dedicated blocking thread — reqwest::blocking must not
|
||||
// run on the Tokio async executor directly.
|
||||
@@ -273,62 +292,54 @@ pub async fn discord_update_presence(
|
||||
|
||||
let client = guard.as_mut().unwrap();
|
||||
|
||||
// Discord RPC only exposes two visible text rows (details + state).
|
||||
// The application name "Psysonic" is shown automatically by Discord as the
|
||||
// header line. Album goes into large_text — visible as a hover tooltip on
|
||||
// the cover art icon.
|
||||
let large_text = album.as_deref().unwrap_or("Psysonic");
|
||||
// Apply templates for the three configurable text fields.
|
||||
let details_str = details_template.as_deref().unwrap_or("{artist} - {title}");
|
||||
let details_text = apply_template(details_str, &title, &artist, album.as_deref());
|
||||
|
||||
let state_str = state_template.as_deref().unwrap_or("{album}");
|
||||
let state_text = apply_template(state_str, &title, &artist, album.as_deref());
|
||||
|
||||
let large_text_str = large_text_template.as_deref().unwrap_or("{album}");
|
||||
let large_text = apply_template(large_text_str, &title, &artist, album.as_deref());
|
||||
|
||||
let assets = if let Some(ref url) = artwork_url {
|
||||
Assets::new()
|
||||
.large_image(url.as_str())
|
||||
.large_text(large_text)
|
||||
.large_text(&large_text)
|
||||
} else {
|
||||
// Fallback to default Psysonic icon
|
||||
Assets::new()
|
||||
.large_image("psysonic")
|
||||
.large_text(large_text)
|
||||
.large_text(&large_text)
|
||||
};
|
||||
|
||||
// When paused, show "Paused" as the state text (replaces artist name).
|
||||
let state_text: String = if is_playing {
|
||||
artist.clone()
|
||||
} else {
|
||||
"Paused".to_string()
|
||||
};
|
||||
// When paused: clear activity completely to avoid any timer issues
|
||||
// When playing: show full activity with timer
|
||||
if !is_playing {
|
||||
if client.clear_activity().is_err() {
|
||||
*guard = None;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// ActivityType::Listening causes the Discord client to auto-start a running
|
||||
// timer from "now" even when no timestamps are provided. Switch to Playing
|
||||
// when paused — Playing only shows a timer when timestamps are explicitly set.
|
||||
let activity_type = if is_playing {
|
||||
ActivityType::Listening
|
||||
} else {
|
||||
ActivityType::Playing
|
||||
};
|
||||
|
||||
let mut activity = Activity::new()
|
||||
.activity_type(activity_type)
|
||||
.details(&title)
|
||||
// Only reach here when playing
|
||||
let activity = Activity::new()
|
||||
.activity_type(ActivityType::Listening)
|
||||
.details(&details_text)
|
||||
.state(&state_text)
|
||||
.assets(assets);
|
||||
|
||||
// Start timestamp: Discord auto-counts up from this point. We back-calculate
|
||||
// it so the displayed elapsed time matches the actual playback position.
|
||||
// Only set when playing — Playing type without timestamps shows no timer.
|
||||
if is_playing {
|
||||
if let Some(elapsed) = elapsed_secs {
|
||||
.assets(assets)
|
||||
.timestamps(if let Some(elapsed) = elapsed_secs {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
let start = now - elapsed.floor() as i64;
|
||||
activity = activity.timestamps(Timestamps::new().start(start));
|
||||
}
|
||||
}
|
||||
Timestamps::new().start(start)
|
||||
} else {
|
||||
Timestamps::new()
|
||||
});
|
||||
|
||||
if client.set_activity(activity).is_err() {
|
||||
// IPC pipe broke (Discord restarted etc.) — drop the client so the next
|
||||
// call re-connects.
|
||||
*guard = None;
|
||||
}
|
||||
|
||||
|
||||
@@ -539,6 +539,11 @@ export const deTranslation = {
|
||||
useCustomTitlebarDesc: 'Ersetzt die System-Titelleiste durch eine eingebaute, die zum App-Theme passt. Deaktivieren, um die native GNOME/GTK-Titelleiste zu verwenden.',
|
||||
discordAppleCovers: 'Cover über Apple Music für Discord laden',
|
||||
discordAppleCoversDesc: 'Sendet Künstler- und Albumname an die Apple-Such-API, um Cover für dein Discord-Profil zu finden. Standardmäßig aus Datenschutzgründen deaktiviert.',
|
||||
discordTemplates: 'Benutzerdefinierte Textvorlagen',
|
||||
discordTemplatesDesc: 'Passen Sie an, welche Informationen in Ihrem Discord-Profil angezeigt werden. Variablen: {title}, {artist}, {album}',
|
||||
discordTemplateDetails: 'Primäre Zeile (details)',
|
||||
discordTemplateState: 'Sekundäre Zeile (state)',
|
||||
discordTemplateLargeText: 'Album-Tooltip (largeText)',
|
||||
nowPlayingEnabled: 'Im Livefenster anzeigen',
|
||||
nowPlayingEnabledDesc: 'Überträgt den aktuell gespielten Titel an die Livehörer-Ansicht des Servers. Deaktivieren, um keine Wiedergabedaten zu senden.',
|
||||
lyricsServerFirst: 'Server-Lyrics bevorzugen',
|
||||
|
||||
@@ -541,6 +541,11 @@ export const enTranslation = {
|
||||
useCustomTitlebarDesc: 'Replace the system title bar with a built-in one that matches the app theme. Disable to use the native GNOME/GTK title bar.',
|
||||
discordAppleCovers: 'Fetch covers from Apple Music for Discord',
|
||||
discordAppleCoversDesc: 'Sends the artist and album name to Apple\'s search API to find cover art for your Discord profile. Disabled by default for privacy.',
|
||||
discordTemplates: 'Custom text templates',
|
||||
discordTemplatesDesc: 'Customize what information is shown on your Discord profile. Variables: {title}, {artist}, {album}',
|
||||
discordTemplateDetails: 'Primary line (details)',
|
||||
discordTemplateState: 'Secondary line (state)',
|
||||
discordTemplateLargeText: 'Album tooltip (largeText)',
|
||||
nowPlayingEnabled: 'Show in Now Playing',
|
||||
nowPlayingEnabledDesc: 'Broadcast your currently playing track to the server\'s live listener view. Disable to stop sending playback data.',
|
||||
lyricsServerFirst: 'Prefer server lyrics',
|
||||
|
||||
@@ -542,6 +542,11 @@ export const esTranslation = {
|
||||
useCustomTitlebarDesc: 'Reemplaza la barra de título del sistema con una integrada que coincide con el tema de la app. Desactiva para usar la barra nativa de GNOME/GTK.',
|
||||
discordAppleCovers: 'Obtener portadas de Apple Music para Discord',
|
||||
discordAppleCoversDesc: 'Envía el artista y nombre del álbum a la API de búsqueda de Apple para encontrar portadas para tu perfil de Discord. Desactivado por defecto por privacidad.',
|
||||
discordTemplates: 'Plantillas de texto personalizadas',
|
||||
discordTemplatesDesc: 'Personaliza qué información se muestra en tu perfil de Discord. Variables: {title}, {artist}, {album}',
|
||||
discordTemplateDetails: 'Línea principal (details)',
|
||||
discordTemplateState: 'Línea secundaria (state)',
|
||||
discordTemplateLargeText: 'Tooltip del álbum (largeText)',
|
||||
nowPlayingEnabled: 'Mostrar en Reproduciendo Ahora',
|
||||
nowPlayingEnabledDesc: 'Transmite tu pista actual a la vista de oyentes en vivo del servidor. Desactiva para dejar de enviar datos de reproducción.',
|
||||
lyricsServerFirst: 'Preferir letras del servidor',
|
||||
|
||||
@@ -537,6 +537,11 @@ export const frTranslation = {
|
||||
discordRichPresenceDesc: 'Affiche le titre en cours de lecture sur votre profil Discord. Discord doit être ouvert.',
|
||||
discordAppleCovers: 'Récupérer les pochettes via Apple Music pour Discord',
|
||||
discordAppleCoversDesc: 'Envoie le nom de l\'artiste et de l\'album à l\'API Apple pour trouver une pochette pour votre profil Discord. Désactivé par défaut pour des raisons de confidentialité.',
|
||||
discordTemplates: 'Modèles de texte personnalisés',
|
||||
discordTemplatesDesc: 'Personnalisez les informations affichées sur votre profil Discord. Variables : {title}, {artist}, {album}',
|
||||
discordTemplateDetails: 'Ligne principale (details)',
|
||||
discordTemplateState: 'Ligne secondaire (state)',
|
||||
discordTemplateLargeText: 'Info-bulle album (largeText)',
|
||||
nowPlayingEnabled: 'Afficher dans la fenêtre live',
|
||||
nowPlayingEnabledDesc: 'Diffuse le titre en cours de lecture vers la vue des auditeurs en direct du serveur. Désactiver pour ne pas envoyer de données de lecture.',
|
||||
lyricsServerFirst: 'Préférer les paroles du serveur',
|
||||
|
||||
@@ -536,6 +536,11 @@ export const nbTranslation = {
|
||||
discordRichPresenceDesc: 'Vis sporet som spilles i din Discord-profil. Krever at Discord kjører.',
|
||||
discordAppleCovers: 'Hent covere fra Apple Music til Discord',
|
||||
discordAppleCoversDesc: 'Sender artist- og albumnavn til Apples søke-API for å finne cover til Discord-profilen din. Deaktivert som standard av personvernhensyn.',
|
||||
discordTemplates: 'Egendefinerte tekstmaler',
|
||||
discordTemplatesDesc: 'Tilpass hvilken informasjon som vises på Discord-profilen din. Variabler: {title}, {artist}, {album}',
|
||||
discordTemplateDetails: 'Primær linje (details)',
|
||||
discordTemplateState: 'Sekundær linje (state)',
|
||||
discordTemplateLargeText: 'Album-verktøytips (largeText)',
|
||||
nowPlayingEnabled: 'Vis i "Nå spiller"',
|
||||
nowPlayingEnabledDesc: 'Send sporet som spilles av til tjenerens live-lyttervisning. Deaktiver for å stoppe sending av avspillingsdata.',
|
||||
lyricsServerFirst: 'Foretrekk server-sangtekst',
|
||||
|
||||
@@ -536,6 +536,11 @@ export const nlTranslation = {
|
||||
discordRichPresenceDesc: 'Toont het huidige nummer op je Discord-profiel. Discord moet daarvoor geopend zijn.',
|
||||
discordAppleCovers: 'Hoezen ophalen via Apple Music voor Discord',
|
||||
discordAppleCoversDesc: 'Stuurt artiest- en albumnaam naar de Apple-zoek-API om een hoes te vinden voor je Discord-profiel. Standaard uitgeschakeld vanwege privacy.',
|
||||
discordTemplates: 'Aangepaste tekstsjablonen',
|
||||
discordTemplatesDesc: 'Pas aan welke informatie wordt weergegeven op je Discord-profiel. Variabelen: {title}, {artist}, {album}',
|
||||
discordTemplateDetails: 'Primaire regel (details)',
|
||||
discordTemplateState: 'Secundaire regel (state)',
|
||||
discordTemplateLargeText: 'Album-tooltip (largeText)',
|
||||
nowPlayingEnabled: 'Weergeven in live-venster',
|
||||
nowPlayingEnabledDesc: 'Stuurt het huidige nummer naar de live-luisteraarsweergave van de server. Uitschakelen om geen afspeelgegevens te verzenden.',
|
||||
lyricsServerFirst: 'Server-songtekst voorrang geven',
|
||||
|
||||
@@ -559,6 +559,11 @@ export const ruTranslation = {
|
||||
'Показывать текущий трек в профиле и статусе Discord. Нужен запущенный клиент Discord.',
|
||||
discordAppleCovers: 'Загружать обложки через Apple Music для Discord',
|
||||
discordAppleCoversDesc: 'Отправляет имя исполнителя и альбома в Apple Music API для поиска обложки для профиля Discord. По умолчанию отключено из соображений конфиденциальности.',
|
||||
discordTemplates: 'Пользовательские шаблоны текста',
|
||||
discordTemplatesDesc: 'Настройте, какая информация отображается в профиле Discord. Переменные: {title}, {artist}, {album}',
|
||||
discordTemplateDetails: 'Основная строка (details)',
|
||||
discordTemplateState: 'Вторичная строка (state)',
|
||||
discordTemplateLargeText: 'Подсказка альбома (largeText)',
|
||||
nowPlayingEnabled: 'Показывать в «Сейчас играет»',
|
||||
nowPlayingEnabledDesc:
|
||||
'Отправлять на сервер, что вы сейчас слушаете. Отключите, чтобы не делиться этим.',
|
||||
|
||||
@@ -532,6 +532,11 @@ export const zhTranslation = {
|
||||
discordRichPresenceDesc: '在 Discord 个人资料上显示当前播放的曲目。需要 Discord 处于运行状态。',
|
||||
discordAppleCovers: '通过 Apple Music 为 Discord 获取封面',
|
||||
discordAppleCoversDesc: '将艺术家和专辑名称发送至 Apple 搜索 API,以为 Discord 个人资料查找封面图片。出于隐私考虑,默认禁用。',
|
||||
discordTemplates: '自定义文本模板',
|
||||
discordTemplatesDesc: '自定义在 Discord 个人资料上显示的信息。变量:{title}, {artist}, {album}',
|
||||
discordTemplateDetails: '主行 (details)',
|
||||
discordTemplateState: '副行 (state)',
|
||||
discordTemplateLargeText: '专辑提示 (largeText)',
|
||||
nowPlayingEnabled: '在实时窗口中显示',
|
||||
nowPlayingEnabledDesc: '将当前播放的曲目广播到服务器的实时听众视图。禁用以停止发送播放数据。',
|
||||
lyricsServerFirst: '优先使用服务器歌词',
|
||||
|
||||
@@ -1155,6 +1155,41 @@ export default function Settings() {
|
||||
<span className="toggle-track" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="settings-section-divider" />
|
||||
<div style={{ paddingLeft: 16, paddingTop: 8, paddingBottom: 8 }}>
|
||||
<div style={{ fontWeight: 500, fontSize: 13, marginBottom: 8 }}>{t('settings.discordTemplates')}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: 12 }}>{t('settings.discordTemplatesDesc')}</div>
|
||||
<div className="form-group" style={{ marginBottom: '0.75rem' }}>
|
||||
<label style={{ fontSize: 12 }}>{t('settings.discordTemplateDetails')}</label>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={auth.discordTemplateDetails}
|
||||
onChange={e => auth.setDiscordTemplateDetails(e.target.value)}
|
||||
placeholder="{artist} - {title}"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group" style={{ marginBottom: '0.75rem' }}>
|
||||
<label style={{ fontSize: 12 }}>{t('settings.discordTemplateState')}</label>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={auth.discordTemplateState}
|
||||
onChange={e => auth.setDiscordTemplateState(e.target.value)}
|
||||
placeholder="{album}"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label style={{ fontSize: 12 }}>{t('settings.discordTemplateLargeText')}</label>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
value={auth.discordTemplateLargeText}
|
||||
onChange={e => auth.setDiscordTemplateLargeText(e.target.value)}
|
||||
placeholder="{album}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="settings-section-divider" />
|
||||
|
||||
@@ -61,6 +61,9 @@ interface AuthState {
|
||||
minimizeToTray: boolean;
|
||||
discordRichPresence: boolean;
|
||||
enableAppleMusicCoversDiscord: boolean;
|
||||
discordTemplateDetails: string;
|
||||
discordTemplateState: string;
|
||||
discordTemplateLargeText: string;
|
||||
useCustomTitlebar: boolean;
|
||||
nowPlayingEnabled: boolean;
|
||||
lyricsServerFirst: boolean;
|
||||
@@ -187,6 +190,9 @@ interface AuthState {
|
||||
setMinimizeToTray: (v: boolean) => void;
|
||||
setDiscordRichPresence: (v: boolean) => void;
|
||||
setEnableAppleMusicCoversDiscord: (v: boolean) => void;
|
||||
setDiscordTemplateDetails: (v: string) => void;
|
||||
setDiscordTemplateState: (v: string) => void;
|
||||
setDiscordTemplateLargeText: (v: string) => void;
|
||||
setUseCustomTitlebar: (v: boolean) => void;
|
||||
setNowPlayingEnabled: (v: boolean) => void;
|
||||
setLyricsServerFirst: (v: boolean) => void;
|
||||
@@ -285,6 +291,9 @@ export const useAuthStore = create<AuthState>()(
|
||||
minimizeToTray: false,
|
||||
discordRichPresence: false,
|
||||
enableAppleMusicCoversDiscord: false,
|
||||
discordTemplateDetails: '{artist} - {title}',
|
||||
discordTemplateState: '{album}',
|
||||
discordTemplateLargeText: '{album}',
|
||||
useCustomTitlebar: false,
|
||||
nowPlayingEnabled: false,
|
||||
lyricsServerFirst: true,
|
||||
@@ -407,6 +416,9 @@ export const useAuthStore = create<AuthState>()(
|
||||
setMinimizeToTray: (v) => set({ minimizeToTray: v }),
|
||||
setDiscordRichPresence: (v) => set({ discordRichPresence: v }),
|
||||
setEnableAppleMusicCoversDiscord: (v) => set({ enableAppleMusicCoversDiscord: v }),
|
||||
setDiscordTemplateDetails: (v) => set({ discordTemplateDetails: v }),
|
||||
setDiscordTemplateState: (v) => set({ discordTemplateState: v }),
|
||||
setDiscordTemplateLargeText: (v) => set({ discordTemplateLargeText: v }),
|
||||
setUseCustomTitlebar: (v) => set({ useCustomTitlebar: v }),
|
||||
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
|
||||
setLyricsServerFirst: (v: boolean) => set({ lyricsServerFirst: v }),
|
||||
|
||||
@@ -745,16 +745,28 @@ export function initAudioListeners(): () => void {
|
||||
let discordPrevTrackId: string | null = null;
|
||||
let discordPrevIsPlaying: boolean | null = null;
|
||||
let discordPrevFetchCovers: boolean | null = null;
|
||||
let discordPrevTemplateDetails: string | null = null;
|
||||
let discordPrevTemplateState: string | null = null;
|
||||
let discordPrevTemplateLargeText: string | null = null;
|
||||
|
||||
function syncDiscord() {
|
||||
const { currentTrack, isPlaying, currentTime } = usePlayerStore.getState();
|
||||
const { discordRichPresence, enableAppleMusicCoversDiscord } = useAuthStore.getState();
|
||||
const {
|
||||
discordRichPresence,
|
||||
enableAppleMusicCoversDiscord,
|
||||
discordTemplateDetails,
|
||||
discordTemplateState,
|
||||
discordTemplateLargeText,
|
||||
} = useAuthStore.getState();
|
||||
|
||||
if (!discordRichPresence || !currentTrack) {
|
||||
if (discordPrevTrackId !== null) {
|
||||
discordPrevTrackId = null;
|
||||
discordPrevIsPlaying = null;
|
||||
discordPrevFetchCovers = null;
|
||||
discordPrevTemplateDetails = null;
|
||||
discordPrevTemplateState = null;
|
||||
discordPrevTemplateLargeText = null;
|
||||
invoke('discord_clear_presence').catch(() => {});
|
||||
}
|
||||
return;
|
||||
@@ -763,24 +775,31 @@ export function initAudioListeners(): () => void {
|
||||
const trackChanged = currentTrack.id !== discordPrevTrackId;
|
||||
const playingChanged = isPlaying !== discordPrevIsPlaying;
|
||||
const coversSettingChanged = enableAppleMusicCoversDiscord !== discordPrevFetchCovers;
|
||||
if (!trackChanged && !playingChanged && !coversSettingChanged) return;
|
||||
const detailsTemplateChanged = discordTemplateDetails !== discordPrevTemplateDetails;
|
||||
const stateTemplateChanged = discordTemplateState !== discordPrevTemplateState;
|
||||
const largeTextTemplateChanged = discordTemplateLargeText !== discordPrevTemplateLargeText;
|
||||
if (!trackChanged && !playingChanged && !coversSettingChanged && !detailsTemplateChanged && !stateTemplateChanged && !largeTextTemplateChanged) return;
|
||||
|
||||
discordPrevTrackId = currentTrack.id;
|
||||
discordPrevIsPlaying = isPlaying;
|
||||
discordPrevFetchCovers = enableAppleMusicCoversDiscord;
|
||||
discordPrevTemplateDetails = discordTemplateDetails;
|
||||
discordPrevTemplateState = discordTemplateState;
|
||||
discordPrevTemplateLargeText = discordTemplateLargeText;
|
||||
|
||||
invoke('discord_update_presence', {
|
||||
title: currentTrack.title,
|
||||
artist: currentTrack.artist ?? 'Unknown Artist',
|
||||
album: currentTrack.album ?? null,
|
||||
isPlaying,
|
||||
// Pass elapsed when playing so Discord shows a live running timer.
|
||||
// Pass null when paused — Discord clears the timer.
|
||||
elapsedSecs: isPlaying ? currentTime : null,
|
||||
// coverArtUrl is intentionally not passed — Subsonic URLs require auth.
|
||||
// iTunes cover fetching is only done when explicitly opted in.
|
||||
coverArtUrl: null,
|
||||
fetchItunesCovers: enableAppleMusicCoversDiscord,
|
||||
detailsTemplate: discordTemplateDetails,
|
||||
stateTemplate: discordTemplateState,
|
||||
largeTextTemplate: discordTemplateLargeText,
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user