feat: create updateInstanceController

for now only update: title, description, screenshots (user must provide the image URL) and thumbnail (user must provide the image URL)
screenshots array is stored in the content of the kind 0 of the
This commit is contained in:
P. Reis 2024-11-01 23:14:59 -03:00
parent c6ff4a9d25
commit 339b13c084
6 changed files with 96 additions and 4 deletions

View file

@ -49,6 +49,7 @@ import {
nameRequestController, nameRequestController,
nameRequestsController, nameRequestsController,
statusZapSplitsController, statusZapSplitsController,
updateInstanceController,
updateZapSplitsController, updateZapSplitsController,
} from '@/controllers/api/ditto.ts'; } from '@/controllers/api/ditto.ts';
import { emptyArrayController, notImplementedController } from '@/controllers/api/fallback.ts'; import { emptyArrayController, notImplementedController } from '@/controllers/api/fallback.ts';
@ -303,6 +304,8 @@ app.delete('/api/v1/pleroma/admin/statuses/:id', requireRole('admin'), pleromaAd
app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController); app.get('/api/v1/admin/ditto/relays', requireRole('admin'), adminRelaysController);
app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController); app.put('/api/v1/admin/ditto/relays', requireRole('admin'), adminSetRelaysController);
app.put('/api/v1/admin/ditto/instance', requireRole('admin'), updateInstanceController);
app.post('/api/v1/ditto/names', requireSigner, nameRequestController); app.post('/api/v1/ditto/names', requireSigner, nameRequestController);
app.get('/api/v1/ditto/names', requireSigner, nameRequestsController); app.get('/api/v1/ditto/names', requireSigner, nameRequestsController);

View file

@ -7,11 +7,13 @@ import { addTag } from '@/utils/tags.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts';
import { booleanParamSchema, percentageSchema } from '@/schema.ts'; import { booleanParamSchema, percentageSchema } from '@/schema.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { createEvent, paginated, parseBody } from '@/utils/api.ts'; import { createEvent, paginated, parseBody, updateEvent } from '@/utils/api.ts';
import { getInstanceMetadata } from '@/utils/instance.ts';
import { deleteTag } from '@/utils/tags.ts'; import { deleteTag } from '@/utils/tags.ts';
import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts';
import { DittoZapSplits, getZapSplits } from '@/utils/zap-split.ts'; import { DittoZapSplits, getZapSplits } from '@/utils/zap-split.ts';
import { getAuthor } from '@/queries.ts'; import { getAuthor } from '@/queries.ts';
import { screenshotsSchema } from '@/schemas/nostr.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';
import { renderNameRequest } from '@/views/ditto.ts'; import { renderNameRequest } from '@/views/ditto.ts';
import { renderAccount } from '@/views/mastodon/accounts.ts'; import { renderAccount } from '@/views/mastodon/accounts.ts';
@ -287,3 +289,54 @@ export const statusZapSplitsController: AppController = async (c) => {
return c.json(zapSplits, 200); return c.json(zapSplits, 200);
}; };
const updateInstanceSchema = z.object({
title: z.string().optional(),
description: z.string().optional(),
screenshots: screenshotsSchema.optional(),
thumbnail: z.object({
url: z.string().url(),
blurhash: z.string().optional(),
versions: z.object({
'@1x': z.string().url().optional(),
'@2x': z.string().url().optional(),
}).optional(),
}).optional(),
}).strict();
export const updateInstanceController: AppController = async (c) => {
const body = await parseBody(c.req.raw);
const result = updateInstanceSchema.safeParse(body);
const pubkey = Conf.pubkey;
if (!result.success) {
return c.json(result.error, 422);
}
await updateEvent(
{ kinds: [0], authors: [pubkey], limit: 1 },
async (_) => {
const meta = await getInstanceMetadata(await Storages.db(), c.req.raw.signal);
const {
title,
description,
screenshots,
thumbnail,
} = result.data;
meta.name = title ?? meta.name;
meta.about = description ?? meta.about;
meta.screenshots = screenshots ?? meta.screenshots;
meta.thumbnail = thumbnail ?? meta.thumbnail;
return {
kind: 0,
content: JSON.stringify(meta),
tags: [],
};
},
c,
);
return c.json(204);
};

View file

@ -96,6 +96,7 @@ const instanceV2Controller: AppController = async (c) => {
'@2x': meta.picture, '@2x': meta.picture,
}, },
}, },
screenshots: meta.screenshots,
languages: [ languages: [
'en', 'en',
], ],

View file

