Skip to content

Commit

Permalink
Merge pull request #216 from FoxxMD/GH-215/notifiersRelax
Browse files Browse the repository at this point in the history
feat(notifications): Make init more granular and always complete init
  • Loading branch information
FoxxMD authored Dec 10, 2024
2 parents feb02bf + 65012d4 commit 83ff534
Show file tree
Hide file tree
Showing 17 changed files with 237 additions and 106 deletions.
3 changes: 2 additions & 1 deletion src/backend/common/vendor/LastfmApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import LastFm, {
} from "lastfm-node-client";
import { PlayObject } from "../../../core/Atomic.js";
import { nonEmptyStringOrDefault, splitByFirstFound } from "../../../core/StringUtils.js";
import { joinedUrl, readJson, removeUndefinedKeys, sleep, writeFile } from "../../utils.js";
import { readJson, removeUndefinedKeys, sleep, writeFile } from "../../utils.js";
import { joinedUrl } from "../../utils/NetworkUtils.js";
import { getScrobbleTsSOCDate } from "../../utils/TimeUtils.js";
import { getNodeNetworkException, isNodeNetworkException } from "../errors/NodeErrors.js";
import { UpstreamError } from "../errors/UpstreamError.js";
Expand Down
3 changes: 2 additions & 1 deletion src/backend/ioc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import { WildcardEmitter } from "./common/WildcardEmitter.js";
import { Notifiers } from "./notifier/Notifiers.js";
import ScrobbleClients from "./scrobblers/ScrobbleClients.js";
import ScrobbleSources from "./sources/ScrobbleSources.js";
import { generateBaseURL } from "./utils.js";

import { generateBaseURL } from "./utils/NetworkUtils.js";
import { PassThrough } from "stream";

let version: string = 'unknown';
Expand Down
26 changes: 17 additions & 9 deletions src/backend/notifier/AppriseWebhookNotifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
WebhookPayload
} from "../common/infrastructure/config/health/webhooks.js";
import { AbstractWebhookNotifier } from "./AbstractWebhookNotifier.js";
import { URLData } from "../../core/Atomic.js";
import { isPortReachable, joinedUrl, normalizeWebAddress } from "../utils/NetworkUtils.js";

const shortKey = truncateStringToLength(10);

