Merge branch 'main' into admin-reports-api
Update local branch & solve conflicts in hydrate.ts
This commit is contained in:
commit
d903a2d0fb
10
src/app.ts
10
src/app.ts
|
@ -31,6 +31,7 @@ import { blocksController } from '@/controllers/api/blocks.ts';
|
|||
import { bookmarksController } from '@/controllers/api/bookmarks.ts';
|
||||
import { emptyArrayController, emptyObjectController, notImplementedController } from '@/controllers/api/fallback.ts';
|
||||
import { instanceController } from '@/controllers/api/instance.ts';
|
||||
import { markersController, updateMarkersController } from '@/controllers/api/markers.ts';
|
||||
import { mediaController } from '@/controllers/api/media.ts';
|
||||
import { mutesController } from '@/controllers/api/mutes.ts';
|
||||
import { notificationsController } from '@/controllers/api/notifications.ts';
|
||||
|
@ -62,6 +63,7 @@ import {
|
|||
zapController,
|
||||
} from '@/controllers/api/statuses.ts';
|
||||
import { streamingController } from '@/controllers/api/streaming.ts';
|
||||
import { suggestionsV1Controller, suggestionsV2Controller } from '@/controllers/api/suggestions.ts';
|
||||
import {
|
||||
hashtagTimelineController,
|
||||
homeTimelineController,
|
||||
|
@ -185,12 +187,18 @@ app.get('/api/pleroma/frontend_configurations', frontendConfigController);
|
|||
app.get('/api/v1/trends/tags', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController);
|
||||
app.get('/api/v1/trends', cache({ cacheName: 'web', expires: Time.minutes(15) }), trendingTagsController);
|
||||
|
||||
app.get('/api/v1/suggestions', suggestionsV1Controller);
|
||||
app.get('/api/v2/suggestions', suggestionsV2Controller);
|
||||
|
||||
app.get('/api/v1/notifications', requirePubkey, notificationsController);
|
||||
app.get('/api/v1/favourites', requirePubkey, favouritesController);
|
||||
app.get('/api/v1/bookmarks', requirePubkey, bookmarksController);
|
||||
app.get('/api/v1/blocks', requirePubkey, blocksController);
|
||||
app.get('/api/v1/mutes', requirePubkey, mutesController);
|
||||
|
||||
app.get('/api/v1/markers', requireProof(), markersController);
|
||||
app.post('/api/v1/markers', requireProof(), updateMarkersController);
|
||||
|
||||
app.get('/api/v1/admin/accounts', requireRole('admin'), adminAccountsController);
|
||||
app.get('/api/v1/pleroma/admin/config', requireRole('admin'), configController);
|
||||
app.post('/api/v1/pleroma/admin/config', requireRole('admin'), updateConfigController);
|
||||
|
@ -205,9 +213,7 @@ app.get('/api/v1/admin/reports', requirePubkey, requireRole('admin'), viewAllRep
|
|||
// Not (yet) implemented.
|
||||
app.get('/api/v1/custom_emojis', emptyArrayController);
|
||||
app.get('/api/v1/filters', emptyArrayController);
|
||||
app.get('/api/v1/mutes', emptyArrayController);
|
||||
app.get('/api/v1/domain_blocks', emptyArrayController);
|
||||
app.get('/api/v1/markers', emptyObjectController);
|
||||
app.get('/api/v1/conversations', emptyArrayController);
|
||||
app.get('/api/v1/lists', emptyArrayController);
|
||||
|
||||
|
|
|
@ -45,6 +45,7 @@ const instanceController: AppController = async (c) => {
|
|||
'mastodon_api_streaming',
|
||||
'exposable_reactions',
|
||||
'quote_posting',
|
||||
'v2_suggestions',
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { AppController } from '@/app.ts';
|
||||
import { parseBody } from '@/utils/api.ts';
|
||||
|
||||
const kv = await Deno.openKv();
|
||||
|
||||
type Timeline = 'home' | 'notifications';
|
||||
|
||||
interface Marker {
|
||||
last_read_id: string;
|
||||
version: number;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export const markersController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const timelines = c.req.queries('timeline[]') ?? [];
|
||||
|
||||
const results = await kv.getMany<Marker[]>(
|
||||
timelines.map((timeline) => ['markers', pubkey, timeline]),
|
||||
);
|
||||
|
||||
const marker = results.reduce<Record<string, Marker>>((acc, { key, value }) => {
|
||||
if (value) {
|
||||
const timeline = key[key.length - 1] as string;
|
||||
acc[timeline] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return c.json(marker);
|
||||
};
|
||||
|
||||
const markerDataSchema = z.object({
|
||||
last_read_id: z.string(),
|
||||
});
|
||||
|
||||
export const updateMarkersController: AppController = async (c) => {
|
||||
const pubkey = c.get('pubkey')!;
|
||||
const record = z.record(z.enum(['home', 'notifications']), markerDataSchema).parse(await parseBody(c.req.raw));
|
||||
const timelines = Object.keys(record) as Timeline[];
|
||||
|
||||
const markers: Record<string, Marker> = {};
|
||||
|
||||
const entries = await kv.getMany<Marker[]>(
|
||||
timelines.map((timeline) => ['markers', pubkey, timeline]),
|
||||
);
|
||||
|
||||
for (const timeline of timelines) {
|
||||
const last = entries.find(({ key }) => key[key.length - 1] === timeline);
|
||||
|
||||
const marker: Marker = {
|
||||
last_read_id: record[timeline]!.last_read_id,
|
||||
version: last?.value ? last.value.version + 1 : 1,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await kv.set(['markers', pubkey, timeline], marker);
|
||||
markers[timeline] = marker;
|
||||
}
|
||||
|
||||
return c.json(markers);
|
||||
};
|
|
@ -9,7 +9,6 @@ import { bech32ToPubkey } from '@/utils.ts';
|
|||
import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { UserStore } from '@/storages/UserStore.ts';
|
||||
|
||||
const debug = Debug('ditto:streaming');
|
||||
|
||||
|
@ -68,17 +67,14 @@ const streamingController: AppController = (c) => {
|
|||
const filter = await topicToFilter(stream, c.req.query(), pubkey);
|
||||
if (!filter) return;
|
||||
|
||||
const store = pubkey ? new UserStore(pubkey, Storages.admin) : Storages.admin;
|
||||
|
||||
try {
|
||||
for await (const msg of Storages.pubsub.req([filter], { signal: controller.signal })) {
|
||||
if (msg[0] === 'EVENT') {
|
||||
const [event] = await store.query([{ ids: [msg[2].id] }]);
|
||||
if (!event) continue;
|
||||
const event = msg[2];
|
||||
|
||||
await hydrateEvents({
|
||||
events: [event],
|
||||
storage: store,
|
||||
storage: Storages.admin,
|
||||
signal: AbortSignal.timeout(1000),
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { NStore } from '@nostrify/nostrify';
|
||||
|
||||
import { AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { getTagSet } from '@/tags.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||
|
||||
export const suggestionsV1Controller: AppController = async (c) => {
|
||||
const store = c.get('store');
|
||||
const signal = c.req.raw.signal;
|
||||
const accounts = await renderSuggestedAccounts(store, signal);
|
||||
|
||||
return c.json(accounts);
|
||||
};
|
||||
|
||||
export const suggestionsV2Controller: AppController = async (c) => {
|
||||
const store = c.get('store');
|
||||
const signal = c.req.raw.signal;
|
||||
const accounts = await renderSuggestedAccounts(store, signal);
|
||||
|
||||
const suggestions = accounts.map((account) => ({
|
||||
source: 'staff',
|
||||
account,
|
||||
}));
|
||||
|
||||
return c.json(suggestions);
|
||||
};
|
||||
|
||||
async function renderSuggestedAccounts(store: NStore, signal?: AbortSignal) {
|
||||
const [follows] = await store.query(
|
||||
[{ kinds: [3], authors: [Conf.pubkey], limit: 1 }],
|
||||
{ signal },
|
||||
);
|
||||
|
||||
// TODO: pagination
|
||||
const pubkeys = [...getTagSet(follows?.tags ?? [], 'p')].slice(0, 20);
|
||||
|
||||
const profiles = await store.query(
|
||||
[{ kinds: [0], authors: pubkeys, limit: pubkeys.length }],
|
||||
{ signal },
|
||||
)
|
||||
.then((events) => hydrateEvents({ events, storage: store, signal }));
|
||||
|
||||
const accounts = await Promise.all(pubkeys.map((pubkey) => {
|
||||
const profile = profiles.find((event) => event.pubkey === pubkey);
|
||||
return profile ? renderAccount(profile) : accountFromPubkey(pubkey);
|
||||
}));
|
||||
|
||||
return accounts.filter(Boolean);
|
||||
}
|
|
@ -54,15 +54,18 @@ function deleteUnattachedMediaByUrl(url: string) {
|
|||
}
|
||||
|
||||
/** Get unattached media by IDs. */
|
||||
function getUnattachedMediaByIds(ids: string[]) {
|
||||
// deno-lint-ignore require-await
|
||||
async function getUnattachedMediaByIds(ids: string[]) {
|
||||
if (!ids.length) return [];
|
||||
return selectUnattachedMediaQuery()
|
||||
.where('id', 'in', ids)
|
||||
.execute();
|
||||
}
|
||||
|
||||
/** Delete rows as an event with media is being created. */
|
||||
function deleteAttachedMedia(pubkey: string, urls: string[]) {
|
||||
return db.deleteFrom('unattached_media')
|
||||
async function deleteAttachedMedia(pubkey: string, urls: string[]): Promise<void> {
|
||||
if (!urls.length) return;
|
||||
await db.deleteFrom('unattached_media')
|
||||
.where('pubkey', '=', pubkey)
|
||||
.where('url', 'in', urls)
|
||||
.execute();
|
||||
|
|
|
@ -91,6 +91,13 @@ function assembleEvents(
|
|||
}
|
||||
}
|
||||
|
||||
if (event.kind === 7) {
|
||||
const id = event.tags.find(([name]) => name === 'e')?.[1];
|
||||
if (id) {
|
||||
event.reacted = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e));
|
||||
}
|
||||
}
|
||||
|
||||
if (event.kind === 1984) {
|
||||
const targetAccountId = event.tags.find(([name]) => name === 'p')?.[1];
|
||||
if (targetAccountId) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
|||
import { nostrDate } from '@/utils.ts';
|
||||
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||
import { NostrEvent } from '@nostrify/nostrify';
|
||||
|
||||
interface RenderNotificationOpts {
|
||||
viewerPubkey: string;
|
||||
|
@ -32,7 +33,7 @@ async function renderMention(event: DittoEvent, opts: RenderNotificationOpts) {
|
|||
if (!status) return;
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
id: notificationId(event),
|
||||
type: 'mention',
|
||||
created_at: nostrDate(event.created_at).toISOString(),
|
||||
account: status.account,
|
||||
|
@ -47,7 +48,7 @@ async function renderReblog(event: DittoEvent, opts: RenderNotificationOpts) {
|
|||
const account = event.author ? await renderAccount(event.author) : accountFromPubkey(event.pubkey);
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
id: notificationId(event),
|
||||
type: 'reblog',
|
||||
created_at: nostrDate(event.created_at).toISOString(),
|
||||
account,
|
||||
|
@ -62,7 +63,7 @@ async function renderFavourite(event: DittoEvent, opts: RenderNotificationOpts)
|
|||
const account = event.author ? await renderAccount(event.author) : accountFromPubkey(event.pubkey);
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
id: notificationId(event),
|
||||
type: 'favourite',
|
||||
created_at: nostrDate(event.created_at).toISOString(),
|
||||
account,
|
||||
|
@ -77,7 +78,7 @@ async function renderReaction(event: DittoEvent, opts: RenderNotificationOpts) {
|
|||
const account = event.author ? await renderAccount(event.author) : accountFromPubkey(event.pubkey);
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
id: notificationId(event),
|
||||
type: 'pleroma:emoji_reaction',
|
||||
emoji: event.content,
|
||||
created_at: nostrDate(event.created_at).toISOString(),
|
||||
|
@ -86,4 +87,9 @@ async function renderReaction(event: DittoEvent, opts: RenderNotificationOpts) {
|
|||
};
|
||||
}
|
||||
|
||||
/** This helps notifications be sorted in the correct order. */
|
||||
function notificationId({ id, created_at }: NostrEvent): string {
|
||||
return `${created_at}-${id}`;
|
||||
}
|
||||
|
||||
export { renderNotification };
|
||||
|
|
Loading…
Reference in New Issue