@ -20,6 +20,7 @@ export const manifestController: AppController = async (c) => {
scope: '/', scope: '/',
short_name: meta.name, short_name: meta.name,
start_url: '/', start_url: '/',
screenshots: meta.screenshots,
}; };
return c.json(manifest, { return c.json(manifest, {

View file

@ -2,17 +2,48 @@ import { NSchema as n } from '@nostrify/nostrify';
import { getEventHash, verifyEvent } from 'nostr-tools'; import { getEventHash, verifyEvent } from 'nostr-tools';
import { z } from 'zod'; import { z } from 'zod';
import { safeUrlSchema } from '@/schema.ts'; import { safeUrlSchema, sizesSchema } from '@/schema.ts';
/** Nostr event schema that also verifies the event's signature. */ /** Nostr event schema that also verifies the event's signature. */
const signedEventSchema = n.event() const signedEventSchema = n.event()
.refine((event) => event.id === getEventHash(event), 'Event ID does not match hash') .refine((event) => event.id === getEventHash(event), 'Event ID does not match hash')
.refine(verifyEvent, 'Event signature is invalid'); .refine(verifyEvent, 'Event signature is invalid');
/**
* Stored in the kind 0 content.
* https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots
*/
const screenshotsSchema = z.array(z.object({
form_factor: z.enum(['narrow', 'wide']).optional(),
label: z.string().optional(),
platform: z.enum([
'android',
'chromeos',
'ipados',
'ios',
'kaios',
'macos',
'windows',
'xbox',
'chrome_web_store',
'itunes',
'microsoft-inbox',
'microsoft-store',
'play',
]).optional(),
/** https://developer.mozilla.org/en-US/docs/Web/Manifest/screenshots#sizes */
sizes: sizesSchema,
/** Absolute URL. */
src: z.string().url(),
/** MIME type of the image. */
type: z.string().optional(),
}));
/** Kind 0 content schema for the Ditto server admin user. */ /** Kind 0 content schema for the Ditto server admin user. */
const serverMetaSchema = n.metadata().and(z.object({ const serverMetaSchema = n.metadata().and(z.object({
tagline: z.string().optional().catch(undefined), tagline: z.string().optional().catch(undefined),
email: z.string().optional().catch(undefined), email: z.string().optional().catch(undefined),
screenshots: screenshotsSchema.optional(),
})); }));
/** NIP-11 Relay Information Document. */ /** NIP-11 Relay Information Document. */
@ -32,4 +63,4 @@ const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url()
/** NIP-30 custom emoji tag. */ /** NIP-30 custom emoji tag. */
type EmojiTag = z.infer<typeof emojiTagSchema>; type EmojiTag = z.infer<typeof emojiTagSchema>;
export { type EmojiTag, emojiTagSchema, relayInfoDocSchema, serverMetaSchema, signedEventSchema }; export { type EmojiTag, emojiTagSchema, relayInfoDocSchema, screenshotsSchema, serverMetaSchema, signedEventSchema };

View file

@ -1,7 +1,8 @@
import { NostrEvent, NostrMetadata, NSchema as n, NStore } from '@nostrify/nostrify'; import { NostrEvent, NostrMetadata, NSchema as n, NStore } from '@nostrify/nostrify';
import { z } from 'zod';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { serverMetaSchema } from '@/schemas/nostr.ts'; import { screenshotsSchema, serverMetaSchema } from '@/schemas/nostr.ts';
/** Like NostrMetadata, but some fields are required and also contains some extra fields. */ /** Like NostrMetadata, but some fields are required and also contains some extra fields. */
export interface InstanceMetadata extends NostrMetadata { export interface InstanceMetadata extends NostrMetadata {
@ -11,6 +12,7 @@ export interface InstanceMetadata extends NostrMetadata {
picture: string; picture: string;
tagline: string; tagline: string;
event?: NostrEvent; event?: NostrEvent;
screenshots: z.infer<typeof screenshotsSchema>;
} }
/** Get and parse instance metadata from the kind 0 of the admin user. */ /** Get and parse instance metadata from the kind 0 of the admin user. */
@ -34,5 +36,6 @@ export async function getInstanceMetadata(store: NStore, signal?: AbortSignal):
email: meta.email ?? `postmaster@${Conf.url.host}`, email: meta.email ?? `postmaster@${Conf.url.host}`,
picture: meta.picture ?? Conf.local('/images/thumbnail.png'), picture: meta.picture ?? Conf.local('/images/thumbnail.png'),
event, event,
screenshots: meta.screenshots ?? [],
}; };
} }