← Public packages
@kentcdodds/bluesky
Bluesky and AT Protocol helpers using package-storage handle plus blueskyAppPassword.
src/write-action-input.ts
257 lines · 6.1 KB · TypeScriptimport {
ValidationError,
any,
boolean,
object,
optional,
parse,
string,
} from 'remix/data-schema'
import { formatSchemaValidationError } from './schema-input-errors.ts'
import type {
DeletePostInput,
FollowInput,
LikeRepostInput,
UnfollowInput,
} from './types.ts'
const nonEmptyString = string().refine((value) => value.trim().length > 0, 'Expected non-empty string')
function firstNonEmpty(...values: Array<unknown>): string | undefined {
for (const value of values) {
if (typeof value === 'string' && value.trim().length > 0) return value.trim()
}
return undefined
}
const SERVICE_CONFIRM = {
service: optional(string()),
dryRun: optional(boolean()),
confirm: optional(boolean()),
} as const
const DELETE_KNOWN_KEYS = [
'uri',
'postUri',
'atUri',
'post',
'repo',
'collection',
'rkey',
'service',
'dryRun',
'confirm',
] as const
const LIKE_REPOST_KNOWN_KEYS = [
'uri',
'postUri',
'atUri',
'post',
'cid',
'postCid',
'subject',
'createdAt',
'service',
'dryRun',
'confirm',
] as const
const FOLLOW_KNOWN_KEYS = [
'actor',
'handle',
'did',
'user',
'username',
'createdAt',
'service',
'dryRun',
'confirm',
] as const
const UNFOLLOW_KNOWN_KEYS = [
'uri',
'followUri',
'actor',
'handle',
'did',
'user',
'username',
'service',
'dryRun',
'confirm',
] as const
const DeleteSchema = object(
{
uri: optional(nonEmptyString),
postUri: optional(nonEmptyString),
atUri: optional(nonEmptyString),
post: optional(nonEmptyString),
repo: optional(nonEmptyString),
collection: optional(nonEmptyString),
rkey: optional(nonEmptyString),
...SERVICE_CONFIRM,
},
{ unknownKeys: 'error' },
).transform((raw): DeletePostInput => {
const uri = firstNonEmpty(raw.uri, raw.postUri, raw.atUri, raw.post)
return {
uri,
repo: raw.repo,
collection: raw.collection,
rkey: raw.rkey,
service: raw.service,
dryRun: raw.dryRun,
confirm: raw.confirm,
}
})
const LikeRepostSchema = object(
{
uri: optional(nonEmptyString),
postUri: optional(nonEmptyString),
atUri: optional(nonEmptyString),
post: optional(nonEmptyString),
cid: optional(nonEmptyString),
postCid: optional(nonEmptyString),
subject: optional(any()),
createdAt: optional(string()),
...SERVICE_CONFIRM,
},
{ unknownKeys: 'error' },
).transform((raw): LikeRepostInput => {
let uri = firstNonEmpty(raw.uri, raw.postUri, raw.atUri, raw.post)
let cid = firstNonEmpty(raw.cid, raw.postCid)
if (raw.subject && typeof raw.subject === 'object' && !Array.isArray(raw.subject)) {
const subject = raw.subject as Record<string, unknown>
uri = uri ?? firstNonEmpty(subject.uri, subject.postUri, subject.atUri)
cid = cid ?? firstNonEmpty(subject.cid, subject.postCid)
}
return {
uri,
cid,
createdAt: raw.createdAt,
service: raw.service,
dryRun: raw.dryRun,
confirm: raw.confirm,
}
})
const FollowSchema = object(
{
actor: optional(nonEmptyString),
handle: optional(nonEmptyString),
did: optional(nonEmptyString),
user: optional(nonEmptyString),
username: optional(nonEmptyString),
createdAt: optional(string()),
...SERVICE_CONFIRM,
},
{ unknownKeys: 'error' },
).transform((raw): FollowInput => ({
actor: firstNonEmpty(raw.actor, raw.handle, raw.did, raw.user, raw.username),
createdAt: raw.createdAt,
service: raw.service,
dryRun: raw.dryRun,
confirm: raw.confirm,
}))
const UnfollowSchema = object(
{
uri: optional(nonEmptyString),
followUri: optional(nonEmptyString),
actor: optional(nonEmptyString),
handle: optional(nonEmptyString),
did: optional(nonEmptyString),
user: optional(nonEmptyString),
username: optional(nonEmptyString),
...SERVICE_CONFIRM,
},
{ unknownKeys: 'error' },
).transform((raw): UnfollowInput => ({
uri: firstNonEmpty(raw.uri, raw.followUri),
actor: firstNonEmpty(raw.actor, raw.handle, raw.did, raw.user, raw.username),
service: raw.service,
dryRun: raw.dryRun,
confirm: raw.confirm,
}))
function wrapParse<T>(
surface: string,
knownKeys: readonly string[],
hint: string,
schema: { /* opaque */ },
input: unknown,
): T {
try {
return parse(schema as never, input ?? {}) as T
} catch (error) {
if (error instanceof ValidationError) {
throw formatSchemaValidationError(error, { surface, knownKeys, hint, input })
}
throw error
}
}
export function parseDeletePostInput(input: unknown): DeletePostInput {
const normalized = wrapParse<DeletePostInput>(
'delete-post',
DELETE_KNOWN_KEYS,
`Pass uri (aliases: postUri, atUri, post) or repo+rkey (+ optional collection). Unknown keys are rejected.`,
DeleteSchema,
input,
)
if (!normalized.uri && !(normalized.repo && normalized.rkey)) {
throw new Error(
'delete-post requires uri (aliases: postUri, atUri, post) or repo + rkey (optional collection; defaults to app.bsky.feed.post).',
)
}
return normalized
}
export function parseLikeRepostInput(input: unknown, surface: string): LikeRepostInput {
const normalized = wrapParse<LikeRepostInput>(
surface,
LIKE_REPOST_KNOWN_KEYS,
`Pass uri + cid (aliases: postUri/atUri/post, postCid; or subject: { uri, cid }). Unknown keys are rejected.`,
LikeRepostSchema,
input,
)
if (!normalized.uri || !normalized.cid) {
throw new Error(
`${surface} requires uri and cid (aliases: postUri/atUri/post + postCid, or subject: { uri, cid }).`,
)
}
return normalized
}
export function parseFollowInput(input: unknown): FollowInput {
const normalized = wrapParse<FollowInput>(
'follow',
FOLLOW_KNOWN_KEYS,
`Pass actor (aliases: handle, did, user, username). Unknown keys are rejected.`,
FollowSchema,
input,
)
if (!normalized.actor) {
throw new Error('follow requires actor (aliases: handle, did, user, username).')
}
return normalized
}
export function parseUnfollowInput(input: unknown): UnfollowInput {
const normalized = wrapParse<UnfollowInput>(
'unfollow',
UNFOLLOW_KNOWN_KEYS,
`Pass uri/followUri of the follow record, or actor (aliases: handle, did, user, username) already followed. Unknown keys are rejected.`,
UnfollowSchema,
input,
)
if (!normalized.uri && !normalized.actor) {
throw new Error(
'unfollow requires uri (alias: followUri) or actor (aliases: handle, did, user, username).',
)
}
return normalized
}