Merge branch 'delete' into 'develop'
Process deletions Closes #63 See merge request soapbox-pub/ditto!36
This commit is contained in:
commit
4b4bfd48c7
|
@ -1,3 +1,4 @@
|
||||||
|
import '@/cron.ts';
|
||||||
import {
|
import {
|
||||||
type Context,
|
type Context,
|
||||||
cors,
|
cors,
|
||||||
|
|
|
@ -1,32 +1,9 @@
|
||||||
import { Conf } from '@/config.ts';
|
import { type Event, type Filter, matchFilters } from '@/deps.ts';
|
||||||
import { type Event, type Filter, matchFilters, RelayPool, TTLCache } from '@/deps.ts';
|
|
||||||
import * as pipeline from '@/pipeline.ts';
|
import * as pipeline from '@/pipeline.ts';
|
||||||
import { Time } from '@/utils.ts';
|
import { allRelays, pool } from '@/pool.ts';
|
||||||
|
|
||||||
import type { GetFiltersOpts } from '@/filter.ts';
|
import type { GetFiltersOpts } from '@/filter.ts';
|
||||||
|
|
||||||
type Pool = InstanceType<typeof RelayPool>;
|
|
||||||
|
|
||||||
/** HACK: Websockets in Deno are finnicky... get a new pool every 30 minutes. */
|
|
||||||
const poolCache = new TTLCache<0, Pool>({
|
|
||||||
ttl: Time.minutes(30),
|
|
||||||
max: 2,
|
|
||||||
dispose: (pool) => {
|
|
||||||
console.log('Closing pool.');
|
|
||||||
pool.close();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function getPool(): Pool {
|
|
||||||
const cached = poolCache.get(0);
|
|
||||||
if (cached !== undefined) return cached;
|
|
||||||
|
|
||||||
console.log('Creating new pool.');
|
|
||||||
const pool = new RelayPool(Conf.poolRelays);
|
|
||||||
poolCache.set(0, pool);
|
|
||||||
return pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get events from a NIP-01 filter. */
|
/** Get events from a NIP-01 filter. */
|
||||||
function getFilters<K extends number>(filters: Filter<K>[], opts: GetFiltersOpts = {}): Promise<Event<K>[]> {
|
function getFilters<K extends number>(filters: Filter<K>[], opts: GetFiltersOpts = {}): Promise<Event<K>[]> {
|
||||||
if (!filters.length) return Promise.resolve([]);
|
if (!filters.length) return Promise.resolve([]);
|
||||||
|
@ -34,9 +11,9 @@ function getFilters<K extends number>(filters: Filter<K>[], opts: GetFiltersOpts
|
||||||
let tid: number;
|
let tid: number;
|
||||||
const results: Event[] = [];
|
const results: Event[] = [];
|
||||||
|
|
||||||
const unsub = getPool().subscribe(
|
const unsub = pool.subscribe(
|
||||||
filters,
|
filters,
|
||||||
Conf.poolRelays,
|
allRelays,
|
||||||
(event: Event | null) => {
|
(event: Event | null) => {
|
||||||
if (event && matchFilters(filters, event)) {
|
if (event && matchFilters(filters, event)) {
|
||||||
pipeline.handleEvent(event).catch(() => {});
|
pipeline.handleEvent(event).catch(() => {});
|
||||||
|
|
|
@ -58,14 +58,6 @@ const Conf = {
|
||||||
get adminEmail() {
|
get adminEmail() {
|
||||||
return Deno.env.get('ADMIN_EMAIL') || 'webmaster@localhost';
|
return Deno.env.get('ADMIN_EMAIL') || 'webmaster@localhost';
|
||||||
},
|
},
|
||||||
/** @deprecated Use relays from the database instead. */
|
|
||||||
get poolRelays() {
|
|
||||||
return (Deno.env.get('RELAY_POOL') || '').split(',').filter(Boolean);
|
|
||||||
},
|
|
||||||
/** @deprecated Publish only to the local relay unless users are mentioned, then try to also send to the relay of those users. Deletions should also be fanned out. */
|
|
||||||
get publishRelays() {
|
|
||||||
return ['wss://relay.mostr.pub'];
|
|
||||||
},
|
|
||||||
/** Domain of the Ditto server as a `URL` object, for easily grabbing the `hostname`, etc. */
|
/** Domain of the Ditto server as a `URL` object, for easily grabbing the `hostname`, etc. */
|
||||||
get url() {
|
get url() {
|
||||||
return new URL(Conf.localDomain);
|
return new URL(Conf.localDomain);
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import * as eventsDB from '@/db/events.ts';
|
||||||
|
import { cron } from '@/deps.ts';
|
||||||
|
import { Time } from '@/utils/time.ts';
|
||||||
|
|
||||||
|
/** Clean up old remote events. */
|
||||||
|
async function cleanupEvents() {
|
||||||
|
console.log('Cleaning up old remote events...');
|
||||||
|
|
||||||
|
const [result] = await eventsDB.deleteFilters([{
|
||||||
|
until: Math.floor((Date.now() - Time.days(7)) / 1000),
|
||||||
|
local: false,
|
||||||
|
}]);
|
||||||
|
|
||||||
|
console.log(`Cleaned up ${result?.numDeletedRows ?? 0} old remote events.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await cleanupEvents();
|
||||||
|
|
||||||
|
cron.every15Minute(cleanupEvents);
|
28
src/db.ts
28
src/db.ts
|
@ -29,9 +29,7 @@ interface EventFTSRow {
|
||||||
|
|
||||||
interface TagRow {
|
interface TagRow {
|
||||||
tag: string;
|
tag: string;
|
||||||
value_1: string | null;
|
value: string;
|
||||||
value_2: string | null;
|
|
||||||
value_3: string | null;
|
|
||||||
event_id: string;
|
event_id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,8 +61,26 @@ const migrator = new Migrator({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Running migrations...');
|
/** Migrate the database to the latest version. */
|
||||||
const results = await migrator.migrateToLatest();
|
async function migrate() {
|
||||||
console.log('Migrations finished:', results);
|
console.log('Running migrations...');
|
||||||
|
const results = await migrator.migrateToLatest();
|
||||||
|
|
||||||
|
if (results.error) {
|
||||||
|
console.error(results.error);
|
||||||
|
Deno.exit(1);
|
||||||
|
} else {
|
||||||
|
if (!results.results?.length) {
|
||||||
|
console.log('Everything up-to-date.');
|
||||||
|
} else {
|
||||||
|
console.log('Migrations finished!');
|
||||||
|
for (const { migrationName, status } of results.results) {
|
||||||
|
console.log(` - ${migrationName}: ${status}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await migrate();
|
||||||
|
|
||||||
export { db, type DittoDB, type EventRow, type TagRow, type UserRow };
|
export { db, type DittoDB, type EventRow, type TagRow, type UserRow };
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import event55920b75 from '~/fixtures/events/55920b75.json' assert { type: 'json' };
|
import event55920b75 from '~/fixtures/events/55920b75.json' assert { type: 'json' };
|
||||||
import { assertEquals } from '@/deps-test.ts';
|
import { assertEquals } from '@/deps-test.ts';
|
||||||
|
|
||||||
import { getFilters, insertEvent } from './events.ts';
|
import { countFilters, deleteFilters, getFilters, insertEvent } from './events.ts';
|
||||||
|
import { insertUser } from '@/db/users.ts';
|
||||||
|
|
||||||
|
Deno.test('count filters', async () => {
|
||||||
|
assertEquals(await countFilters([{ kinds: [1] }]), 0);
|
||||||
|
await insertEvent(event55920b75);
|
||||||
|
assertEquals(await countFilters([{ kinds: [1] }]), 1);
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test('insert and filter events', async () => {
|
Deno.test('insert and filter events', async () => {
|
||||||
await insertEvent(event55920b75);
|
await insertEvent(event55920b75);
|
||||||
|
@ -15,3 +22,28 @@ Deno.test('insert and filter events', async () => {
|
||||||
[event55920b75],
|
[event55920b75],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test('delete events', async () => {
|
||||||
|
await insertEvent(event55920b75);
|
||||||
|
assertEquals(await getFilters([{ kinds: [1] }]), [event55920b75]);
|
||||||
|
await deleteFilters([{ kinds: [1] }]);
|
||||||
|
assertEquals(await getFilters([{ kinds: [1] }]), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test('query events with local filter', async () => {
|
||||||
|
await insertEvent(event55920b75);
|
||||||
|
|
||||||
|
assertEquals(await getFilters([{}]), [event55920b75]);
|
||||||
|
assertEquals(await getFilters([{ local: true }]), []);
|
||||||
|
assertEquals(await getFilters([{ local: false }]), [event55920b75]);
|
||||||
|
|
||||||
|
await insertUser({
|
||||||
|
username: 'alex',
|
||||||
|
pubkey: event55920b75.pubkey,
|
||||||
|
inserted_at: new Date(),
|
||||||
|
admin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(await getFilters([{ local: true }]), [event55920b75]);
|
||||||
|
assertEquals(await getFilters([{ local: false }]), []);
|
||||||
|
});
|
||||||
|
|
|
@ -34,17 +34,14 @@ function insertEvent(event: Event): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const tagCounts: Record<string, number> = {};
|
const tagCounts: Record<string, number> = {};
|
||||||
const tags = event.tags.reduce<Insertable<TagRow>[]>((results, tag) => {
|
const tags = event.tags.reduce<Insertable<TagRow>[]>((results, [name, value]) => {
|
||||||
const tagName = tag[0];
|
tagCounts[name] = (tagCounts[name] || 0) + 1;
|
||||||
tagCounts[tagName] = (tagCounts[tagName] || 0) + 1;
|
|
||||||
|
|
||||||
if (tagConditions[tagName]?.({ event, count: tagCounts[tagName] - 1 })) {
|
if (value && tagConditions[name]?.({ event, count: tagCounts[name] - 1 })) {
|
||||||
results.push({
|
results.push({
|
||||||
event_id: event.id,
|
event_id: event.id,
|
||||||
tag: tagName,
|
tag: name,
|
||||||
value_1: tag[1] || null,
|
value,
|
||||||
value_2: tag[2] || null,
|
|
||||||
value_3: tag[3] || null,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,12 +108,14 @@ function getFilterQuery(filter: DittoFilter) {
|
||||||
query = query
|
query = query
|
||||||
.leftJoin('tags', 'tags.event_id', 'events.id')
|
.leftJoin('tags', 'tags.event_id', 'events.id')
|
||||||
.where('tags.tag', '=', tag)
|
.where('tags.tag', '=', tag)
|
||||||
.where('tags.value_1', 'in', value) as typeof query;
|
.where('tags.value', 'in', value) as typeof query;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.local) {
|
if (typeof filter.local === 'boolean') {
|
||||||
query = query.innerJoin('users', 'users.pubkey', 'events.pubkey');
|
query = filter.local
|
||||||
|
? query.innerJoin('users', 'users.pubkey', 'events.pubkey') as typeof query
|
||||||
|
: query.leftJoin('users', 'users.pubkey', 'events.pubkey').where('users.pubkey', 'is', null) as typeof query;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.search) {
|
if (filter.search) {
|
||||||
|
@ -128,13 +127,20 @@ function getFilterQuery(filter: DittoFilter) {
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Combine filter queries into a single union query. */
|
||||||
|
function getFiltersQuery(filters: DittoFilter[]) {
|
||||||
|
return filters
|
||||||
|
.map(getFilterQuery)
|
||||||
|
.reduce((result, query) => result.union(query));
|
||||||
|
}
|
||||||
|
|
||||||
/** Get events for filters from the database. */
|
/** Get events for filters from the database. */
|
||||||
async function getFilters<K extends number>(
|
async function getFilters<K extends number>(
|
||||||
filters: DittoFilter<K>[],
|
filters: DittoFilter<K>[],
|
||||||
opts: GetFiltersOpts = {},
|
opts: GetFiltersOpts = {},
|
||||||
): Promise<Event<K>[]> {
|
): Promise<Event<K>[]> {
|
||||||
if (!filters.length) return Promise.resolve([]);
|
if (!filters.length) return Promise.resolve([]);
|
||||||
let query = filters.map(getFilterQuery).reduce((acc, curr) => acc.union(curr));
|
let query = getFiltersQuery(filters);
|
||||||
|
|
||||||
if (typeof opts.limit === 'number') {
|
if (typeof opts.limit === 'number') {
|
||||||
query = query.limit(opts.limit);
|
query = query.limit(opts.limit);
|
||||||
|
@ -145,10 +151,27 @@ async function getFilters<K extends number>(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Delete events based on filters from the database. */
|
||||||
|
function deleteFilters<K extends number>(filters: DittoFilter<K>[]) {
|
||||||
|
if (!filters.length) return Promise.resolve([]);
|
||||||
|
|
||||||
|
return db.transaction().execute(async (trx) => {
|
||||||
|
const query = getFiltersQuery(filters).clearSelect().select('id');
|
||||||
|
|
||||||
|
await trx.deleteFrom('events_fts')
|
||||||
|
.where('id', 'in', () => query)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return trx.deleteFrom('events')
|
||||||
|
.where('id', 'in', () => query)
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Get number of events that would be returned by filters. */
|
/** Get number of events that would be returned by filters. */
|
||||||
async function countFilters<K extends number>(filters: DittoFilter<K>[]): Promise<number> {
|
async function countFilters<K extends number>(filters: DittoFilter<K>[]): Promise<number> {
|
||||||
if (!filters.length) return Promise.resolve(0);
|
if (!filters.length) return Promise.resolve(0);
|
||||||
const query = filters.map(getFilterQuery).reduce((acc, curr) => acc.union(curr));
|
const query = getFiltersQuery(filters);
|
||||||
|
|
||||||
const [{ count }] = await query
|
const [{ count }] = await query
|
||||||
.clearSelect()
|
.clearSelect()
|
||||||
|
@ -176,4 +199,4 @@ function buildUserSearchContent(event: Event<0>): string {
|
||||||
return [name, nip05, about].filter(Boolean).join('\n');
|
return [name, nip05, about].filter(Boolean).join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
export { countFilters, getFilters, insertEvent };
|
export { countFilters, deleteFilters, getFilters, insertEvent };
|
||||||
|
|
|
@ -8,5 +8,5 @@ export async function up(db: Kysely<any>): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>): Promise<void> {
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
await db.schema.dropTable('relays').execute();
|
await db.schema.alterTable('users').dropColumn('admin').execute();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Kysely } from '@/deps.ts';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createIndex('idx_users_pubkey')
|
||||||
|
.on('users')
|
||||||
|
.column('pubkey')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('idx_users_username')
|
||||||
|
.on('users')
|
||||||
|
.column('username')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropIndex('idx_users_pubkey').execute();
|
||||||
|
await db.schema.dropIndex('idx_users_username').execute();
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { Kysely, sql } from '@/deps.ts';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.createTable('tags_new')
|
||||||
|
.addColumn('tag', 'text', (col) => col.notNull())
|
||||||
|
.addColumn('value', 'text', (col) => col.notNull())
|
||||||
|
.addColumn('event_id', 'text', (col) => col.references('events.id').onDelete('cascade'))
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
INSERT INTO tags_new (tag, value, event_id)
|
||||||
|
SELECT tag, value_1 as value, event_id
|
||||||
|
FROM tags
|
||||||
|
WHERE value_1 IS NOT NULL
|
||||||
|
`.execute(db);
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.dropTable('tags')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.alterTable('tags_new')
|
||||||
|
.renameTo('tags').execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('idx_tags_tag')
|
||||||
|
.on('tags')
|
||||||
|
.column('tag')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('idx_tags_value')
|
||||||
|
.on('tags')
|
||||||
|
.column('value')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('idx_tags_event_id')
|
||||||
|
.on('tags')
|
||||||
|
.column('event_id')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.dropTable('tags').execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createTable('tags')
|
||||||
|
.addColumn('tag', 'text', (col) => col.notNull())
|
||||||
|
.addColumn('value_1', 'text')
|
||||||
|
.addColumn('value_2', 'text')
|
||||||
|
.addColumn('value_3', 'text')
|
||||||
|
.addColumn('event_id', 'text', (col) => col.notNull())
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('idx_tags_tag')
|
||||||
|
.on('tags')
|
||||||
|
.column('tag')
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema
|
||||||
|
.createIndex('idx_tags_value_1')
|
||||||
|
.on('tags')
|
||||||
|
.column('value_1')
|
||||||
|
.execute();
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Kysely, sql } from '@/deps.ts';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`PRAGMA foreign_keys = ON`.execute(db);
|
||||||
|
await sql`PRAGMA auto_vacuum = FULL`.execute(db);
|
||||||
|
await sql`VACUUM`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`PRAGMA foreign_keys = OFF`.execute(db);
|
||||||
|
await sql`PRAGMA auto_vacuum = NONE`.execute(db);
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ function addRelays(relays: `wss://${string}`[]) {
|
||||||
if (!relays.length) return Promise.resolve();
|
if (!relays.length) return Promise.resolve();
|
||||||
|
|
||||||
const values = relays.map((url) => ({
|
const values = relays.map((url) => ({
|
||||||
url,
|
url: new URL(url).toString(),
|
||||||
domain: tldts.getDomain(url)!,
|
domain: tldts.getDomain(url)!,
|
||||||
active: true,
|
active: true,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -65,5 +65,6 @@ export {
|
||||||
} from 'npm:kysely@^0.25.0';
|
} from 'npm:kysely@^0.25.0';
|
||||||
export { DenoSqliteDialect } from 'https://gitlab.com/soapbox-pub/kysely-deno-sqlite/-/raw/v1.0.1/mod.ts';
|
export { DenoSqliteDialect } from 'https://gitlab.com/soapbox-pub/kysely-deno-sqlite/-/raw/v1.0.1/mod.ts';
|
||||||
export { default as tldts } from 'npm:tldts@^6.0.14';
|
export { default as tldts } from 'npm:tldts@^6.0.14';
|
||||||
|
export * as cron from 'https://deno.land/x/deno_cron@v1.0.0/cron.ts';
|
||||||
|
|
||||||
export type * as TypeFest from 'npm:type-fest@^4.3.0';
|
export type * as TypeFest from 'npm:type-fest@^4.3.0';
|
||||||
|
|
|
@ -1,18 +1,15 @@
|
||||||
import { getActiveRelays } from '@/db/relays.ts';
|
import { type Event } from '@/deps.ts';
|
||||||
import { type Event, RelayPool } from '@/deps.ts';
|
import { allRelays, pool } from '@/pool.ts';
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
|
|
||||||
import * as pipeline from './pipeline.ts';
|
import * as pipeline from './pipeline.ts';
|
||||||
|
|
||||||
const relays = await getActiveRelays();
|
|
||||||
const pool = new RelayPool(relays);
|
|
||||||
|
|
||||||
// This file watches events on all known relays and performs
|
// This file watches events on all known relays and performs
|
||||||
// side-effects based on them, such as trending hashtag tracking
|
// side-effects based on them, such as trending hashtag tracking
|
||||||
// and storing events for notifications and the home feed.
|
// and storing events for notifications and the home feed.
|
||||||
pool.subscribe(
|
pool.subscribe(
|
||||||
[{ kinds: [0, 1, 3, 5, 6, 7, 10002], limit: 0, since: nostrNow() }],
|
[{ kinds: [0, 1, 3, 5, 6, 7, 10002], limit: 0, since: nostrNow() }],
|
||||||
relays,
|
allRelays,
|
||||||
handleEvent,
|
handleEvent,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
|
|
|
@ -4,10 +4,13 @@ import { addRelays } from '@/db/relays.ts';
|
||||||
import { findUser } from '@/db/users.ts';
|
import { findUser } from '@/db/users.ts';
|
||||||
import { type Event, LRUCache } from '@/deps.ts';
|
import { type Event, LRUCache } from '@/deps.ts';
|
||||||
import { isEphemeralKind } from '@/kinds.ts';
|
import { isEphemeralKind } from '@/kinds.ts';
|
||||||
|
import * as mixer from '@/mixer.ts';
|
||||||
|
import { publish } from '@/pool.ts';
|
||||||
import { isLocallyFollowed } from '@/queries.ts';
|
import { isLocallyFollowed } from '@/queries.ts';
|
||||||
import { Sub } from '@/subs.ts';
|
import { Sub } from '@/subs.ts';
|
||||||
|
import { getTagSet } from '@/tags.ts';
|
||||||
import { trends } from '@/trends.ts';
|
import { trends } from '@/trends.ts';
|
||||||
import { isRelay, nostrDate, nostrNow, Time } from '@/utils.ts';
|
import { eventAge, isRelay, nostrDate, Time } from '@/utils.ts';
|
||||||
|
|
||||||
import type { EventData } from '@/types.ts';
|
import type { EventData } from '@/types.ts';
|
||||||
|
|
||||||
|
@ -21,9 +24,11 @@ async function handleEvent(event: Event): Promise<void> {
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
storeEvent(event, data),
|
storeEvent(event, data),
|
||||||
|
processDeletions(event),
|
||||||
trackRelays(event),
|
trackRelays(event),
|
||||||
trackHashtags(event),
|
trackHashtags(event),
|
||||||
streamOut(event, data),
|
streamOut(event, data),
|
||||||
|
broadcast(event, data),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,16 +54,39 @@ const isAdminEvent = ({ pubkey }: Event): boolean => pubkey === Conf.pubkey;
|
||||||
/** Maybe store the event, if eligible. */
|
/** Maybe store the event, if eligible. */
|
||||||
async function storeEvent(event: Event, data: EventData): Promise<void> {
|
async function storeEvent(event: Event, data: EventData): Promise<void> {
|
||||||
if (isEphemeralKind(event.kind)) return;
|
if (isEphemeralKind(event.kind)) return;
|
||||||
|
|
||||||
if (data.user || isAdminEvent(event) || await isLocallyFollowed(event.pubkey)) {
|
if (data.user || isAdminEvent(event) || await isLocallyFollowed(event.pubkey)) {
|
||||||
await eventsDB.insertEvent(event).catch(console.warn);
|
const [deletion] = await mixer.getFilters(
|
||||||
|
[{ kinds: [5], authors: [event.pubkey], '#e': [event.id], limit: 1 }],
|
||||||
|
{ limit: 1, timeout: Time.seconds(1) },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (deletion) {
|
||||||
|
return Promise.reject(new RelayError('blocked', 'event was deleted'));
|
||||||
|
} else {
|
||||||
|
await eventsDB.insertEvent(event).catch(console.warn);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(new RelayError('blocked', 'only registered users can post'));
|
return Promise.reject(new RelayError('blocked', 'only registered users can post'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Query to-be-deleted events, ensure their pubkey matches, then delete them from the database. */
|
||||||
|
async function processDeletions(event: Event): Promise<void> {
|
||||||
|
if (event.kind === 5) {
|
||||||
|
const ids = getTagSet(event.tags, 'e');
|
||||||
|
const events = await eventsDB.getFilters([{ ids: [...ids] }]);
|
||||||
|
|
||||||
|
const deleteIds = events
|
||||||
|
.filter(({ pubkey, id }) => pubkey === event.pubkey && ids.has(id))
|
||||||
|
.map((event) => event.id);
|
||||||
|
|
||||||
|
await eventsDB.deleteFilters([{ ids: deleteIds }]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Track whenever a hashtag is used, for processing trending tags. */
|
/** Track whenever a hashtag is used, for processing trending tags. */
|
||||||
// deno-lint-ignore require-await
|
function trackHashtags(event: Event): void {
|
||||||
async function trackHashtags(event: Event): Promise<void> {
|
|
||||||
const date = nostrDate(event.created_at);
|
const date = nostrDate(event.created_at);
|
||||||
|
|
||||||
const tags = event.tags
|
const tags = event.tags
|
||||||
|
@ -93,7 +121,7 @@ function trackRelays(event: Event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Determine if the event is being received in a timely manner. */
|
/** Determine if the event is being received in a timely manner. */
|
||||||
const isFresh = ({ created_at }: Event): boolean => created_at >= nostrNow() - Time.seconds(10);
|
const isFresh = (event: Event): boolean => eventAge(event) < Time.seconds(10);
|
||||||
|
|
||||||
/** Distribute the event through active subscriptions. */
|
/** Distribute the event through active subscriptions. */
|
||||||
function streamOut(event: Event, data: EventData) {
|
function streamOut(event: Event, data: EventData) {
|
||||||
|
@ -104,6 +132,18 @@ function streamOut(event: Event, data: EventData) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish the event to other relays.
|
||||||
|
* This should only be done in certain circumstances, like mentioning a user or publishing deletions.
|
||||||
|
*/
|
||||||
|
function broadcast(event: Event, data: EventData) {
|
||||||
|
if (!data.user || !isFresh(event)) return;
|
||||||
|
|
||||||
|
if (event.kind === 5) {
|
||||||
|
publish(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** NIP-20 command line result. */
|
/** NIP-20 command line result. */
|
||||||
class RelayError extends Error {
|
class RelayError extends Error {
|
||||||
constructor(prefix: 'duplicate' | 'pow' | 'blocked' | 'rate-limited' | 'invalid' | 'error', message: string) {
|
constructor(prefix: 'duplicate' | 'pow' | 'blocked' | 'rate-limited' | 'invalid' | 'error', message: string) {
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { getActiveRelays } from '@/db/relays.ts';
|
||||||
|
import { type Event, RelayPool } from '@/deps.ts';
|
||||||
|
|
||||||
|
const allRelays = await getActiveRelays();
|
||||||
|
const pool = new RelayPool(allRelays);
|
||||||
|
|
||||||
|
/** Publish an event to the given relays, or the entire pool. */
|
||||||
|
function publish(event: Event, relays: string[] = allRelays) {
|
||||||
|
return pool.publish(event, relays);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { allRelays, pool, publish };
|
|
@ -0,0 +1,14 @@
|
||||||
|
/** Get the values for a tag in a `Set`. */
|
||||||
|
function getTagSet(tags: string[][], tagName: string): Set<string> {
|
||||||
|
const set = new Set<string>();
|
||||||
|
|
||||||
|
tags.forEach((tag) => {
|
||||||
|
if (tag[0] === tagName) {
|
||||||
|
set.add(tag[1]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getTagSet };
|
Loading…
Reference in New Issue