diff --git a/cli/bootstrap.ts b/cli/bootstrap.ts new file mode 100644 index 0000000..389ba2b --- /dev/null +++ b/cli/bootstrap.ts @@ -0,0 +1,37 @@ +import { type Config, loadConfig, updateConfig } from "~/lib/config.ts"; +import { dim, log } from "~/cli/formatter.ts"; +import { AppError, describe } from "~/lib/errors.ts"; +import { Ok, Result } from "~/lib/result.ts"; +import { ask } from "~/utils/prompt.ts"; +import { italic } from "@std/fmt/colors"; + +type BootstrapConfig = Omit, "password" | "username">; + +export const SECRET_WARNING = italic(dim( + "\n\nYour Last.fm credentials are used only to authenticate with Last.fm services. " + + "They are sent directly to Last.fm over a secure connection and are not stored locally after authentication.", +)); + +export async function requireBaseConfig(): Promise> { + const result = await loadConfig(); + + if (result.ok && result.value.apiKey && result.value.secret) { + return Ok(result.value as BootstrapConfig); + } + + if (!result.ok && result.error.tag !== "not_found") { + return result; + } + + log.warn("No API credentials found. Let's set up scrobkit :3\n"); + + const apiKey = ask(" What is your Last.fm API key?"); + const sharedSecret = ask(SECRET_WARNING + "\n\n What about your key's shared secret, eh?", true); + + const saved = await updateConfig({ apiKey, secret: sharedSecret }); + if (!saved.ok) { + return saved; + } + + return Ok(saved.value as BootstrapConfig); +} diff --git a/cli/formatter.ts b/cli/formatter.ts index c835ec9..0a86da1 100644 --- a/cli/formatter.ts +++ b/cli/formatter.ts @@ -1,4 +1,4 @@ -import { cyan, dim, gray, green, red } from "@std/fmt/colors"; +import { cyan, dim, gray, green, red, yellow } from "@std/fmt/colors"; export const symbols = { retry: gray("\u21bb"), @@ -6,6 +6,19 @@ export const symbols = { success: green("\u2714"), forbid: red("\u2298"), error: red("\u2716"), + info: yellow("\u{1f6c8}"), } as const; +export const log = { + success: (msg: string) => console.log(` ${symbols.success} ${msg}`), + + error: (msg: string) => console.error(`${symbols.error} ${red(msg)}`), + + warn: (msg: string) => console.warn(`${symbols.warn} ${msg}`), + + info: (msg: string) => console.warn(`${symbols.info} ${msg}`), + + forbid: (msg: string) => console.log(`${symbols.forbid} ${cyan(msg)}`), +}; + export { dim }; diff --git a/cli/session.ts b/cli/session.ts new file mode 100644 index 0000000..db765e7 --- /dev/null +++ b/cli/session.ts @@ -0,0 +1,42 @@ +import { authenticate, verifySession } from "~/api/lastfm.ts"; +import { type Config, updateConfig } from "~/lib/config.ts"; +import { AppError } from "~/lib/errors.ts"; +import { log } from "~/cli/formatter.ts"; +import { ask } from "~/utils/prompt.ts"; +import { Ok, Result } from "~/lib/result.ts"; +import { SECRET_WARNING } from "~/cli/bootstrap.ts"; + +type SessionConfig = Omit, "password">; + +export async function ensureSession(config: Config): Promise> { + if (config.apiKey && config.secret && config.sessionKey && config.username) { + const check = await verifySession(config.apiKey, config.secret, config.sessionKey); + if (check.ok) { + return Ok(config as SessionConfig); + } + log.warn("Session key is invalid or expired. Re-authenticating..."); + } + + const username = ask(" What's your Last.fm username? "); + const password = ask(SECRET_WARNING + "\n\n What about your account password? ", true); + + log.info("Authenticating..."); + const auth = await authenticate(config.apiKey, config.secret, username, password); + + if (!auth.ok) { + return auth; + } + + const updated = await updateConfig({ + username: auth.value.name, + sessionKey: auth.value.key, + password: undefined, + }); + + if (!updated.ok) { + return updated; + } + + log.success(`Authenticated as ${auth.value.name}`); + return Ok(updated.value as SessionConfig); +} diff --git a/lib/config.ts b/lib/config.ts index 3c8f75f..4d2dd2d 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -6,6 +6,7 @@ export interface Config { apiKey: string; secret: string; username?: string; + sessionKey?: string; password?: string; }