Expand All @@ -20,6 +22,8 @@ export class AppriseWebhookNotifier extends AbstractWebhookNotifier {

priorities: PrioritiesConfig;

protected endpoint: URLData;

urls: string[];
keys: string[];

Expand All @@ -42,18 +46,24 @@ export class AppriseWebhookNotifier extends AbstractWebhookNotifier {
}

initialize = async () => {
// check url is correct
// check url is correct as a courtesy
this.endpoint = normalizeWebAddress(this.config.host);
this.logger.verbose(`Config URL: '${this.config.host}' => Normalized: '${this.endpoint.normal}'`)

this.initialized = true; // always set as ready to go. Server issues may be transient.

try {
await request.get(this.config.host);
await isPortReachable(this.endpoint.port, { host: this.endpoint.url.hostname });
} catch (e) {
this.logger.error(new Error('Failed to contact Apprise server', {cause: e}));
this.logger.warn(new Error('Unable to detect if server is reachable', { cause: e }));
return;
}

if (this.keys.length > 0) {
let anyOk = false;
for (const key of this.keys) {
try {
const resp = await request.get(`${this.config.host}/json/urls/${key}`);
const resp = await request.get(joinedUrl(this.endpoint.url, `/json/urls/${key}`).toString());
if (resp.statusCode === 204) {
this.logger.warn(`Details for Config ${shortKey(key)} returned no content. Double check the key is set correctly or that the apprise Config is not empty.`);
} else {
Expand All @@ -64,9 +74,7 @@ export class AppriseWebhookNotifier extends AbstractWebhookNotifier {
}
}
if (!anyOk) {
this.logger.error('No Apprise Configs were valid!');
this.initialized = false;
return;
this.logger.warn('No Apprise Configs were valid!');
}
}
this.initialized = true;
Expand All @@ -83,7 +91,7 @@ export class AppriseWebhookNotifier extends AbstractWebhookNotifier {
if (this.keys.length > 0) {
for (const key of this.keys) {
try {
const resp = await this.callApi(request.post(`${this.config.host}/notify/${key}`)
const resp = await this.callApi(request.post(joinedUrl(this.endpoint.url, `/notify/${key}`).toString())
.type('json')
.send(body));
anyOk = true;
Expand All @@ -99,7 +107,7 @@ export class AppriseWebhookNotifier extends AbstractWebhookNotifier {
body.urls = this.urls.join(',')
}
try {
const resp = await this.callApi(request.post(`${this.config.host}/notify`)
const resp = await this.callApi(request.post(joinedUrl(this.endpoint.url, '/notify').toString())
.type('json')
.send(body));
anyOk = true;
Expand Down
31 changes: 23 additions & 8 deletions src/backend/notifier/GotifyWebhookNotifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ import { gotify } from 'gotify';
import request from 'superagent';
import { GotifyConfig, PrioritiesConfig, WebhookPayload } from "../common/infrastructure/config/health/webhooks.js";
import { AbstractWebhookNotifier } from "./AbstractWebhookNotifier.js";
import { isPortReachable, normalizeWebAddress } from "../utils/NetworkUtils.js";
import { URLData } from "../../core/Atomic.js";

export class GotifyWebhookNotifier extends AbstractWebhookNotifier {

declare config: GotifyConfig;

priorities: PrioritiesConfig;

protected endpoint: URLData;

constructor(defaultName: string, config: GotifyConfig, logger: Logger) {
super('Gotify', defaultName, config, logger);
this.requiresAuth = true;
Expand All @@ -29,14 +33,25 @@ export class GotifyWebhookNotifier extends AbstractWebhookNotifier {
}

initialize = async () => {
// check url is correct
// check url is correct as a courtesy
this.endpoint = normalizeWebAddress(this.config.url);
this.logger.verbose(`Config URL: '${this.config.url}' => Normalized: '${this.endpoint.normal}'`)

this.initialized = true; // always set as ready to go. Server issues may be transient.

try {
await isPortReachable(this.endpoint.port, { host: this.endpoint.url.hostname });
} catch (e) {
this.logger.warn(new Error('Unable to detect if server is reachable', { cause: e }));
return;
}

try {
const url = this.config.url;
const resp = await request.get(`${url}/version`);
this.logger.verbose(`Initialized. Found Server version ${resp.body.version}`);
this.initialized = true;
this.logger.verbose(`Found Server version ${resp.body.version}`);
} catch (e) {
this.logger.error(`Failed to contact server | Error: ${e.message}`);
this.logger.warn(new Error('Server was reachable but could not determine version', { cause: e }));
}
}

Expand All @@ -48,19 +63,19 @@ export class GotifyWebhookNotifier extends AbstractWebhookNotifier {
doNotify = async (payload: WebhookPayload) => {
try {
await gotify({
server: this.config.url,
server: this.endpoint.normal,
app: this.config.token,
message: payload.message,
title: payload.title,
priority: this.priorities[payload.priority]
});
this.logger.verbose(`Pushed notification.`);
} catch (e: any) {
} catch (e) {
if(e instanceof HTTPError && e.response.statusCode === 401) {
this.logger.warn(`Unable to push notification. Error returned with 401 which means the TOKEN provided is probably incorrect. Disabling Notifier | Error => ${e.response.body}`);
this.logger.warn(new Error(`Unable to push notification. Error returned with 401 which means the TOKEN provided is probably incorrect. Disabling Notifier \n Response Error => ${e.response.body}`, {cause: e}));
this.authed = false;
} else {
this.logger.warn(`Failed to push notification | Error => ${e.message}`);
this.logger.warn(new Error('Failed to push notification', {cause: e}));
}
}
}
Expand Down
29 changes: 22 additions & 7 deletions src/backend/notifier/NtfyWebhookNotifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { Config, publish } from 'ntfy';
import request from "superagent";
import { NtfyConfig, PrioritiesConfig, WebhookPayload } from "../common/infrastructure/config/health/webhooks.js";
import { AbstractWebhookNotifier } from "./AbstractWebhookNotifier.js";
import { URLData } from "../../core/Atomic.js";
import { isPortReachable, normalizeWebAddress } from "../utils/NetworkUtils.js";

export class NtfyWebhookNotifier extends AbstractWebhookNotifier {

declare config: NtfyConfig;

priorities: PrioritiesConfig;

protected endpoint: URLData;

constructor(defaultName: string, config: NtfyConfig, logger: Logger) {
super('Ntfy', defaultName, config, logger);
const {
Expand All @@ -27,24 +31,35 @@ export class NtfyWebhookNotifier extends AbstractWebhookNotifier {
}

initialize = async () => {
// check url is correct
// check url is correct as a courtesy
this.endpoint = normalizeWebAddress(this.config.url);
this.logger.verbose(`Config URL: '${this.config.url}' => Normalized: '${this.endpoint.normal}'`)

this.initialized = true; // always set as ready to go. Server issues may be transient.

try {
await isPortReachable(this.endpoint.port, { host: this.endpoint.url.hostname });
} catch (e) {
this.logger.warn(new Error('Unable to detect if server is reachable', { cause: e }));
return;
}

try {
const url = this.config.url;
const resp = await request.get(`${url}/v1/health`);
if(resp.body !== undefined && typeof resp.body === 'object') {
const {health} = resp.body;
if(health === false) {
this.logger.error('Found Ntfy server but it responded that it was not ready.')
this.logger.warn('Found server but it responded that it was not ready.')
return;
}
} else {
this.logger.error(`Found Ntfy server but expected a response with 'health' in payload. Found => ${resp.body}`);
this.logger.warn(`Found server but expected a response with 'health' in payload. Found => ${resp.body}`);
return;
}
this.logger.info('Initialized. Found Ntfy server');
this.initialized = true;
this.logger.info('Found Ntfy server');
} catch (e) {
this.logger.error(`Failed to contact Ntfy server | Error: ${e.message}`);
this.logger.error(new Error('Failed to contact server', {cause: e}));
}
}

Expand All @@ -54,7 +69,7 @@ export class NtfyWebhookNotifier extends AbstractWebhookNotifier {
message: payload.message,
topic: this.config.topic,
title: payload.title,
server: this.config.url,
server: this.endpoint.normal,
priority: this.priorities[payload.priority],
};
if (this.config.username !== undefined) {
Expand Down
3 changes: 2 additions & 1 deletion src/backend/scrobblers/ScrobbleClients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { ListenBrainzClientConfig } from "../common/infrastructure/config/client
import { MalojaClientConfig } from "../common/infrastructure/config/client/maloja.js";
import { WildcardEmitter } from "../common/WildcardEmitter.js";
import { Notifiers } from "../notifier/Notifiers.js";
import { joinedUrl, readJson, thresholdResultSummary } from "../utils.js";
import { readJson, thresholdResultSummary } from "../utils.js";
import { joinedUrl } from "../utils/NetworkUtils.js";
import { getTypeSchemaFromConfigGenerator } from "../utils/SchemaUtils.js";
import { validateJson } from "../utils/ValidationUtils.js";
import AbstractScrobbleClient from "./AbstractScrobbleClient.js";
Expand Down
3 changes: 2 additions & 1 deletion src/backend/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import path from "path";
import ViteExpress from "vite-express";
import { projectDir } from "../common/index.js";
import { getRoot } from "../ioc.js";
import { getAddress, parseBool } from "../utils.js";
import { parseBool } from "../utils.js";
import { getAddress } from "../utils/NetworkUtils.js";
import { setupApi } from "./api.js";

const app = addAsync(express());
Expand Down
3 changes: 2 additions & 1 deletion src/backend/sources/DeezerSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import request from 'superagent';
import { PlayObject } from "../../core/Atomic.js";
import { DEFAULT_RETRY_MULTIPLIER, FormatPlayObjectOptions, InternalConfig } from "../common/infrastructure/Atomic.js";
import { DeezerSourceConfig } from "../common/infrastructure/config/source/deezer.js";
import { joinedUrl, parseRetryAfterSecsFromObj, readJson, sleep, sortByOldestPlayDate, writeFile, } from "../utils.js";
import { parseRetryAfterSecsFromObj, readJson, sleep, sortByOldestPlayDate, writeFile, } from "../utils.js";
import { joinedUrl } from "../utils/NetworkUtils.js";
import AbstractSource, { RecentlyPlayedOptions } from "./AbstractSource.js";

export default class DeezerSource extends AbstractSource {
Expand Down
3 changes: 2 additions & 1 deletion src/backend/sources/JellyfinApiSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ import {
PlayPlatformId, REPORTED_PLAYER_STATUSES
} from "../common/infrastructure/Atomic.js";
import { JellyApiSourceConfig } from "../common/infrastructure/config/source/jellyfin.js";
import { genGroupIdStr, getPlatformIdFromData, joinedUrl, parseBool, } from "../utils.js";
import { genGroupIdStr, getPlatformIdFromData, parseBool, } from "../utils.js";
import { joinedUrl } from "../utils/NetworkUtils.js";
import { parseArrayFromMaybeString } from "../utils/StringUtils.js";
import { MemoryPositionalSource } from "./MemoryPositionalSource.js";
import { FixedSizeList } from "fixed-size-list";
Expand Down
6 changes: 3 additions & 3 deletions src/backend/sources/PlexApiSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import {
PlayerStateDataMaybePlay,
PlayPlatformId, REPORTED_PLAYER_STATUSES
} from "../common/infrastructure/Atomic.js";
import { genGroupIdStr, getFirstNonEmptyString, getPlatformIdFromData, joinedUrl, parseBool, } from "../utils.js";
import { genGroupIdStr, getFirstNonEmptyString, getPlatformIdFromData, parseBool, } from "../utils.js";
import { buildStatePlayerPlayIdententifyingInfo, parseArrayFromMaybeString } from "../utils/StringUtils.js";
import { GetSessionsMetadata } from "@lukehagar/plexjs/sdk/models/operations/getsessions.js";
import { PlexAPI } from "@lukehagar/plexjs";
import {
SDKValidationError,
} from "@lukehagar/plexjs/sdk/models/errors";
import { PlexApiSourceConfig } from "../common/infrastructure/config/source/plex.js";
import { isPortReachable } from '../utils/NetworkUtils.js';
import { isPortReachable, joinedUrl } from '../utils/NetworkUtils.js';
import normalizeUrl from 'normalize-url';
import { GetTokenDetailsResponse, GetTokenDetailsUserPlexAccount } from '@lukehagar/plexjs/sdk/models/operations/gettokendetails.js';
import { parseRegexSingle } from '@foxxmd/regex-buddy-core';
Expand Down Expand Up @@ -393,7 +393,7 @@ export default class PlexApiSource extends MemoryPositionalSource {
return validSession;
}
return sessions[0];
}
}

sessionToPlayerState = (obj: GetSessionsMetadata): PlayerStateDataMaybePlay => {

Expand Down
2 changes: 1 addition & 1 deletion src/backend/sources/SpotifySource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import {
} from "../common/infrastructure/Atomic.js";
import { SpotifySourceConfig } from "../common/infrastructure/config/source/spotify.js";
import {
joinedUrl,
parseRetryAfterSecsFromObj,
readJson,
sleep,
sortByOldestPlayDate,
writeFile,
} from "../utils.js";
import { findCauseByFunc } from "../utils/ErrorUtils.js";
import { joinedUrl } from "../utils/NetworkUtils.js";
import { RecentlyPlayedOptions } from "./AbstractSource.js";
import AlbumObjectSimplified = SpotifyApi.AlbumObjectSimplified;
import ArtistObjectSimplified = SpotifyApi.ArtistObjectSimplified;
Expand Down
3 changes: 2 additions & 1 deletion src/backend/sources/WebScrobblerSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
WebScrobblerPayload,
WebScrobblerSong
} from "../common/vendor/webscrobbler/interfaces.js";
import { joinedUrl } from "../utils.js";

import { joinedUrl } from "../utils/NetworkUtils.js";
import MemorySource from "./MemorySource.js";

export class WebScrobblerSource extends MemorySource {
Expand Down
3 changes: 2 additions & 1 deletion src/backend/sources/YTMusicSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { YTMusicSourceConfig } from "../common/infrastructure/config/source/ytmu
import { Innertube, UniversalCache, Parser, YTNodes, ApiResponse, IBrowseResponse, Log, SessionOptions } from 'youtubei.js';
import { GenerateAuthUrlOpts, OAuth2Client } from 'google-auth-library';
import {resolve} from 'path';
import { joinedUrl, parseBool, sleep } from "../utils.js";
import { parseBool, sleep } from "../utils.js";
import {
getPlaysDiff,
humanReadableDiff,
Expand All @@ -16,6 +16,7 @@ import {
} from "../utils/PlayComparisonUtils.js";
import AbstractSource, { RecentlyPlayedOptions } from "./AbstractSource.js";
import { truncateStringToLength } from "../../core/StringUtils.js";
import { joinedUrl } from "../utils/NetworkUtils.js";

export const ytiHistoryResponseToListItems = (res: ApiResponse): YTNodes.MusicResponsiveListItem[] => {
const page = Parser.parseResponse<IBrowseResponse>(res.data);
Expand Down
Loading

0 comments on commit 83ff534

Please sign in to comment.