Merge branch 'main' into admin-reports-api

Update local branch & solve conflicts in hydrate.ts
This commit is contained in:
P. Reis
2024-05-04 20:23:16 -03:00
8 changed files with 149 additions and 15 deletions

View File

@@ -45,6 +45,7 @@ const instanceController: AppController = async (c) => {
'mastodon_api_streaming',
'exposable_reactions',
'quote_posting',
'v2_suggestions',
],
},
},

View File

@@ -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);
};

View File

@@ -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),
});

View File

@@ -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);
}