diff --git a/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts b/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts new file mode 100644 index 00000000..cc8fcd1c --- /dev/null +++ b/src/backend/common/vendor/bluesky/AbstractBlueSkyApiClient.ts @@ -0,0 +1,26 @@ +import { getRoot } from "../../../ioc.js"; +import { AbstractApiOptions } from "../../infrastructure/Atomic.js"; +import { TealClientData } from "../../infrastructure/config/client/tealfm.js"; +import AbstractApiClient from "../AbstractApiClient.js"; +import { Agent } from "@atproto/api"; +import { MSCache } from "../../Cache.js"; + + +export abstract class AbstractBlueSkyApiClient extends AbstractApiClient { + + declare config: TealClientData; + + agent!: Agent; + + cache: MSCache; + + constructor(name: any, config: TealClientData, options: AbstractApiOptions) { + super('blueSky', name, config, options); + + this.cache = getRoot().items.cache(); + } + + abstract initClient(): void; + + abstract restoreSession(): Promise; +} \ No newline at end of file diff --git a/src/backend/common/vendor/bluesky/BlueSkyAppApiClient.ts b/src/backend/common/vendor/bluesky/BlueSkyAppApiClient.ts new file mode 100644 index 00000000..cdc82af0 --- /dev/null +++ b/src/backend/common/vendor/bluesky/BlueSkyAppApiClient.ts @@ -0,0 +1,69 @@ +import { AbstractApiOptions } from "../../infrastructure/Atomic.js"; +import { TealClientData } from "../../infrastructure/config/client/tealfm.js"; +import { Agent, CredentialSession, AtpSessionEvent, AtpSessionData } from "@atproto/api"; +import { AbstractBlueSkyApiClient } from "./AbstractBlueSkyApiClient.js"; + + +export class BlueSkyAppApiClient extends AbstractBlueSkyApiClient { + + appSession?: CredentialSession; + appPwAuth: boolean + + + constructor(name: any, config: TealClientData, options: AbstractApiOptions) { + super(name, config, options); + + this.logger.verbose('Using App Password auth for session'); + } + + protected initClientApp() { + this.appSession = new CredentialSession(new URL('https://bsky.social'), undefined, (evt: AtpSessionEvent, sess?: AtpSessionData) => { + this.cache.cacheAuth.set(`appPwSession-${this.name}`, sess); + }); + this.agent = new Agent(this.appSession); + } + + initClient() { + this.appSession = new CredentialSession(new URL('https://bsky.social'), undefined, (evt: AtpSessionEvent, sess?: AtpSessionData) => { + this.cache.cacheAuth.set(`appPwSession-${this.name}`, sess); + }); + this.agent = new Agent(this.appSession); + } + + restoreSession = async (): Promise => { + const savedSession = await this.cache.cacheAuth.get(`appPwSession-${this.name}`); + if (savedSession !== undefined) { + try { + this.logger.debug('Found existing session, trying to resume...'); + await this.appSession.resumeSession(savedSession); + this.logger.debug('Resumed session!'); + return true; + } catch (e) { + this.logger.warn(new Error('Could not resume app password session from data', { cause: e })); + return false; + } + } + this.logger.debug('No app password session data to restore'); + } + + appLogin = async (): Promise => { + try { + + const f = await this.appSession.login({ + identifier: this.config.identifier, + password: this.config.appPassword + }); + if (!f.success) { + this.logger.error('Login was not successful with app password'); + return false; + } + this.logger.debug('Logged in.'); + //this.cache.cacheAuth.set(`appPwSession-${this.name}`, f.data); + return true; + } catch (e) { + this.logger.error('Could not login using app password', { cause: e }); + return false; + } + } + +} \ No newline at end of file diff --git a/src/backend/common/vendor/bluesky/BlueSkyApiClient.ts b/src/backend/common/vendor/bluesky/BlueSkyOauthApiClient.ts similarity index 52% rename from src/backend/common/vendor/bluesky/BlueSkyApiClient.ts rename to src/backend/common/vendor/bluesky/BlueSkyOauthApiClient.ts index 3b34f17a..81b8415d 100644 --- a/src/backend/common/vendor/bluesky/BlueSkyApiClient.ts +++ b/src/backend/common/vendor/bluesky/BlueSkyOauthApiClient.ts @@ -1,7 +1,5 @@ -import { getRoot } from "../../../ioc.js"; import { AbstractApiOptions } from "../../infrastructure/Atomic.js"; import { TealClientData } from "../../infrastructure/config/client/tealfm.js"; -import AbstractApiClient from "../AbstractApiClient.js"; import { NodeOAuthClient, NodeSavedStateStore, @@ -9,45 +7,24 @@ import { type OAuthClientMetadataInput, OAuthSession, } from "@atproto/oauth-client-node"; -import { Agent, CredentialSession, AtpSessionEvent, AtpSessionData } from "@atproto/api"; -import { MSCache } from "../../Cache.js"; +import { Agent } from "@atproto/api"; +import { AbstractBlueSkyApiClient } from "./AbstractBlueSkyApiClient.js"; -export class BlueSkyApiClient extends AbstractApiClient { +export class BlueSkyOauthApiClient extends AbstractBlueSkyApiClient { declare config: TealClientData; oauthClient?: NodeOAuthClient; oauthSession: OAuthSession; - agent?: Agent; - appSession?: CredentialSession; - appPwAuth: boolean - - cache: MSCache; constructor(name: any, config: TealClientData, options: AbstractApiOptions) { - super('blueSky', name, config, options); - - this.cache = getRoot().items.cache(); - - if (config.appPassword !== undefined) { - this.logger.verbose('Found app password, Will use App Password auth for session'); - this.appPwAuth = true; - } else if (config.baseUri !== undefined) { - this.logger.verbose('Found baseUri, will use oauth for session'); - this.appPwAuth = false; - } + super(name, config, options); + this.logger.verbose('Will use oauth for session'); } - protected initClientApp() { - this.appSession = new CredentialSession(new URL('https://bsky.social'), undefined, (evt: AtpSessionEvent, sess?: AtpSessionData) => { - this.cache.cacheAuth.set(`appPwSession-${this.name}`, sess); - }); - this.agent = new Agent(this.appSession); - } - - protected initClientOauth() { + initClient = () => { const sessionStore: NodeSavedSessionStore = { set: (k: string, state) => this.cache.cacheAuth.set(`session-${this.name}-${k}`, state).then(() => null), get: (k: string) => this.cache.cacheAuth.get(`session-${this.name}-${k}`), @@ -71,31 +48,7 @@ export class BlueSkyApiClient extends AbstractApiClient { } } - initClient = () => { - if (this.appPwAuth) { - this.initClientApp(); - } else { - this.initClientOauth(); - } - } - - protected async restoreSessionApp(): Promise { - const savedSession = await this.cache.cacheAuth.get(`appPwSession-${this.name}`); - if (savedSession !== undefined) { - try { - this.logger.debug('Found existing session, trying to resume...'); - await this.appSession.resumeSession(savedSession); - this.logger.debug('Resumed session!'); - return true; - } catch (e) { - this.logger.warn(new Error('Could not resume app password session from data', { cause: e })); - return false; - } - } - this.logger.debug('No app password session data to restore'); - } - - protected async restoreOauthApp(): Promise { + restoreSession = async (): Promise => { const did = await this.cache.cacheAuth.get(`did-${this.name}`); if (did === undefined) { this.logger.debug('No did has been stored yet'); @@ -110,34 +63,6 @@ export class BlueSkyApiClient extends AbstractApiClient { } } - restoreSession = async (): Promise => { - if (this.appSession) { - return await this.restoreSessionApp(); - } else { - return await this.restoreOauthApp(); - } - } - - appLogin = async (): Promise => { - try { - - const f = await this.appSession.login({ - identifier: this.config.identifier, - password: this.config.appPassword - }); - if (!f.success) { - this.logger.error('Login was not successful with app password'); - return false; - } - this.logger.debug('Logged in.'); - //this.cache.cacheAuth.set(`appPwSession-${this.name}`, f.data); - return true; - } catch (e) { - this.logger.error('Could not login using app password', { cause: e }); - return false; - } - } - createAuthorizeUrl = async (handle: string) => { const url = await this.oauthClient.authorize(handle.replace('@', '')); diff --git a/src/backend/scrobblers/TealfmScrobbler.ts b/src/backend/scrobblers/TealfmScrobbler.ts index cb7def95..fe308063 100644 --- a/src/backend/scrobblers/TealfmScrobbler.ts +++ b/src/backend/scrobblers/TealfmScrobbler.ts @@ -13,7 +13,9 @@ import { isDebugMode } from "../utils.js"; import { KoitoClientConfig } from "../common/infrastructure/config/client/koito.js"; import { KoitoApiClient, listenObjectResponseToPlay } from "../common/vendor/koito/KoitoApiClient.js"; import { TealClientConfig } from "../common/infrastructure/config/client/tealfm.js"; -import { BlueSkyApiClient } from "../common/vendor/bluesky/BlueSkyApiClient.js"; +import { BlueSkyAppApiClient } from "../common/vendor/bluesky/BlueSkyAppApiClient.js"; +import { BlueSkyOauthApiClient } from "../common/vendor/bluesky/BlueSkyOauthApiClient.js"; +import { AbstractBlueSkyApiClient } from "../common/vendor/bluesky/AbstractBlueSkyApiClient.js"; export default class TealScrobbler extends AbstractScrobbleClient { @@ -22,7 +24,7 @@ export default class TealScrobbler extends AbstractScrobbleClient { declare config: TealClientConfig; - client: BlueSkyApiClient; + client: AbstractBlueSkyApiClient; constructor(name: any, config: TealClientConfig, options = {}, notifier: Notifiers, emitter: EventEmitter, logger: Logger) { super('tealfm', name, config, notifier, emitter, logger); @@ -30,9 +32,13 @@ export default class TealScrobbler extends AbstractScrobbleClient { // 1000 is way too high. maxing at 100 this.MAX_INITIAL_SCROBBLES_FETCH = 100; this.supportsNowPlaying = false; - this.client = new BlueSkyApiClient(name, config.data, {...options, logger}); - if(this.config.data.appPassword !== undefined) { + if(config.data.appPassword !== undefined) { + this.client = new BlueSkyAppApiClient(name, config.data, {...options, logger}); this.requiresAuthInteraction = false; + } else if(config.data.baseUri !== undefined) { + this.client = new BlueSkyOauthApiClient(name, config.data, {...options, logger}); + } else { + throw new Error(`Must define either 'baseUri' or 'appPassword' in configuration!`); } } @@ -61,7 +67,7 @@ export default class TealScrobbler extends AbstractScrobbleClient { } async getAuthorizeUrl(): Promise { - return await this.client.createAuthorizeUrl(this.config.data.identifier); + return await (this.client as BlueSkyOauthApiClient).createAuthorizeUrl(this.config.data.identifier); } doAuthentication = async () => { @@ -71,7 +77,7 @@ export default class TealScrobbler extends AbstractScrobbleClient { if(sessionRes) { return true; } - if(this.config.data.appPassword !== undefined) { + if(this.client instanceof BlueSkyAppApiClient) { return await this.client.appLogin(); } } catch (e) { diff --git a/src/backend/server/auth.ts b/src/backend/server/auth.ts index 98779fe8..feb9d6a8 100644 --- a/src/backend/server/auth.ts +++ b/src/backend/server/auth.ts @@ -12,6 +12,7 @@ import { sortAndDeduplicateDiagnostics } from "typescript"; import { source } from "common-tags"; import TealScrobbler from "../scrobblers/TealfmScrobbler.js"; import { parseRegexSingle } from "@foxxmd/regex-buddy-core"; +import { BlueSkyOauthApiClient } from "../common/vendor/bluesky/BlueSkyOauthApiClient.js"; export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMiddle: ExpressHandler, clientMiddle: ExpressHandler, scrobbleSources: ScrobbleSources, scrobbleClients: ScrobbleClients) => { app.use('/api/client/auth', clientMiddle); @@ -171,16 +172,16 @@ export const setupAuthRoutes = (app: ExpressWithAsync, logger: Logger, sourceMid handle } = {} } = req; - const url = await validClient.client.createAuthorizeUrl(handle as string); + const url = await (validClient.client as BlueSkyOauthApiClient).createAuthorizeUrl(handle as string); res.redirect(url) } if(intents[1].includes('client-metadata.json')) { - return res.json(validClient.client.getMetadata()); + return res.json((validClient.client as BlueSkyOauthApiClient).getMetadata()); } if(intents[1].includes('oauth/callback')) { - const result = await validClient.client.handleCallback(new URLSearchParams(req.query as Record)); + const result = await (validClient.client as BlueSkyOauthApiClient).handleCallback(new URLSearchParams(req.query as Record)); if(result) { return res.status(200); }