Merge branch 'pipeline' into 'develop'
Event Pipeline See merge request soapbox-pub/ditto!14
This commit is contained in:
commit
4a61ce26a9
|
@ -1,5 +1,6 @@
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { type Event, type Filter, matchFilters, RelayPool, TTLCache } from '@/deps.ts';
|
import { type Event, type Filter, matchFilters, RelayPool, TTLCache } from '@/deps.ts';
|
||||||
|
import * as pipeline from '@/pipeline.ts';
|
||||||
import { Time } from '@/utils.ts';
|
import { Time } from '@/utils.ts';
|
||||||
|
|
||||||
import type { GetFiltersOpts } from '@/types.ts';
|
import type { GetFiltersOpts } from '@/types.ts';
|
||||||
|
@ -37,6 +38,7 @@ function getFilters<K extends number>(filters: Filter<K>[], opts: GetFiltersOpts
|
||||||
Conf.poolRelays,
|
Conf.poolRelays,
|
||||||
(event: Event | null) => {
|
(event: Event | null) => {
|
||||||
if (event && matchFilters(filters, event)) {
|
if (event && matchFilters(filters, event)) {
|
||||||
|
pipeline.handleEvent(event).catch(() => {});
|
||||||
results.push({
|
results.push({
|
||||||
id: event.id,
|
id: event.id,
|
||||||
kind: event.kind,
|
kind: event.kind,
|
||||||
|
@ -70,14 +72,4 @@ function getFilters<K extends number>(filters: Filter<K>[], opts: GetFiltersOpts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Publish an event to the Nostr relay. */
|
export { getFilters };
|
||||||
function publish(event: Event, relays = Conf.publishRelays): void {
|
|
||||||
console.log('Publishing event', event, relays);
|
|
||||||
try {
|
|
||||||
getPool().publish(event, relays);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getFilters, publish };
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { type Filter, findReplyTag, z } from '@/deps.ts';
|
import { type Filter, findReplyTag, z } from '@/deps.ts';
|
||||||
import { publish } from '@/client.ts';
|
|
||||||
import * as mixer from '@/mixer.ts';
|
import * as mixer from '@/mixer.ts';
|
||||||
|
import * as pipeline from '@/pipeline.ts';
|
||||||
import { getAuthor, getFollows } from '@/queries.ts';
|
import { getAuthor, getFollows } from '@/queries.ts';
|
||||||
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
|
||||||
import { signEvent } from '@/sign.ts';
|
import { signEvent } from '@/sign.ts';
|
||||||
|
@ -167,7 +167,13 @@ const updateCredentialsController: AppController = async (c) => {
|
||||||
created_at: nostrNow(),
|
created_at: nostrNow(),
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
publish(event);
|
try {
|
||||||
|
await pipeline.handleEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof pipeline.RelayError) {
|
||||||
|
return c.json({ error: e.message }, 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const account = await toAccount(event);
|
const account = await toAccount(event);
|
||||||
return c.json(account);
|
return c.json(account);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { type AppController } from '@/app.ts';
|
import { type AppController } from '@/app.ts';
|
||||||
import { publish } from '@/client.ts';
|
|
||||||
import { ISO6391, Kind, z } from '@/deps.ts';
|
import { ISO6391, Kind, z } from '@/deps.ts';
|
||||||
|
import * as pipeline from '@/pipeline.ts';
|
||||||
import { getAncestors, getDescendants, getEvent } from '@/queries.ts';
|
import { getAncestors, getDescendants, getEvent } from '@/queries.ts';
|
||||||
import { signEvent } from '@/sign.ts';
|
import { signEvent } from '@/sign.ts';
|
||||||
import { toStatus } from '@/transformers/nostr-to-mastoapi.ts';
|
import { toStatus } from '@/transformers/nostr-to-mastoapi.ts';
|
||||||
|
@ -77,7 +77,13 @@ const createStatusController: AppController = async (c) => {
|
||||||
created_at: nostrNow(),
|
created_at: nostrNow(),
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
publish(event);
|
try {
|
||||||
|
await pipeline.handleEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof pipeline.RelayError) {
|
||||||
|
return c.json({ error: e.message }, 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return c.json(await toStatus(event));
|
return c.json(await toStatus(event));
|
||||||
} else {
|
} else {
|
||||||
|
@ -118,7 +124,13 @@ const favouriteController: AppController = async (c) => {
|
||||||
created_at: nostrNow(),
|
created_at: nostrNow(),
|
||||||
}, c);
|
}, c);
|
||||||
|
|
||||||
publish(event);
|
try {
|
||||||
|
await pipeline.handleEvent(event);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof pipeline.RelayError) {
|
||||||
|
return c.json({ error: e.message }, 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const status = await toStatus(target);
|
const status = await toStatus(target);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as eventsDB from '@/db/events.ts';
|
import * as eventsDB from '@/db/events.ts';
|
||||||
import { findUser } from '@/db/users.ts';
|
import * as pipeline from '@/pipeline.ts';
|
||||||
import { jsonSchema } from '@/schema.ts';
|
import { jsonSchema } from '@/schema.ts';
|
||||||
import {
|
import {
|
||||||
type ClientCLOSE,
|
type ClientCLOSE,
|
||||||
|
@ -15,12 +15,14 @@ import type { Event, Filter } from '@/deps.ts';
|
||||||
/** Limit of events returned per-filter. */
|
/** Limit of events returned per-filter. */
|
||||||
const FILTER_LIMIT = 100;
|
const FILTER_LIMIT = 100;
|
||||||
|
|
||||||
|
/** NIP-01 relay to client message. */
|
||||||
type RelayMsg =
|
type RelayMsg =
|
||||||
| ['EVENT', string, Event]
|
| ['EVENT', string, Event]
|
||||||
| ['NOTICE', string]
|
| ['NOTICE', string]
|
||||||
| ['EOSE', string]
|
| ['EOSE', string]
|
||||||
| ['OK', string, boolean, string];
|
| ['OK', string, boolean, string];
|
||||||
|
|
||||||
|
/** Set up the Websocket connection. */
|
||||||
function connectStream(socket: WebSocket) {
|
function connectStream(socket: WebSocket) {
|
||||||
socket.onmessage = (e) => {
|
socket.onmessage = (e) => {
|
||||||
const result = jsonSchema.pipe(clientMsgSchema).safeParse(e.data);
|
const result = jsonSchema.pipe(clientMsgSchema).safeParse(e.data);
|
||||||
|
@ -31,6 +33,7 @@ function connectStream(socket: WebSocket) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Handle client message. */
|
||||||
function handleMsg(msg: ClientMsg) {
|
function handleMsg(msg: ClientMsg) {
|
||||||
switch (msg[0]) {
|
switch (msg[0]) {
|
||||||
case 'REQ':
|
case 'REQ':
|
||||||
|
@ -45,6 +48,7 @@ function connectStream(socket: WebSocket) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Handle REQ. Start a subscription. */
|
||||||
async function handleReq([_, sub, ...filters]: ClientREQ) {
|
async function handleReq([_, sub, ...filters]: ClientREQ) {
|
||||||
for (const event of await eventsDB.getFilters(prepareFilters(filters))) {
|
for (const event of await eventsDB.getFilters(prepareFilters(filters))) {
|
||||||
send(['EVENT', sub, event]);
|
send(['EVENT', sub, event]);
|
||||||
|
@ -52,20 +56,28 @@ function connectStream(socket: WebSocket) {
|
||||||
send(['EOSE', sub]);
|
send(['EOSE', sub]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Handle EVENT. Store the event. */
|
||||||
async function handleEvent([_, event]: ClientEVENT) {
|
async function handleEvent([_, event]: ClientEVENT) {
|
||||||
if (await findUser({ pubkey: event.pubkey })) {
|
try {
|
||||||
eventsDB.insertEvent(event);
|
// This will store it (if eligible) and run other side-effects.
|
||||||
|
await pipeline.handleEvent(event);
|
||||||
send(['OK', event.id, true, '']);
|
send(['OK', event.id, true, '']);
|
||||||
} else {
|
} catch (e) {
|
||||||
send(['OK', event.id, false, 'blocked: only registered users can post']);
|
if (e instanceof pipeline.RelayError) {
|
||||||
|
send(['OK', event.id, false, e.message]);
|
||||||
|
} else {
|
||||||
|
send(['OK', event.id, false, 'error: something went wrong']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Handle CLOSE. Close the subscription. */
|
||||||
function handleClose([_, _sub]: ClientCLOSE) {
|
function handleClose([_, _sub]: ClientCLOSE) {
|
||||||
// TODO: ???
|
// TODO: ???
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Send a message back to the client. */
|
||||||
function send(msg: RelayMsg) {
|
function send(msg: RelayMsg) {
|
||||||
return socket.send(JSON.stringify(msg));
|
return socket.send(JSON.stringify(msg));
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,16 +119,4 @@ async function getFilters<K extends number>(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns whether the pubkey is followed by a local user. */
|
export { getFilters, insertEvent };
|
||||||
async function isLocallyFollowed(pubkey: string): Promise<boolean> {
|
|
||||||
return Boolean(
|
|
||||||
await getFilterQuery({
|
|
||||||
kinds: [3],
|
|
||||||
'#p': [pubkey],
|
|
||||||
limit: 1,
|
|
||||||
local: true,
|
|
||||||
}).executeTakeFirst(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getFilters, insertEvent, isLocallyFollowed };
|
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { insertEvent, isLocallyFollowed } from '@/db/events.ts';
|
import { getActiveRelays } from '@/db/relays.ts';
|
||||||
import { addRelays, getActiveRelays } from '@/db/relays.ts';
|
|
||||||
import { findUser } from '@/db/users.ts';
|
|
||||||
import { type Event, RelayPool } from '@/deps.ts';
|
import { type Event, RelayPool } from '@/deps.ts';
|
||||||
import { trends } from '@/trends.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
import { isRelay, nostrDate, nostrNow } from '@/utils.ts';
|
|
||||||
|
import * as pipeline from './pipeline.ts';
|
||||||
|
|
||||||
const relays = await getActiveRelays();
|
const relays = await getActiveRelays();
|
||||||
const pool = new RelayPool(relays);
|
const pool = new RelayPool(relays);
|
||||||
|
@ -20,48 +19,10 @@ pool.subscribe(
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Handle events through the firehose pipeline. */
|
/** Handle events through the firehose pipeline. */
|
||||||
async function handleEvent(event: Event): Promise<void> {
|
function handleEvent(event: Event): Promise<void> {
|
||||||
console.info(`firehose: Event<${event.kind}> ${event.id}`);
|
console.info(`firehose: Event<${event.kind}> ${event.id}`);
|
||||||
|
|
||||||
trackHashtags(event);
|
return pipeline
|
||||||
trackRelays(event);
|
.handleEvent(event)
|
||||||
|
.catch(() => {});
|
||||||
if (await findUser({ pubkey: event.pubkey }) || await isLocallyFollowed(event.pubkey)) {
|
|
||||||
insertEvent(event).catch(console.warn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Track whenever a hashtag is used, for processing trending tags. */
|
|
||||||
function trackHashtags(event: Event): void {
|
|
||||||
const date = nostrDate(event.created_at);
|
|
||||||
|
|
||||||
const tags = event.tags
|
|
||||||
.filter((tag) => tag[0] === 't')
|
|
||||||
.map((tag) => tag[1])
|
|
||||||
.slice(0, 5);
|
|
||||||
|
|
||||||
if (!tags.length) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.info('tracking tags:', tags);
|
|
||||||
trends.addTagUsages(event.pubkey, tags, date);
|
|
||||||
} catch (_e) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Tracks known relays in the database. */
|
|
||||||
function trackRelays(event: Event) {
|
|
||||||
const relays = new Set<`wss://${string}`>();
|
|
||||||
|
|
||||||
event.tags.forEach((tag) => {
|
|
||||||
if (['p', 'e', 'a'].includes(tag[0]) && isRelay(tag[2])) {
|
|
||||||
relays.add(tag[2]);
|
|
||||||
}
|
|
||||||
if (event.kind === 10002 && tag[0] === 'r' && isRelay(tag[1])) {
|
|
||||||
relays.add(tag[1]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return addRelays([...relays]);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import * as eventsDB from '@/db/events.ts';
|
||||||
|
import { addRelays } from '@/db/relays.ts';
|
||||||
|
import { findUser } from '@/db/users.ts';
|
||||||
|
import { type Event } from '@/deps.ts';
|
||||||
|
import { isLocallyFollowed } from '@/queries.ts';
|
||||||
|
import { trends } from '@/trends.ts';
|
||||||
|
import { isRelay, nostrDate } from '@/utils.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common pipeline function to process (and maybe store) events.
|
||||||
|
* It is idempotent, so it can be called multiple times for the same event.
|
||||||
|
*/
|
||||||
|
async function handleEvent(event: Event): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
storeEvent(event),
|
||||||
|
trackRelays(event),
|
||||||
|
trackHashtags(event),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Maybe store the event, if eligible. */
|
||||||
|
async function storeEvent(event: Event): Promise<void> {
|
||||||
|
if (await findUser({ pubkey: event.pubkey }) || await isLocallyFollowed(event.pubkey)) {
|
||||||
|
await eventsDB.insertEvent(event).catch(console.warn);
|
||||||
|
} else {
|
||||||
|
return Promise.reject(new RelayError('blocked', 'only registered users can post'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Track whenever a hashtag is used, for processing trending tags. */
|
||||||
|
// deno-lint-ignore require-await
|
||||||
|
async function trackHashtags(event: Event): Promise<void> {
|
||||||
|
const date = nostrDate(event.created_at);
|
||||||
|
|
||||||
|
const tags = event.tags
|
||||||
|
.filter((tag) => tag[0] === 't')
|
||||||
|
.map((tag) => tag[1])
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
if (!tags.length) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.info('tracking tags:', tags);
|
||||||
|
trends.addTagUsages(event.pubkey, tags, date);
|
||||||
|
} catch (_e) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tracks known relays in the database. */
|
||||||
|
function trackRelays(event: Event) {
|
||||||
|
const relays = new Set<`wss://${string}`>();
|
||||||
|
|
||||||
|
event.tags.forEach((tag) => {
|
||||||
|
if (['p', 'e', 'a'].includes(tag[0]) && isRelay(tag[2])) {
|
||||||
|
relays.add(tag[2]);
|
||||||
|
}
|
||||||
|
if (event.kind === 10002 && tag[0] === 'r' && isRelay(tag[1])) {
|
||||||
|
relays.add(tag[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return addRelays([...relays]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** NIP-20 command line result. */
|
||||||
|
class RelayError extends Error {
|
||||||
|
constructor(prefix: 'duplicate' | 'pow' | 'blocked' | 'rate-limited' | 'invalid' | 'error', message: string) {
|
||||||
|
super(`${prefix}: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { handleEvent, RelayError };
|
|
@ -1,8 +1,8 @@
|
||||||
|
import * as eventsDB from '@/db/events.ts';
|
||||||
import { type Event, type Filter, findReplyTag } from '@/deps.ts';
|
import { type Event, type Filter, findReplyTag } from '@/deps.ts';
|
||||||
|
import * as mixer from '@/mixer.ts';
|
||||||
import { type PaginationParams } from '@/utils.ts';
|
import { type PaginationParams } from '@/utils.ts';
|
||||||
|
|
||||||
import * as mixer from './mixer.ts';
|
|
||||||
|
|
||||||
interface GetEventOpts<K extends number> {
|
interface GetEventOpts<K extends number> {
|
||||||
/** Timeout in milliseconds. */
|
/** Timeout in milliseconds. */
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
@ -83,4 +83,10 @@ function getDescendants(eventId: string): Promise<Event<1>[]> {
|
||||||
return mixer.getFilters([{ kinds: [1], '#e': [eventId] }], { limit: 200, timeout: 2000 });
|
return mixer.getFilters([{ kinds: [1], '#e': [eventId] }], { limit: 200, timeout: 2000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getAncestors, getAuthor, getDescendants, getEvent, getFeed, getFollows, getPublicFeed };
|
/** Returns whether the pubkey is followed by a local user. */
|
||||||
|
async function isLocallyFollowed(pubkey: string): Promise<boolean> {
|
||||||
|
const [event] = await eventsDB.getFilters([{ kinds: [3], '#p': [pubkey], local: true }], { limit: 1 });
|
||||||
|
return Boolean(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getAncestors, getAuthor, getDescendants, getEvent, getFeed, getFollows, getPublicFeed, isLocallyFollowed };
|
||||||
|
|
Loading…
Reference in New Issue