diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 5e345ae4..412fa530 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -7,9 +7,10 @@ import { type DittoFilter } from '@/filter.ts'; import { getAuthor, getFollowedPubkeys, getFollows } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { jsonMetaContentSchema } from '@/schemas/nostr.ts'; +import { setTag } from '@/tags.ts'; import { uploadFile } from '@/upload.ts'; import { isFollowing, lookupAccount, nostrNow } from '@/utils.ts'; -import { paginated, paginationSchema, parseBody } from '@/utils/web.ts'; +import { paginated, paginationSchema, parseBody, updateListEvent } from '@/utils/web.ts'; import { createEvent } from '@/utils/web.ts'; import { renderEventAccounts } from '@/views.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; @@ -219,14 +220,12 @@ const followController: AppController = async (c) => { const source = await getFollows(sourcePubkey); if (!source || !isFollowing(source, targetPubkey)) { - await createEvent({ - kind: 3, - content: '', - tags: [ - ...(source?.tags ?? []), - ['p', targetPubkey], - ], - }, c); + await updateListEvent( + source ?? { kind: 3 }, + ['p', targetPubkey], + setTag, + c, + ); } const relationship = await renderRelationship(sourcePubkey, targetPubkey); diff --git a/src/tags.ts b/src/tags.ts index a55560c3..4909b3ff 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -11,6 +11,11 @@ function getTagSet(tags: string[][], tagName: string): Set { return set; } +/** Check if the tag exists by its name and value. */ +function hasTag(tags: string[][], tag: string[]): boolean { + return tags.some(([name, value]) => name === tag[0] && value === tag[1]); +} + /** Delete all occurences of the tag by its name/value pair. */ function deleteTag(tags: readonly string[][], tag: string[]): string[][] { return tags.filter(([name, value]) => !(name === tag[0] && value === tag[1])); @@ -26,4 +31,4 @@ function setTag(tags: readonly string[][], tag: string[]): string[][] { } } -export { deleteTag, getTagSet, setTag }; +export { deleteTag, getTagSet, hasTag, setTag }; diff --git a/src/utils.ts b/src/utils.ts index 1f10cd3b..27c61364 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import { type Event, type EventTemplate, getEventHash, nip19, z } from '@/deps.ts'; import { getAuthor } from '@/queries.ts'; +import { hasTag } from '@/tags.ts'; import { lookupNip05Cached } from '@/utils/nip05.ts'; import { nostrIdSchema } from '@/schemas/nostr.ts'; @@ -97,9 +98,7 @@ const isRelay = (relay: string): relay is `wss://${string}` => relaySchema.safeP /** Check whether source is following target. */ function isFollowing(source: Event<3>, targetPubkey: string): boolean { - return Boolean( - source.tags.find(([tagName, tagValue]) => tagName === 'p' && tagValue === targetPubkey), - ); + return hasTag(source.tags, ['p', targetPubkey]); } /** Deduplicate events by ID. */ diff --git a/src/utils/web.ts b/src/utils/web.ts index b02209f1..0e559c63 100644 --- a/src/utils/web.ts +++ b/src/utils/web.ts @@ -1,13 +1,12 @@ +import { type AppContext } from '@/app.ts'; import { Conf } from '@/config.ts'; import { type Context, type Event, EventTemplate, HTTPException, parseFormData, type TypeFest, z } from '@/deps.ts'; import * as pipeline from '@/pipeline.ts'; import { signAdminEvent, signEvent } from '@/sign.ts'; import { nostrNow } from '@/utils.ts'; -import type { AppContext } from '@/app.ts'; - /** EventTemplate with defaults. */ -type EventStub = TypeFest.SetOptional, 'created_at' | 'tags'>; +type EventStub = TypeFest.SetOptional, 'content' | 'created_at' | 'tags'>; /** Publish an event through the pipeline. */ async function createEvent(t: EventStub, c: AppContext): Promise> { @@ -18,6 +17,7 @@ async function createEvent(t: EventStub, c: AppContext): Pr } const event = await signEvent({ + content: '', created_at: nostrNow(), tags: [], ...t, @@ -26,9 +26,24 @@ async function createEvent(t: EventStub, c: AppContext): Pr return publishEvent(event, c); } +/** Add the tag to the list and then publish the new list, or throw if the tag already exists. */ +function updateListEvent>( + t: E, + tag: string[], + fn: (tags: string[][], tag: string[]) => string[][], + c: AppContext, +): Promise> { + const { kind, content, tags = [] } = t; + return createEvent( + { kind, content, tags: fn(tags, tag) }, + c, + ); +} + /** Publish an admin event through the pipeline. */ async function createAdminEvent(t: EventStub, c: AppContext): Promise> { const event = await signAdminEvent({ + content: '', created_at: nostrNow(), tags: [], ...t, @@ -139,4 +154,5 @@ export { type PaginationParams, paginationSchema, parseBody, + updateListEvent, };