✨ Fully rewrite relay
This commit is contained in:
parent
190e38dfc1
commit
20ffbd4c6d
47 changed files with 3489 additions and 128 deletions
314
src/commands/ccn.ts
Normal file
314
src/commands/ccn.ts
Normal file
|
@ -0,0 +1,314 @@
|
|||
import type { Database } from 'jsr:@db/sqlite';
|
||||
import { encodeBase64 } from 'jsr:@std/encoding@0.224/base64';
|
||||
import { hexToBytes } from '@noble/ciphers/utils';
|
||||
import * as nostrTools from '@nostr/tools';
|
||||
import type { UserConnection } from '../UserConnection.ts';
|
||||
import { handleSocketError } from '../index.ts';
|
||||
import { createNewCCN } from '../utils/createNewCCN.ts';
|
||||
import { encryptUint8Array, encryptionKey } from '../utils/encryption.ts';
|
||||
import { getEveFilePath } from '../utils/files.ts';
|
||||
import { getAllCCNs } from '../utils/getAllCCNs.ts';
|
||||
import { log } from '../utils/logs.ts';
|
||||
import { sql } from '../utils/queries.ts';
|
||||
import {
|
||||
SecurityEventType,
|
||||
SecuritySeverity,
|
||||
logAuthEvent,
|
||||
logSecurityEvent,
|
||||
} from '../utils/securityLogs.ts';
|
||||
|
||||
function activateCCN(database: Database, pubkey: string): void {
|
||||
sql`UPDATE ccns SET is_active = 0`(database);
|
||||
sql`UPDATE ccns SET is_active = 1 WHERE pubkey = ${pubkey}`(database);
|
||||
|
||||
logAuthEvent(SecurityEventType.CCN_ACTIVATION_ATTEMPT, true, {
|
||||
ccn_pubkey: pubkey,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCreateCCN(
|
||||
connection: UserConnection,
|
||||
data: { name: string; seed?: string; creator: string },
|
||||
): Promise<void> {
|
||||
log.debug('start', { tag: 'handleCreateCCN', data });
|
||||
|
||||
logSecurityEvent({
|
||||
eventType: SecurityEventType.CCN_CREATION_ATTEMPT,
|
||||
severity: SecuritySeverity.MEDIUM,
|
||||
source: 'ccn_management',
|
||||
details: {
|
||||
ccn_name: data.name,
|
||||
creator: data.creator,
|
||||
has_seed: !!data.seed,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
if (!data.name || typeof data.name !== 'string') {
|
||||
logSecurityEvent({
|
||||
eventType: SecurityEventType.CCN_CREATION_ATTEMPT,
|
||||
severity: SecuritySeverity.MEDIUM,
|
||||
source: 'ccn_management',
|
||||
details: {
|
||||
error: 'invalid_name',
|
||||
name_provided: !!data.name,
|
||||
name_type: typeof data.name,
|
||||
},
|
||||
});
|
||||
|
||||
connection.sendNotice('Name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data.creator || typeof data.creator !== 'string') {
|
||||
logSecurityEvent({
|
||||
eventType: SecurityEventType.CCN_CREATION_ATTEMPT,
|
||||
severity: SecuritySeverity.MEDIUM,
|
||||
source: 'ccn_management',
|
||||
details: {
|
||||
error: 'invalid_creator',
|
||||
creator_provided: !!data.creator,
|
||||
creator_type: typeof data.creator,
|
||||
},
|
||||
});
|
||||
|
||||
connection.sendNotice('Creator is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const newCcn = await createNewCCN(
|
||||
connection.db,
|
||||
data.name,
|
||||
data.creator,
|
||||
data.seed,
|
||||
);
|
||||
log.debug('created new CCN', {
|
||||
tag: 'handleCreateCCN',
|
||||
pubkey: newCcn.pubkey,
|
||||
});
|
||||
activateCCN(connection.db, newCcn.pubkey);
|
||||
log.debug('activated new CCN', {
|
||||
tag: 'handleCreateCCN',
|
||||
pubkey: newCcn.pubkey,
|
||||
});
|
||||
|
||||
logSecurityEvent({
|
||||
eventType: SecurityEventType.CCN_CREATION_ATTEMPT,
|
||||
severity: SecuritySeverity.LOW,
|
||||
source: 'ccn_management',
|
||||
details: {
|
||||
success: true,
|
||||
ccn_pubkey: newCcn.pubkey,
|
||||
ccn_name: data.name,
|
||||
creator: data.creator,
|
||||
},
|
||||
});
|
||||
|
||||
connection.sendResponse([
|
||||
'OK',
|
||||
'CCN CREATED',
|
||||
true,
|
||||
JSON.stringify({
|
||||
pubkey: newCcn.pubkey,
|
||||
name: data.name,
|
||||
}),
|
||||
]);
|
||||
|
||||
log.info('CCN created', data);
|
||||
} catch (error: unknown) {
|
||||
log.error('error', { tag: 'handleCreateCCN', error });
|
||||
|
||||
logSecurityEvent({
|
||||
eventType: SecurityEventType.CCN_CREATION_ATTEMPT,
|
||||
severity: SecuritySeverity.HIGH,
|
||||
source: 'ccn_management',
|
||||
details: {
|
||||
success: false,
|
||||
error_message: error instanceof Error ? error.message : 'Unknown error',
|
||||
ccn_name: data.name,
|
||||
creator: data.creator,
|
||||
},
|
||||
});
|
||||
|
||||
handleSocketError(connection, 'create CCN', error);
|
||||
}
|
||||
log.debug('end', { tag: 'handleCreateCCN' });
|
||||
}
|
||||
|
||||
function handleGetCCNs(connection: UserConnection): void {
|
||||
try {
|
||||
const ccns = getAllCCNs(connection.db);
|
||||
connection.sendResponse(['OK', 'CCN LIST', true, JSON.stringify(ccns)]);
|
||||
} catch (error: unknown) {
|
||||
handleSocketError(connection, 'get CCNs', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleActivateCCN(
|
||||
connection: UserConnection,
|
||||
data: { pubkey: string },
|
||||
): void {
|
||||
log.debug('start', { tag: 'handleActivateCCN', data });
|
||||
try {
|
||||
if (!data.pubkey || typeof data.pubkey !== 'string') {
|
||||
connection.sendNotice('CCN pubkey is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const ccnExists = sql`
|
||||
SELECT COUNT(*) as count FROM ccns WHERE pubkey = ${data.pubkey}
|
||||
`(connection.db)[0].count;
|
||||
|
||||
if (ccnExists === 0) {
|
||||
connection.sendNotice('CCN not found');
|
||||
log.debug('CCN not found', {
|
||||
tag: 'handleActivateCCN',
|
||||
pubkey: data.pubkey,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const subscriptionId of connection.subscriptions.keys()) {
|
||||
connection.sendResponse([
|
||||
'CLOSED',
|
||||
subscriptionId,
|
||||
'Subscription closed due to CCN activation',
|
||||
]);
|
||||
log.debug('closed subscription', {
|
||||
tag: 'handleActivateCCN',
|
||||
subscriptionId,
|
||||
});
|
||||
}
|
||||
|
||||
connection.subscriptions.clear();
|
||||
log.info('All subscriptions cleared due to CCN activation', {});
|
||||
|
||||
activateCCN(connection.db, data.pubkey);
|
||||
log.debug('activated CCN', {
|
||||
tag: 'handleActivateCCN',
|
||||
pubkey: data.pubkey,
|
||||
});
|
||||
|
||||
const activatedCCN = sql`
|
||||
SELECT pubkey, name FROM ccns WHERE pubkey = ${data.pubkey}
|
||||
`(connection.db)[0];
|
||||
|
||||
connection.sendResponse([
|
||||
'OK',
|
||||
'CCN ACTIVATED',
|
||||
true,
|
||||
JSON.stringify(activatedCCN),
|
||||
]);
|
||||
|
||||
log.info(`CCN activated: ${activatedCCN.name}`, {});
|
||||
} catch (error: unknown) {
|
||||
log.error('error', { tag: 'handleActivateCCN', error });
|
||||
handleSocketError(connection, 'activate CCN', error);
|
||||
}
|
||||
log.debug('end', { tag: 'handleActivateCCN' });
|
||||
}
|
||||
|
||||
async function handleAddCCN(
|
||||
connection: UserConnection,
|
||||
data: { name: string; allowedPubkeys: string[]; privateKey: string },
|
||||
): Promise<void> {
|
||||
log.debug('start', { tag: 'handleAddCCN', data });
|
||||
try {
|
||||
if (!data.privateKey || typeof data.privateKey !== 'string') {
|
||||
connection.sendNotice('CCN private key is required');
|
||||
return;
|
||||
}
|
||||
|
||||
const privateKeyBytes = hexToBytes(data.privateKey);
|
||||
const pubkey = nostrTools.getPublicKey(privateKeyBytes);
|
||||
log.debug('derived pubkey', { tag: 'handleAddCCN', pubkey });
|
||||
|
||||
const ccnExists = sql`
|
||||
SELECT COUNT(*) as count FROM ccns WHERE pubkey = ${pubkey}
|
||||
`(connection.db)[0].count;
|
||||
|
||||
if (ccnExists > 0) {
|
||||
connection.sendNotice('CCN already exists');
|
||||
log.debug('CCN already exists', {
|
||||
tag: 'handleAddCCN',
|
||||
pubkey,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const ccnPublicKey = nostrTools.getPublicKey(privateKeyBytes);
|
||||
const ccnPrivPath = await getEveFilePath(`ccn_keys/${ccnPublicKey}`);
|
||||
const encryptedPrivateKey = encryptUint8Array(
|
||||
privateKeyBytes,
|
||||
encryptionKey,
|
||||
);
|
||||
Deno.writeTextFileSync(ccnPrivPath, encodeBase64(encryptedPrivateKey));
|
||||
|
||||
connection.db.run('BEGIN TRANSACTION');
|
||||
log.debug('begin transaction', { tag: 'handleAddCCN' });
|
||||
|
||||
sql`INSERT INTO ccns (pubkey, name) VALUES (${ccnPublicKey}, ${data.name})`(
|
||||
connection.db,
|
||||
);
|
||||
for (const allowedPubkey of data.allowedPubkeys)
|
||||
sql`INSERT INTO allowed_writes (ccn_pubkey, pubkey) VALUES (${ccnPublicKey}, ${allowedPubkey})`(
|
||||
connection.db,
|
||||
);
|
||||
|
||||
connection.db.run('COMMIT TRANSACTION');
|
||||
log.debug('committed transaction', { tag: 'handleAddCCN' });
|
||||
activateCCN(connection.db, ccnPublicKey);
|
||||
log.debug('activated CCN', {
|
||||
tag: 'handleAddCCN',
|
||||
pubkey: ccnPublicKey,
|
||||
});
|
||||
|
||||
connection.sendResponse([
|
||||
'OK',
|
||||
'CCN ADDED',
|
||||
true,
|
||||
JSON.stringify({
|
||||
pubkey: ccnPublicKey,
|
||||
name: 'New CCN',
|
||||
}),
|
||||
]);
|
||||
} catch (error: unknown) {
|
||||
log.error('error', { tag: 'handleAddCCN', error });
|
||||
handleSocketError(connection, 'ADD CCN', error);
|
||||
}
|
||||
log.debug('end', { tag: 'handleAddCCN' });
|
||||
}
|
||||
|
||||
function handleCCNCommands(
|
||||
connection: UserConnection,
|
||||
command: string,
|
||||
data: unknown,
|
||||
) {
|
||||
switch (command) {
|
||||
case 'CREATE':
|
||||
return handleCreateCCN(
|
||||
connection,
|
||||
data as { name: string; seed?: string; creator: string },
|
||||
);
|
||||
case 'ADD':
|
||||
return handleAddCCN(
|
||||
connection,
|
||||
data as { name: string; allowedPubkeys: string[]; privateKey: string },
|
||||
);
|
||||
case 'LIST':
|
||||
return handleGetCCNs(connection);
|
||||
case 'ACTIVATE':
|
||||
return handleActivateCCN(connection, data as { pubkey: string });
|
||||
default:
|
||||
return log.warn('Invalid CCN command', {});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
activateCCN,
|
||||
handleActivateCCN,
|
||||
handleAddCCN,
|
||||
handleCCNCommands,
|
||||
handleCreateCCN,
|
||||
handleGetCCNs,
|
||||
};
|
15
src/commands/close.ts
Normal file
15
src/commands/close.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { UserConnection } from '../UserConnection.ts';
|
||||
import { log } from '../utils/logs.ts';
|
||||
|
||||
export function handleClose(
|
||||
connection: UserConnection,
|
||||
subscriptionId: string,
|
||||
) {
|
||||
if (!connection.subscriptions.has(subscriptionId)) {
|
||||
return log.warn(
|
||||
`Closing unknown subscription? That's weird. Subscription ID: ${subscriptionId}`,
|
||||
);
|
||||
}
|
||||
|
||||
connection.subscriptions.delete(subscriptionId);
|
||||
}
|
85
src/commands/event.ts
Normal file
85
src/commands/event.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import * as nostrTools from '@nostr/tools';
|
||||
import type { UserConnection } from '../UserConnection.ts';
|
||||
import { addEventToDb } from '../dbEvents/addEventToDb.ts';
|
||||
import {
|
||||
EventAlreadyExistsException,
|
||||
createEncryptedEvent,
|
||||
} from '../eventEncryptionDecryption.ts';
|
||||
import { filtersMatchingEvent } from '../utils/filtersMatchingEvent.ts';
|
||||
import { getActiveCCN } from '../utils/getActiveCCN.ts';
|
||||
import { isArray } from '../utils/isArray.ts';
|
||||
import { log } from '../utils/logs.ts';
|
||||
import { queueEventForTransmission } from '../utils/outboundQueue.ts';
|
||||
|
||||
export async function handleEvent(
|
||||
connection: UserConnection,
|
||||
event: nostrTools.Event,
|
||||
) {
|
||||
log.debug('start', { tag: 'handleEvent', eventId: event.id });
|
||||
const valid = nostrTools.verifyEvent(event);
|
||||
if (!valid) {
|
||||
connection.sendNotice('Invalid event');
|
||||
return log.warn('Invalid event', { tag: 'handleEvent' });
|
||||
}
|
||||
|
||||
const activeCCN = getActiveCCN(connection.db);
|
||||
if (!activeCCN) {
|
||||
connection.sendNotice('No active CCN found');
|
||||
return log.warn('No active CCN found', { tag: 'handleEvent' });
|
||||
}
|
||||
|
||||
const encryptedEvent = await createEncryptedEvent(event, connection.db);
|
||||
try {
|
||||
if (isArray(encryptedEvent)) {
|
||||
log.debug('adding chunked event to database', {
|
||||
tag: 'handleEvent',
|
||||
});
|
||||
addEventToDb(connection.db, event, encryptedEvent[0], activeCCN.pubkey);
|
||||
} else {
|
||||
addEventToDb(connection.db, event, encryptedEvent, activeCCN.pubkey);
|
||||
}
|
||||
|
||||
queueEventForTransmission(
|
||||
connection.db,
|
||||
event.id,
|
||||
encryptedEvent,
|
||||
activeCCN.pubkey,
|
||||
);
|
||||
|
||||
log.debug('event queued for transmission', {
|
||||
tag: 'handleEvent',
|
||||
eventId: event.id,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof EventAlreadyExistsException) {
|
||||
log.warn('Event already exists');
|
||||
return;
|
||||
}
|
||||
if (e instanceof Error)
|
||||
log.error('error adding event', {
|
||||
tag: 'handleEvent',
|
||||
error: e.stack,
|
||||
});
|
||||
else
|
||||
log.error('error adding event', {
|
||||
tag: 'handleEvent',
|
||||
error: String(e),
|
||||
});
|
||||
}
|
||||
|
||||
connection.sendOK(event.id, true, 'Event added');
|
||||
log.debug('sent OK', { tag: 'handleEvent', eventId: event.id });
|
||||
|
||||
const filtersThatMatchEvent = filtersMatchingEvent(event, connection);
|
||||
|
||||
for (let i = 0; i < filtersThatMatchEvent.length; i++) {
|
||||
const filter = filtersThatMatchEvent[i];
|
||||
connection.sendEvent(filter, event);
|
||||
log.debug('sent event to filter', {
|
||||
tag: 'handleEvent',
|
||||
filter,
|
||||
eventId: event.id,
|
||||
});
|
||||
}
|
||||
log.debug('end', { tag: 'handleEvent', eventId: event.id });
|
||||
}
|
291
src/commands/request.ts
Normal file
291
src/commands/request.ts
Normal file
|
@ -0,0 +1,291 @@
|
|||
import type { NostrClientREQ } from 'jsr:@nostrify/types';
|
||||
import type { UserConnection } from '../UserConnection.ts';
|
||||
import { isCCNReplaceableEvent } from '../utils/eventTypes.ts';
|
||||
import { getActiveCCN } from '../utils/getActiveCCN.ts';
|
||||
import { log } from '../utils/logs.ts';
|
||||
import { parseATagQuery } from '../utils/parseATagQuery.ts';
|
||||
import { mixQuery, sql, sqlPartial } from '../utils/queries.ts';
|
||||
|
||||
export function handleRequest(
|
||||
connection: UserConnection,
|
||||
request: NostrClientREQ,
|
||||
) {
|
||||
log.debug('start', { tag: 'handleRequest', request });
|
||||
const [, subscriptionId, ...filters] = request;
|
||||
if (connection.subscriptions.has(subscriptionId)) {
|
||||
return log.warn('Duplicate subscription ID', {
|
||||
tag: 'handleRequest',
|
||||
});
|
||||
}
|
||||
|
||||
log.info(
|
||||
`New subscription: ${subscriptionId} with filters: ${JSON.stringify(
|
||||
filters,
|
||||
)}`,
|
||||
);
|
||||
|
||||
const activeCCN = getActiveCCN(connection.db);
|
||||
if (!activeCCN) {
|
||||
connection.sendNotice('No active CCN found');
|
||||
return log.warn('No active CCN found', { tag: 'handleRequest' });
|
||||
}
|
||||
|
||||
let query = sqlPartial`SELECT * FROM events WHERE replaced = 0 AND deleted = 0 AND ccn_pubkey = ${activeCCN.pubkey}`;
|
||||
|
||||
let minLimit: number | null = null;
|
||||
for (const filter of filters) {
|
||||
if (filter.limit && filter.limit > 0) {
|
||||
minLimit =
|
||||
minLimit === null ? filter.limit : Math.min(minLimit, filter.limit);
|
||||
}
|
||||
}
|
||||
|
||||
const filtersAreNotEmpty = filters.some((filter) => {
|
||||
return Object.values(filter).some((value) => {
|
||||
return value.length > 0;
|
||||
});
|
||||
});
|
||||
|
||||
if (filtersAreNotEmpty) {
|
||||
query = mixQuery(query, sqlPartial`AND`);
|
||||
|
||||
for (let i = 0; i < filters.length; i++) {
|
||||
// filters act as OR, filter groups act as AND
|
||||
query = mixQuery(query, sqlPartial`(`);
|
||||
|
||||
const filter = Object.entries(filters[i]).filter(([type, value]) => {
|
||||
if (type === 'ids') return value.length > 0;
|
||||
if (type === 'authors') return value.length > 0;
|
||||
if (type === 'kinds') return value.length > 0;
|
||||
if (type.startsWith('#')) return value.length > 0;
|
||||
if (type === 'since') return value > 0;
|
||||
if (type === 'until') return value > 0;
|
||||
if (type === 'limit') return value > 0;
|
||||
return false;
|
||||
});
|
||||
|
||||
const filterWithoutLimit = filter.filter(([type]) => type !== 'limit');
|
||||
|
||||
for (let j = 0; j < filter.length; j++) {
|
||||
const [type, value] = filter[j];
|
||||
|
||||
if (type === 'ids') {
|
||||
const uniqueIds = [...new Set(value)];
|
||||
query = mixQuery(query, sqlPartial`(`);
|
||||
for (let k = 0; k < uniqueIds.length; k++) {
|
||||
const id = uniqueIds[k] as string;
|
||||
|
||||
query = mixQuery(query, sqlPartial`(id = ${id})`);
|
||||
|
||||
if (k < uniqueIds.length - 1) {
|
||||
query = mixQuery(query, sqlPartial`OR`);
|
||||
}
|
||||
}
|
||||
query = mixQuery(query, sqlPartial`)`);
|
||||
}
|
||||
|
||||
if (type === 'authors') {
|
||||
const uniqueAuthors = [...new Set(value)];
|
||||
query = mixQuery(query, sqlPartial`(`);
|
||||
for (let k = 0; k < uniqueAuthors.length; k++) {
|
||||
const author = uniqueAuthors[k] as string;
|
||||
|
||||
query = mixQuery(query, sqlPartial`(pubkey = ${author})`);
|
||||
|
||||
if (k < uniqueAuthors.length - 1) {
|
||||
query = mixQuery(query, sqlPartial`OR`);
|
||||
}
|
||||
}
|
||||
query = mixQuery(query, sqlPartial`)`);
|
||||
}
|
||||
|
||||
if (type === 'kinds') {
|
||||
const uniqueKinds = [...new Set(value)];
|
||||
query = mixQuery(query, sqlPartial`(`);
|
||||
for (let k = 0; k < uniqueKinds.length; k++) {
|
||||
const kind = uniqueKinds[k] as number;
|
||||
|
||||
query = mixQuery(query, sqlPartial`(kind = ${kind})`);
|
||||
|
||||
if (k < uniqueKinds.length - 1) {
|
||||
query = mixQuery(query, sqlPartial`OR`);
|
||||
}
|
||||
}
|
||||
query = mixQuery(query, sqlPartial`)`);
|
||||
}
|
||||
|
||||
if (type.startsWith('#')) {
|
||||
const tag = type.slice(1);
|
||||
const uniqueValues = [...new Set(value)];
|
||||
query = mixQuery(query, sqlPartial`(`);
|
||||
for (let k = 0; k < uniqueValues.length; k++) {
|
||||
const tagValue = uniqueValues[k] as string;
|
||||
if (tag === 'a') {
|
||||
const aTagInfo = parseATagQuery(tagValue);
|
||||
|
||||
if (aTagInfo.dTag && aTagInfo.dTag !== '') {
|
||||
if (isCCNReplaceableEvent(aTagInfo.kind)) {
|
||||
// CCN replaceable event reference
|
||||
query = mixQuery(
|
||||
query,
|
||||
sqlPartial`id IN (
|
||||
SELECT e.id
|
||||
FROM events e
|
||||
JOIN event_tags t ON e.id = t.event_id
|
||||
JOIN event_tags_values v ON t.tag_id = v.tag_id
|
||||
WHERE e.kind = ${aTagInfo.kind}
|
||||
AND t.tag_name = 'd'
|
||||
AND v.value_position = 1
|
||||
AND v.value = ${aTagInfo.dTag}
|
||||
)`,
|
||||
);
|
||||
} else {
|
||||
// Addressable event reference
|
||||
query = mixQuery(
|
||||
query,
|
||||
sqlPartial`id IN (
|
||||
SELECT e.id
|
||||
FROM events e
|
||||
JOIN event_tags t ON e.id = t.event_id
|
||||
JOIN event_tags_values v ON t.tag_id = v.tag_id
|
||||
WHERE e.kind = ${aTagInfo.kind}
|
||||
AND e.pubkey = ${aTagInfo.pubkey}
|
||||
AND t.tag_name = 'd'
|
||||
AND v.value_position = 1
|
||||
AND v.value = ${aTagInfo.dTag}
|
||||
)`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Replaceable event reference
|
||||
query = mixQuery(
|
||||
query,
|
||||
sqlPartial`id IN (
|
||||
SELECT id
|
||||
FROM events
|
||||
WHERE kind = ${aTagInfo.kind}
|
||||
AND pubkey = ${aTagInfo.pubkey}
|
||||
)`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Regular tag handling (unchanged)
|
||||
query = mixQuery(
|
||||
query,
|
||||
sqlPartial`id IN (
|
||||
SELECT t.event_id
|
||||
FROM event_tags t
|
||||
WHERE t.tag_name = ${tag}
|
||||
AND t.tag_id IN (
|
||||
SELECT v.tag_id
|
||||
FROM event_tags_values v
|
||||
WHERE v.value_position = 1
|
||||
AND v.value = ${tagValue}
|
||||
)
|
||||
)`,
|
||||
);
|
||||
}
|
||||
|
||||
if (k < uniqueValues.length - 1) {
|
||||
query = mixQuery(query, sqlPartial`OR`);
|
||||
}
|
||||
}
|
||||
query = mixQuery(query, sqlPartial`)`);
|
||||
}
|
||||
|
||||
if (type === 'since') {
|
||||
query = mixQuery(query, sqlPartial`created_at > ${value}`);
|
||||
}
|
||||
|
||||
if (type === 'until') {
|
||||
query = mixQuery(query, sqlPartial`created_at <= ${value}`);
|
||||
}
|
||||
|
||||
if (j < filterWithoutLimit.length - 1)
|
||||
query = mixQuery(query, sqlPartial`AND`);
|
||||
}
|
||||
|
||||
query = mixQuery(query, sqlPartial`)`);
|
||||
|
||||
if (i < filters.length - 1) query = mixQuery(query, sqlPartial`OR`);
|
||||
}
|
||||
}
|
||||
|
||||
query = mixQuery(query, sqlPartial`ORDER BY created_at ASC`);
|
||||
|
||||
if (minLimit !== null) {
|
||||
query = mixQuery(query, sqlPartial`LIMIT ${minLimit}`);
|
||||
}
|
||||
|
||||
log.debug('built query', {
|
||||
tag: 'handleRequest',
|
||||
query: query.query,
|
||||
values: query.values,
|
||||
});
|
||||
|
||||
const events = connection.db.prepare(query.query).all(...query.values);
|
||||
log.debug('found events', {
|
||||
tag: 'handleRequest',
|
||||
count: events.length,
|
||||
});
|
||||
|
||||
for (let i = 0; i < events.length; i++) {
|
||||
const rawTags = sql`SELECT * FROM event_tags_view WHERE event_id = ${
|
||||
events[i].id
|
||||
}`(connection.db);
|
||||
const tagsByIndex = new Map<
|
||||
number,
|
||||
{
|
||||
name: string;
|
||||
values: Map<number, string>;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const tag of rawTags) {
|
||||
let tagData = tagsByIndex.get(tag.tag_index);
|
||||
if (!tagData) {
|
||||
tagData = {
|
||||
name: tag.tag_name,
|
||||
values: new Map(),
|
||||
};
|
||||
tagsByIndex.set(tag.tag_index, tagData);
|
||||
}
|
||||
|
||||
tagData.values.set(tag.tag_value_position, tag.tag_value);
|
||||
}
|
||||
|
||||
const tagsArray = Array.from(tagsByIndex.entries())
|
||||
.sort(([indexA], [indexB]) => indexA - indexB)
|
||||
.map(([_, tagData]) => {
|
||||
const { name, values } = tagData;
|
||||
|
||||
return [
|
||||
name,
|
||||
...Array.from(values.entries())
|
||||
.sort(([posA], [posB]) => posA - posB)
|
||||
.map(([_, value]) => value),
|
||||
];
|
||||
});
|
||||
|
||||
const event = {
|
||||
id: events[i].id,
|
||||
pubkey: events[i].pubkey,
|
||||
created_at: events[i].created_at,
|
||||
kind: events[i].kind,
|
||||
tags: tagsArray,
|
||||
content: events[i].content,
|
||||
sig: events[i].sig,
|
||||
};
|
||||
|
||||
connection.sendEvent(subscriptionId, event);
|
||||
log.debug('sent event', {
|
||||
tag: 'handleRequest',
|
||||
subscriptionId,
|
||||
eventId: event.id,
|
||||
});
|
||||
}
|
||||
connection.sendEOSE(subscriptionId);
|
||||
log.debug('sent EOSE', { tag: 'handleRequest', subscriptionId });
|
||||
connection.subscriptions.set(subscriptionId, filters);
|
||||
log.debug('end', { tag: 'handleRequest', subscriptionId });
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue