diff --git a/docsite/docs/configuration/clients/discord.mdx b/docsite/docs/configuration/clients/discord.mdx
index ae4d8fca..017f5366 100644
--- a/docsite/docs/configuration/clients/discord.mdx
+++ b/docsite/docs/configuration/clients/discord.mdx
@@ -27,15 +27,27 @@ Multi-scrobbler does its upmost to implement, to-spec, [Gateway API communicatio
Aside from the on-paper violation of using a user token programmatically (on your behalf), there is no misuse of the API to achieve rich presence.
-Additionally, MS uses the state of non-MS sessions to determine if it should update presence at all. Essentially, it will not act on your behalf unless you are:
+Additionally, MS uses the state of non-MS sessions to conservatively update presence. [By default it will not conflict](#configure-when-presence-is-used) with any other official presence activities and is only used when you are actually active on a device.
-* using an official client
-* physically online
-* not invisible
+
+
+
+
+Every time multi-scrobbler is started a discord session update needs to be triggered so that [MS can capture the signals it uses to determine if presence can be updated.](#configure-when-presence-is-used) This only needs to be *once*, after starting multi-scrobbler. But it does need to be *every time* multi-scrobbler is (re)started. Without these signals MS will not update presence.
+
+This session update can happen automatically after *some* time so you may not need to do anything. If you want to force the update then do one of the following:
+
+* Open discord on a device it was not recently active on
+* Exit discord on an active device
+* Change your status (online, idle, etc...)
+* Set a custom status
+* Cause a presence update with a different app (listening to..., playing..., etc.)
-## User Token
+## Required Setup
+
+### User Token
You must provide a User Token for this scrobbler to work.
@@ -49,11 +61,167 @@ When finding instructions for obtaining this token only run commands, or follow
:::
-## Artwork
+## Optional Setup
+
+### Configure When Presence Is Used
+
+MS uses several signals from your "real" Discord sessions to determine if it should update presence. The default configuration is **extremely** conservative to ensure that MS does not conflict with other apps using presence. Additionally, it only updates if a "real" user device is active on discord.
+
+:::tip[TLDR]
+
+Without any additional configuration, MS will only update presence if:
+
+* you have discord open on a real device
+* your status is either: online, idle, or dnd (not invisible)
+* no other apps are broadcasting presence (no other listening, playing, competing, etc... activities on your profile)
+
+:::
+
+#### Online Status
+
+Configure if MS is allowed to update presence based on your online status. If this setting is not defined MS will only update if your status is **online**, **idle**, or **dnd**.
+
+Allowed Values: `online` `idle` `dnd` `invisible`
+
+
+
+Example
+
+Only allow presence updating when all of your "real" sessions are either online or idle.
+
+
+In File/AIO:
+
+```json
+[
+ {
+ "name": "MS",
+ "enable": true,
+ "data": {
+ "token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
+ "applicationId": "8190211179716453570",
+ "statusOverrideAllow": ["online", "idle"]
+ }
+ }
+ ]
+```
+
+In ENV:
+
+```
+DISCORD_STATUS_OVERRIDE_ALLOW=online,idle
+```
+
+
+
+#### Activity Type
+
+Configure the activity types of **other** activities that MS is allowed to broadcast at the same time as. If this setting is not defined MS will only update if no other activities or only `custom`.
+
+Note: Discord shows both `custom` and other non `custom` activities at the same time. All other activities are mutually exclusive.
+
+Allowed Values:
+
+* `playing` `streaming` `listening` `custom` `competing`
+* `true` => allow presence during *any*
+* `false` => allow presence during *none*
+
+
+
+Example
+
+You want to allow MS to update presence when you are normally playing any game but not when any other activity (streaming, listening, competing) is happening.
+
+In File/AIO:
+
+```json
+[
+ {
+ "name": "MS",
+ "enable": true,
+ "data": {
+ "token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
+ "applicationId": "8190211179716453570",
+ "activitiesOverrideAllow": ["playing", "custom"]
+ }
+ }
+ ]
+```
+
+In ENV:
+
+```
+DISCORD_ACTIVITIES_OVERRIDE_ALLOW=playing,custom
+```
+
+___
+
+You **do not** want to allow MS to update presence if **any** other activity is occurring, including `custom`.
+
+```json
+[
+ {
+ "name": "MS",
+ "enable": true,
+ "data": {
+ "token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
+ "applicationId": "8190211179716453570",
+ "activitiesOverrideAllow": false
+ }
+ }
+ ]
+```
+
+In ENV:
+
+```
+DISCORD_ACTIVITIES_OVERRIDE_ALLOW=false
+```
+
+
+
+#### Activity Name
+
+Configure the names of **other** activities that MS is **not** allowed to broadcast at the same time as.
+
+It is not required to use [Activity Type](#activity-type) with this setting, but it is useful.
+
+
+
+Example
+
+You want to allow MS to update presence when you are normally playing any game but not when any other activity (streaming, listening, competing) is happening. You also do not want to allow MS to update presence if the game is fortnite.
+
+In File/AIO:
+
+```json
+[
+ {
+ "name": "MS",
+ "enable": true,
+ "data": {
+ "token": "Ma7XBvzRERK3gQdkX4dkb3OQ.6aaGmJ.rvFiiVrzG4VXHHL3LyhZYhpWdG3Ph_if9aKz5E",
+ "applicationId": "8190211179716453570",
+ "activitiesOverrideAllow": ["playing", "custom"],
+ "applicationsOverrideDisallow": ["fortnite"]
+ }
+ }
+ ]
+```
+
+In ENV:
+
+```
+DISCORD_ACTIVITIES_OVERRIDE_ALLOW=playing,custom
+DISCORD_APPNAME_OVERRIDE_DISALLOW=fortnite
+```
+
+
+### Artwork
For some Sources, MS parses album art and displays it in the dashboard. If a Discord **Application Id** is provided then this image can be displayed on Discord alongside your listening status.
-If no Application Id is provided Discord will always show a default image next to your status.
+**If no Application Id is provided Discord will use its own default image next to your status.**
@@ -80,7 +248,7 @@ There are some scenarios where you may not want to use your artwork URLs, for ex
MS is conservative in this context by **disallowing** use of album art URLs **by default.** Additionally, it **always** disallows use of these URLs if they do not start with `https`.
-* To allow **any** `https` album art URLs set ENV `DISCORD_ARTWORK=true`
+* To allow **any** `https` album art URLs set ENV `DISCORD_ARTWORK=true` (or in file as `"artwork": true`)
* To allow only `https` album art URLs from certain domains use a list of keywords like `DISCORD_ARTWORK=spotify,jellyfin` (or in file as `"artwork": ["spotify","jellyfin"]`)
If a listening activity does not have album art, or the URL is disallowed, MS will use the [multi-scrobbler logo](https://github.com/FoxxMD/multi-scrobbler/blob/master/assets/icon.png) as a fallback image.
@@ -92,10 +260,13 @@ You can customize this fallback image with ENV `DISCORD_ARTWORK_DEFAULT_URL` or
## Configuration
- | Environmental Variable | Required? | Default | Description |
- | :---------------------------- | --------- | ------- | :--------------------------------------------------------------------------------------------------------------- |
- | `DISCORD_TOKEN` | Yes | | User Token acquired from an active Discord sessions |
- | `DISCORD_APPLICATION_ID` | Yes | | Application ID for user with album art |
- | `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
- | `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
+ | Environmental Variable | Required? | Default | Description |
+ | :---------------------------------- | --------- | ----------------- | :--------------------------------------------------------------------------------------------------------------- |
+ | `DISCORD_TOKEN` | Yes | | User Token acquired from an active Discord sessions |
+ | `DISCORD_APPLICATION_ID` | No | | Application ID used to display album art |
+ | `DISCORD_ARTWORK` | No | | A boolean indicating if external artwork URLs should be used. Or a comma-separated list of allowed domains |
+ | `DISCORD_ARTWORK_DEFAULT_URL` | No | | A URL of an image to use as a fallback if the album art URL cannot be used. Or `false` to use discord's default. |
+ | `DISCORD_STATUS_OVERRIDE_ALLOW` | No | `online,idle,dnd` | A comma-separated list of statuses are allowed to have presence. |
+ | `DISCORD_ACTIVITIES_OVERRIDE_ALLOW` | No | `custom` | A comma-seperated list of *other* activity types MS can broadcast presence at the same time as. |
+ | `DISCORD_APPNAME_OVERRIDE_DISALLOW` | No | | A commera-seperated list of activity names MS is *not* allowed to broadcast presence at the same time as. |
\ No newline at end of file
diff --git a/src/backend/common/infrastructure/config/client/discord.ts b/src/backend/common/infrastructure/config/client/discord.ts
index 8191a697..82aef02e 100644
--- a/src/backend/common/infrastructure/config/client/discord.ts
+++ b/src/backend/common/infrastructure/config/client/discord.ts
@@ -1,4 +1,3 @@
-import { RequestRetryOptions } from "../common.js"
import { CommonClientConfig, CommonClientData } from "./index.js"
export interface DiscordData {
@@ -6,6 +5,9 @@ export interface DiscordData {
applicationId?: string
artwork?: boolean | string | string[]
artworkDefaultUrl?: string | false
+ statusOverrideAllow?: string | StatusType[]
+ activitiesOverrideAllow?: boolean | string | ActivityType[]
+ applicationsOverrideDisallow?: string | string[]
}
export interface DiscordClientData extends DiscordData, CommonClientData {}
@@ -23,4 +25,15 @@ export interface DiscordClientConfig extends CommonClientConfig {
export interface DiscordClientAIOConfig extends DiscordClientConfig {
type: 'discord'
+}
+
+export type ActivityType = 'playing' | 'streaming' | 'listening' | 'watching' | 'custom' | 'competing';
+export const ActivityTypes: ActivityType[] = ['playing','streaming','listening','watching','custom','competing'];
+export type StatusType = 'online' | 'idle' | 'dnd' | 'invisible';
+
+export interface DiscordStrongData extends DiscordData {
+ artwork?: boolean | string[]
+ statusOverrideAllow?: StatusType[]
+ activitiesOverrideAllow?: ActivityType[]
+ applicationsOverrideDisallow?: string[]
}
\ No newline at end of file
diff --git a/src/backend/common/vendor/discord/DiscordWSClient.ts b/src/backend/common/vendor/discord/DiscordWSClient.ts
index aea0d3e9..8b54ebd5 100644
--- a/src/backend/common/vendor/discord/DiscordWSClient.ts
+++ b/src/backend/common/vendor/discord/DiscordWSClient.ts
@@ -1,7 +1,7 @@
import { childLogger } from "@foxxmd/logging";
import { WS } from 'iso-websocket'
-import { DiscordClientData } from "../../infrastructure/config/client/discord.js";
-import { _DataPayload, _NonDispatchPayload, APIUser, GatewayActivity, GatewayActivityUpdateData, GatewayCloseCodes, GatewayDispatchEvents, GatewayHeartbeatRequest, GatewayHelloData, GatewayIdentify, GatewayIdentifyData, GatewayInvalidSessionData, GatewayOpcodes, GatewayPresenceUpdateData, GatewayReadyDispatchData, GatewayResumeData, GatewayUpdatePresence, PresenceUpdateStatus } from "discord.js";
+import { DiscordClientData, DiscordData, DiscordStrongData, StatusType, ActivityType as MSActivityType, ActivityTypes } from "../../infrastructure/config/client/discord.js";
+import { _DataPayload, _NonDispatchPayload, ActivityType, APIUser, GatewayActivity, GatewayActivityUpdateData, GatewayCloseCodes, GatewayDispatchEvents, GatewayHeartbeatRequest, GatewayHelloData, GatewayIdentify, GatewayIdentifyData, GatewayInvalidSessionData, GatewayOpcodes, GatewayPresenceUpdateData, GatewayReadyDispatchData, GatewayResumeData, GatewayUpdatePresence, PresenceUpdateStatus } from "discord.js";
import { isDebugMode, parseBool, removeUndefinedKeys, sleep } from "../../../utils.js";
import pEvent from 'p-event';
import EventEmitter from "events";
@@ -12,7 +12,7 @@ import { AbstractApiOptions, asPlayerStateData, SourceData } from "../../infrast
import { isPlayObject, PlayObject } from "../../../../core/Atomic.js";
import dayjs from "dayjs";
import { capitalize } from "../../../../core/StringUtils.js";
-import { parseArrayFromMaybeString } from "../../../utils/StringUtils.js";
+import { parseArrayFromMaybeString, parseBoolOrArrayFromMaybeString } from "../../../utils/StringUtils.js";
import { getRoot } from "../../../ioc.js";
import { MSCache } from "../../Cache.js";
import { isSuperAgentResponseError } from "../../errors/ErrorUtils.js";
@@ -33,7 +33,7 @@ const API_GATEWAY_ENDPOINT = 'https://discord.com/api/gateway';
*/
export class DiscordWSClient extends AbstractApiClient {
- declare config: DiscordClientData;
+ declare config: DiscordStrongData;
heartbeatInterval: NodeJS.Timeout
acknowledged: boolean = true;
@@ -56,33 +56,20 @@ export class DiscordWSClient extends AbstractApiClient {
closeEvents: number = 0;
lastActiveStatus?: PresenceUpdateStatus = PresenceUpdateStatus.Offline;
+ lastActivities: GatewayActivity[] = [];
activityTimeout: NodeJS.Timeout;
- artworkOpt: boolean | string[] = false;
-
emitter: EventEmitter;
cache: MSCache;
artFail: boolean = false;
artFailCount = 0;
- constructor(name: any, config: DiscordClientData, options: AbstractApiOptions) {
+ constructor(name: any, config: DiscordStrongData, options: AbstractApiOptions) {
super('Discord', name, config, options);
this.logger = childLogger(options.logger, 'WS Gateway');
this.emitter = new EventEmitter();
- if (typeof this.config.artwork === 'boolean' || Array.isArray(this.config.artwork)) {
- this.artworkOpt = this.config.artwork;
- } else if (typeof this.config.artwork === 'string') {
- if (['true', 'false'].includes(this.config.artwork.toLocaleLowerCase())) {
- this.artworkOpt = parseBool(this.config.artwork)
- } else {
- this.artworkOpt = parseArrayFromMaybeString(this.config.artwork)
- }
- }
- if(this.config.artworkDefaultUrl !== undefined && typeof this.config.artworkDefaultUrl === 'string' && this.config.artworkDefaultUrl.toLocaleLowerCase().trim() === 'false') {
- this.config.artworkDefaultUrl = false;
- }
this.cache = getRoot().items.cache();
}
@@ -302,16 +289,34 @@ export class DiscordWSClient extends AbstractApiClient {
this.sequence = undefined;
this.resume_gateway_url = undefined;
this.user = undefined;
+ this.lastActiveStatus = PresenceUpdateStatus.Offline;
+ this.lastActivities = [];
}
}
handleUserSessionUpdates = (data: UserSession[]) => {
this.logger.debug('Recieved updated user sessions');
- const otherSessions = data.filter(x => x.session_id !== this.session_id);
- if (otherSessions.length === 0) {
+ if (data.filter(x => x.session_id !== this.session_id && x.session_id !== 'all').length === 0) {
this.logger.debug('No other user sessions exist, marking our session presence as inactive');
this.lastActiveStatus = PresenceUpdateStatus.Offline;
+ this.lastActivities = [];
+ return;
}
+ const otherSessions = data.filter(x => x.session_id !== this.session_id);
+ const sessionSummaries = otherSessions.map(x => {
+ let sessionId = `${x.session_id === 'all' ? '(All) | ' : ''}OS ${x.client_info.os} | Client ${x.client_info.client} | Status ${x.status} | Active ${x.active === true}`;
+ if(x.activities.length === 0) {
+ sessionId += " | 0 Activities"
+ } else {
+ const activitySummary = x.activities.map(x => x.type === 4 ? 'Custom Status' : `${activityIdToStr(x.type)} ${x.name}`).join(', ');
+ sessionId += ` | Activities => ${activitySummary}`;
+ };
+ return sessionId;
+ });
+ this.logger.debug(sessionSummaries.join('\n'));
+
+ const last = this.lastActiveStatus;
+
if (otherSessions.some(x => x.status === 'online')) {
this.lastActiveStatus = PresenceUpdateStatus.Online;
} else if (otherSessions.some(x => x.status === 'dnd')) {
@@ -324,6 +329,20 @@ export class DiscordWSClient extends AbstractApiClient {
this.lastActiveStatus = PresenceUpdateStatus.Offline;
}
this.logger.debug(`Best status found: ${this.lastActiveStatus}`);
+
+ this.lastActivities = otherSessions.filter(x => x.session_id !== 'all').map(x => x.activities).flat(1);
+
+ const [allowed, reason] = this.presenceIsAllowed();
+ if(!allowed) {
+ // if updated sessions now disallow updating presence
+ // and we have a current presence in our session
+ // then we need to remove it so it doesn't override anything
+ const ourSession = data.find(x => x.session_id === this.session_id);
+ if(ourSession !== undefined && ourSession.activities.length > 0) {
+ this.logger.debug(`Clearing our session presence, MS presence no longer allowed because ${reason}`);
+ this.clearActivity();
+ }
+ }
}
async handleMessage(message: _DataPayload | _NonDispatchPayload) {
@@ -367,7 +386,7 @@ export class DiscordWSClient extends AbstractApiClient {
case 'SESSIONS_REPLACE':
if (isDebugMode()) {
// @ts-expect-error
- this.logger.debug({ data: message.d }, t);
+ this.logger.debug(`${t} => ${JSON.stringify(message.d)}`);
}
// @ts-expect-error
this.handleUserSessionUpdates(message.d as UserSession[]);
@@ -406,7 +425,9 @@ export class DiscordWSClient extends AbstractApiClient {
playStateToActivity = async (data: SourceData): Promise => {
const { activity, artUrl } = playStateToActivityData(data);
- const artwork = this.artworkOpt;
+ const {
+ artwork = false
+ } = this.config;
const {
artworkDefaultUrl = ARTWORK_PLACEHOLDER
} = this.config;
@@ -552,7 +573,43 @@ export class DiscordWSClient extends AbstractApiClient {
}
return;
}
+ }
+
+ presenceIsAllowedByStatus = (status?: PresenceUpdateStatus | StatusType): [boolean, string?] => {
+ if (!this.config.statusOverrideAllow.includes(status as StatusType ?? this.lastActiveStatus as StatusType)) {
+ return [false, `most active session has a disallowed status: ${status ?? this.lastActiveStatus}`];
+ }
+ return [true];
+ }
+
+ presenceIsAllowedByActivity = (manualActivities?: GatewayActivity[]): [boolean, string?] => {
+ const activities = manualActivities ?? this.lastActivities;
+ if (activities.length !== 0) {
+ const disallowedActivityType = activities.find(x => !this.config.activitiesOverrideAllow.includes(activityIdToStr(x.type)));
+ if (disallowedActivityType !== undefined) {
+ return [false, `a session has an activity type MS is not allowed to override: ${activityIdToStr(disallowedActivityType.type)}`];
+ }
+ const disallowedActivityName = activities.find(x => !this.config.applicationsOverrideDisallow.some(y => x.name.toLocaleLowerCase().includes(y.toLocaleLowerCase())));
+ if (disallowedActivityType !== undefined) {
+ return [false, `a session has an activity name MS is not allowed to override: ${disallowedActivityName.name}`];
+ }
+ }
+
+ return [true];
+ }
+
+ presenceIsAllowed = (): [boolean, string?] => {
+ const [statusAllowed, statusReason] = this.presenceIsAllowedByStatus();
+ if(!statusAllowed) {
+ return [statusAllowed, statusReason];
+ }
+
+ const [activityAllowed, activityReason] = this.presenceIsAllowedByActivity();
+ if(!activityAllowed) {
+ return [activityAllowed, activityReason];
+ }
+ return [true];
}
}
@@ -583,9 +640,21 @@ const opcodeToFriendly = (op: number) => {
interface UserSession {
status: 'online' | 'invisible' | 'dnd' | 'idle'
+ client_info: {
+ version: number
+ os: string
+ client: string
+ }
+ processed_at_timestamp?: number
active?: boolean
session_id: string
- activities: []
+ // activities: {
+ // state: string
+ // created_at: number
+ // type: ActivityType
+ // name: string
+ // }[]
+ activities: GatewayActivity[]
}
export const playStateToActivityData = (data: SourceData, opts: { useArt?: boolean } = {}): { activity: GatewayActivity, artUrl?: string } => {
@@ -638,4 +707,103 @@ export const playStateToActivityData = (data: SourceData, opts: { useArt?: boole
const artUrl = play.meta?.art?.album ?? play.meta.art.track ?? play.meta.art.artist;
return { activity, artUrl };
+}
+
+export const statusStringToType = (str: string): StatusType => {
+ switch(str.trim().toLocaleLowerCase()) {
+ case 'online':
+ return PresenceUpdateStatus.Online;
+ case 'idle':
+ return PresenceUpdateStatus.Idle;
+ case 'dnd':
+ return PresenceUpdateStatus.DoNotDisturb;
+ case 'invisible':
+ return PresenceUpdateStatus.Invisible;
+ default:
+ throw new Error(`Not a valid status type. Must be one of: online | idle | dnd | invisible`);
+ }
+}
+
+export const activityStringToType = (str: string): MSActivityType => {
+ switch(str.trim().toLocaleLowerCase()) {
+ case 'playing':
+ return 'playing';
+ case 'streaming':
+ return 'streaming';
+ case 'listening':
+ return 'listening';
+ case 'watching':
+ return 'watching';
+ case 'custom':
+ return 'custom';
+ case 'competing':
+ return 'competing';
+ default:
+ throw new Error(`Not a valid activity type. Must be one of: playing | streaming | listening | watching | custom | competing`);
+ }
+}
+
+export const activityIdToStr = (id: number): MSActivityType => {
+ switch(id) {
+ case 0:
+ return 'playing';
+ case 1:
+ return 'streaming';
+ case 2:
+ return 'listening';
+ case 3:
+ return 'watching';
+ case 4:
+ return 'custom';
+ case 5:
+ return 'competing';
+ default:
+ throw new Error(`Not a valid activity type. Must be one of: playing | streaming | listening | watching | custom | competing`);
+ }
+}
+
+export const configToStrong = (data: DiscordData): DiscordStrongData => {
+ const {
+ token,
+ applicationId,
+ artwork,
+ artworkDefaultUrl,
+ statusOverrideAllow = ['online','idle','dnd'],
+ activitiesOverrideAllow = ['custom'],
+ applicationsOverrideDisallow = []
+ } = data;
+
+ const strongConfig: DiscordStrongData = {
+ token,
+ applicationId,
+ applicationsOverrideDisallow: parseArrayFromMaybeString(applicationsOverrideDisallow)
+ }
+
+ if (typeof artwork === 'boolean' || Array.isArray(artwork)) {
+ strongConfig.artwork = artwork;
+ } else if (typeof artwork === 'string') {
+ if (['true', 'false'].includes(artwork.toLocaleLowerCase())) {
+ strongConfig.artwork = parseBool(artwork)
+ } else {
+ strongConfig.artwork = parseArrayFromMaybeString(artwork)
+ }
+ }
+
+ if(artworkDefaultUrl !== undefined && typeof artworkDefaultUrl === 'string' && artworkDefaultUrl.toLocaleLowerCase().trim() === 'false') {
+ strongConfig.artworkDefaultUrl = false;
+ } else {
+ strongConfig.artworkDefaultUrl = artworkDefaultUrl;
+ }
+
+ const saRaw = parseArrayFromMaybeString(statusOverrideAllow);
+ strongConfig.statusOverrideAllow = saRaw.map(statusStringToType);
+
+ const aaRaw = parseBoolOrArrayFromMaybeString(activitiesOverrideAllow);
+ if(typeof aaRaw === 'boolean') {
+ strongConfig.activitiesOverrideAllow = aaRaw ? ActivityTypes : [];
+ } else {
+ strongConfig.activitiesOverrideAllow = aaRaw.map(activityStringToType);
+ }
+
+ return strongConfig;
}
\ No newline at end of file
diff --git a/src/backend/scrobblers/DiscordScrobbler.ts b/src/backend/scrobblers/DiscordScrobbler.ts
index 48d46c27..9f65aef3 100644
--- a/src/backend/scrobblers/DiscordScrobbler.ts
+++ b/src/backend/scrobblers/DiscordScrobbler.ts
@@ -1,13 +1,12 @@
import { Logger } from "@foxxmd/logging";
import EventEmitter from "events";
import { PlayObject, SourcePlayerObj } from "../../core/Atomic.js";
-import { CALCULATED_PLAYER_STATUSES, CalculatedPlayerStatus, FormatPlayObjectOptions, REPORTED_PLAYER_STATUSES, ReportedPlayerStatus } from "../common/infrastructure/Atomic.js";
+import { CALCULATED_PLAYER_STATUSES, FormatPlayObjectOptions, REPORTED_PLAYER_STATUSES, ReportedPlayerStatus } from "../common/infrastructure/Atomic.js";
import { Notifiers } from "../notifier/Notifiers.js";
import AbstractScrobbleClient, { nowPlayingUpdateByPlayDuration } from "./AbstractScrobbleClient.js";
-import { DiscordClientConfig } from "../common/infrastructure/config/client/discord.js";
-import { DiscordWSClient, playStateToActivityData } from "../common/vendor/discord/DiscordWSClient.js";
-import { PresenceUpdateStatus } from "discord.js";
+import { DiscordClientConfig, DiscordStrongData, StatusType } from "../common/infrastructure/config/client/discord.js";
+import { configToStrong, DiscordWSClient, playStateToActivityData } from "../common/vendor/discord/DiscordWSClient.js";
export default class DiscordScrobbler extends AbstractScrobbleClient {
@@ -15,11 +14,12 @@ export default class DiscordScrobbler extends AbstractScrobbleClient {
requiresAuth = true;
requiresAuthInteraction = false;
- declare config: DiscordClientConfig;
+ declare config: DiscordClientConfig & {data: DiscordStrongData };
constructor(name: any, config: DiscordClientConfig, options = {}, notifier: Notifiers, emitter: EventEmitter, logger: Logger) {
- super('discord', name, config, notifier, emitter, logger);
- this.api = new DiscordWSClient(name, { ...config.data, ...config.options }, { logger: this.logger });
+ const strong = configToStrong(config.data);
+ super('discord', name, {...config, data: strong}, notifier, emitter, logger);
+ this.api = new DiscordWSClient(name, { ...strong, ...config.options }, { logger: this.logger });
this.api.emitter.on('stopped', async (e) => {
if(e.authFailure) {
this.authFailure = true;
@@ -43,6 +43,15 @@ export default class DiscordScrobbler extends AbstractScrobbleClient {
if (token === undefined) {
throw new Error('Must provide a user token');
}
+ if(typeof this.config.data.artwork === 'boolean') {
+ this.logger.verbose(`Artwork: ${this.config.data.artwork ? 'Allow any with HTTPS' : 'Allow none'}`);
+ } else {
+ this.logger.verbose(`Artwork: Allow HTTPS with these domains: ${this.config.data.artwork.join(', ')}`);
+ }
+ this.logger.verbose(`Artwork Fallback Url: ${this.config.data.artworkDefaultUrl}`);
+ this.logger.verbose(`Allow override statuses: ${this.config.data.statusOverrideAllow.join(', ')}`);
+ this.logger.verbose(`Allow override activity types: ${this.config.data.activitiesOverrideAllow.join(', ')}`);
+ this.logger.verbose(`Disallow override activity names: ${this.config.data.applicationsOverrideDisallow.join(', ')}`);
await this.api.initClient();
return true;
}
@@ -83,18 +92,23 @@ export default class DiscordScrobbler extends AbstractScrobbleClient {
}
shouldUpdatePlayingNowPlatformSpecific = async (data: SourcePlayerObj) => {
- if(data.status.reported === REPORTED_PLAYER_STATUSES.playing
- || [CALCULATED_PLAYER_STATUSES.stopped, CALCULATED_PLAYER_STATUSES.paused].includes(data.status.calculated as ReportedPlayerStatus)
- || data.status.stale)
- if ([PresenceUpdateStatus.Offline, PresenceUpdateStatus.Invisible].includes(this.api.lastActiveStatus)) {
- this.logger.debug('Not updating presence because no user sessions have a visible status');
- return false;
- }
- const [sendOk, reasons] = this.api.checkOkToSend();
- if (!sendOk) {
- this.logger.warn(`Cannot update playing now because api client is ${reasons}`);
- return false;
+ if ([CALCULATED_PLAYER_STATUSES.stopped, CALCULATED_PLAYER_STATUSES.paused, CALCULATED_PLAYER_STATUSES.playing].includes(data.status.calculated as ReportedPlayerStatus)
+ || data.status.stale) {
+
+ const [sendOk, reasons] = this.api.checkOkToSend();
+ if (!sendOk) {
+ this.logger.warn(`Cannot update playing now because api client is ${reasons}`);
+ return false;
+ }
+
+ const [allowed, reason] = this.api.presenceIsAllowed();
+ if(!allowed) {
+ this.logger.debug(reason);
+ }
+
+ return true;
+
}
- return true;
+ return false;
}
}