Merge branch 'main' into mint-cashu

This commit is contained in:
P. Reis 2025-02-17 10:58:08 -03:00
commit 29b1a20193
110 changed files with 367 additions and 322 deletions

View file

@ -10,7 +10,7 @@ test:
stage: test stage: test
script: script:
- deno fmt --check - deno fmt --check
- deno lint - deno task lint
- deno task check - deno task check
- deno task test --coverage=cov_profile - deno task test --coverage=cov_profile
- deno coverage cov_profile - deno coverage cov_profile

View file

@ -1,8 +1,11 @@
{ {
"version": "1.1.0",
"workspace": [ "workspace": [
"./packages/api", "./packages/api",
"./packages/conf", "./packages/conf",
"./packages/ditto" "./packages/db",
"./packages/ditto",
"./packages/metrics"
], ],
"tasks": { "tasks": {
"start": "deno run -A --env-file --deny-read=.env packages/ditto/server.ts", "start": "deno run -A --env-file --deny-read=.env packages/ditto/server.ts",
@ -16,6 +19,7 @@
"debug": "deno run -A --env-file --deny-read=.env --inspect packages/ditto/server.ts", "debug": "deno run -A --env-file --deny-read=.env --inspect packages/ditto/server.ts",
"test": "deno test -A --env-file=.env.test --deny-read=.env --junit-path=./deno-test.xml", "test": "deno test -A --env-file=.env.test --deny-read=.env --junit-path=./deno-test.xml",
"check": "deno check --allow-import .", "check": "deno check --allow-import .",
"lint": "deno lint --allow-import",
"nsec": "deno run scripts/nsec.ts", "nsec": "deno run scripts/nsec.ts",
"admin:event": "deno run -A --env-file --deny-read=.env scripts/admin-event.ts", "admin:event": "deno run -A --env-file --deny-read=.env scripts/admin-event.ts",
"admin:role": "deno run -A --env-file --deny-read=.env scripts/admin-role.ts", "admin:role": "deno run -A --env-file --deny-read=.env scripts/admin-role.ts",
@ -41,7 +45,6 @@
"./public" "./public"
], ],
"imports": { "imports": {
"@/": "./packages/ditto/",
"@b-fuze/deno-dom": "jsr:@b-fuze/deno-dom@^0.1.47", "@b-fuze/deno-dom": "jsr:@b-fuze/deno-dom@^0.1.47",
"@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4", "@bradenmacdonald/s3-lite-client": "jsr:@bradenmacdonald/s3-lite-client@^0.7.4",
"@cashu/cashu-ts": "npm:@cashu/cashu-ts@^2.2.0", "@cashu/cashu-ts": "npm:@cashu/cashu-ts@^2.2.0",

View file

@ -1,4 +1,7 @@
import Module from 'node:module';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path';
import ISO6391, { type LanguageCode } from 'iso-639-1'; import ISO6391, { type LanguageCode } from 'iso-639-1';
import { getPublicKey, nip19 } from 'nostr-tools'; import { getPublicKey, nip19 } from 'nostr-tools';
import { decodeBase64 } from '@std/encoding/base64'; import { decodeBase64 } from '@std/encoding/base64';
@ -346,12 +349,12 @@ export class DittoConf {
/** Path to the custom policy module. Must be an absolute path, https:, npm:, or jsr: URI. */ /** Path to the custom policy module. Must be an absolute path, https:, npm:, or jsr: URI. */
get policy(): string { get policy(): string {
return this.env.get('DITTO_POLICY') || new URL('../data/policy.ts', import.meta.url).pathname; return this.env.get('DITTO_POLICY') || path.join(this.dataDir, 'policy.ts');
} }
/** Absolute path to the data directory used by Ditto. */ /** Absolute path to the data directory used by Ditto. */
get dataDir(): string { get dataDir(): string {
return this.env.get('DITTO_DATA_DIR') || new URL('../data', import.meta.url).pathname; return this.env.get('DITTO_DATA_DIR') || path.join(cwd(), 'data');
} }
/** Absolute path of the Deno directory. */ /** Absolute path of the Deno directory. */
@ -462,3 +465,12 @@ export class DittoConf {
return Number(this.env.get('STREAK_WINDOW') || 129600); return Number(this.env.get('STREAK_WINDOW') || 129600);
} }
} }
/**
* HACK: get cwd without read permissions.
* https://github.com/denoland/deno/issues/27080#issuecomment-2504150155
*/
function cwd() {
// @ts-ignore Internal method, but it does exist.
return Module._nodeModulePaths('a')[0].slice(0, -15);
}

View file

@ -0,0 +1,6 @@
import { DittoDB } from './DittoDB.ts';
Deno.test('DittoDB', async () => {
const db = DittoDB.create('memory://');
await DittoDB.migrate(db.kysely);
});

View file

@ -2,14 +2,14 @@ import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { JsonValue } from '@std/json'; import { FileMigrationProvider, type Kysely, Migrator } from 'kysely';
import { FileMigrationProvider, Kysely, Migrator } from 'kysely';
import { DittoPglite } from '@/db/adapters/DittoPglite.ts'; import { DittoPglite } from './adapters/DittoPglite.ts';
import { DittoPostgres } from '@/db/adapters/DittoPostgres.ts'; import { DittoPostgres } from './adapters/DittoPostgres.ts';
import { DittoDatabase, DittoDatabaseOpts } from '@/db/DittoDatabase.ts';
import { DittoTables } from '@/db/DittoTables.ts'; import type { JsonValue } from '@std/json';
import { errorJson } from '@/utils/log.ts'; import type { DittoDatabase, DittoDatabaseOpts } from './DittoDatabase.ts';
import type { DittoTables } from './DittoTables.ts';
export class DittoDB { export class DittoDB {
/** Open a new database connection. */ /** Open a new database connection. */
@ -49,7 +49,7 @@ export class DittoDB {
msg: 'Migration failed.', msg: 'Migration failed.',
state: 'failed', state: 'failed',
results: results as unknown as JsonValue, results: results as unknown as JsonValue,
error: errorJson(error), error: error instanceof Error ? error : null,
}); });
Deno.exit(1); Deno.exit(1);
} else { } else {

View file

@ -1,6 +1,6 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
import { DittoTables } from '@/db/DittoTables.ts'; import type { DittoTables } from './DittoTables.ts';
export interface DittoDatabase { export interface DittoDatabase {
readonly kysely: Kysely<DittoTables>; readonly kysely: Kysely<DittoTables>;

View file

@ -1,6 +1,5 @@
import { Generated } from 'kysely'; import type { NPostgresSchema } from '@nostrify/db';
import type { Generated } from 'kysely';
import { NPostgresSchema } from '@nostrify/db';
export interface DittoTables extends NPostgresSchema { export interface DittoTables extends NPostgresSchema {
auth_tokens: AuthTokenRow; auth_tokens: AuthTokenRow;

View file

@ -1,8 +1,7 @@
import { logi, LogiValue } from '@soapbox/logi'; import { dbQueriesCounter, dbQueryDurationHistogram } from '@ditto/metrics';
import { Logger } from 'kysely'; import { logi, type LogiValue } from '@soapbox/logi';
import { dbQueriesCounter, dbQueryDurationHistogram } from '@/metrics.ts'; import type { Logger } from 'kysely';
import { errorJson } from '@/utils/log.ts';
/** Log the SQL for queries. */ /** Log the SQL for queries. */
export const KyselyLogger: Logger = (event) => { export const KyselyLogger: Logger = (event) => {
@ -24,7 +23,7 @@ export const KyselyLogger: Logger = (event) => {
ns: 'ditto.sql', ns: 'ditto.sql',
sql, sql,
parameters: parameters as LogiValue, parameters: parameters as LogiValue,
error: errorJson(event.error), error: event.error instanceof Error ? event.error : null,
duration, duration,
}); });
} }

View file

@ -0,0 +1,13 @@
import { assertEquals } from '@std/assert';
import { DittoPglite } from './DittoPglite.ts';
Deno.test('DittoPglite.create', async () => {
const db = DittoPglite.create('memory://');
assertEquals(db.poolSize, 1);
assertEquals(db.availableConnections, 1);
await db.kysely.destroy();
await new Promise((resolve) => setTimeout(resolve, 100));
});

View file

@ -3,10 +3,11 @@ import { pg_trgm } from '@electric-sql/pglite/contrib/pg_trgm';
import { PgliteDialect } from '@soapbox/kysely-pglite'; import { PgliteDialect } from '@soapbox/kysely-pglite';
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { DittoDatabase, DittoDatabaseOpts } from '@/db/DittoDatabase.ts'; import { KyselyLogger } from '../KyselyLogger.ts';
import { DittoTables } from '@/db/DittoTables.ts'; import { isWorker } from '../utils/worker.ts';
import { KyselyLogger } from '@/db/KyselyLogger.ts';
import { isWorker } from '@/utils/worker.ts'; import type { DittoDatabase, DittoDatabaseOpts } from '../DittoDatabase.ts';
import type { DittoTables } from '../DittoTables.ts';
export class DittoPglite { export class DittoPglite {
static create(databaseUrl: string, opts?: DittoDatabaseOpts): DittoDatabase { static create(databaseUrl: string, opts?: DittoDatabaseOpts): DittoDatabase {

View file

@ -1,5 +1,5 @@
import { import {
BinaryOperationNode, type BinaryOperationNode,
FunctionNode, FunctionNode,
Kysely, Kysely,
OperatorNode, OperatorNode,
@ -9,12 +9,13 @@ import {
PrimitiveValueListNode, PrimitiveValueListNode,
ValueNode, ValueNode,
} from 'kysely'; } from 'kysely';
import { PostgresJSDialectConfig, PostgresJSDriver } from 'kysely-postgres-js'; import { type PostgresJSDialectConfig, PostgresJSDriver } from 'kysely-postgres-js';
import postgres from 'postgres'; import postgres from 'postgres';
import { DittoDatabase, DittoDatabaseOpts } from '@/db/DittoDatabase.ts'; import { KyselyLogger } from '../KyselyLogger.ts';
import { DittoTables } from '@/db/DittoTables.ts';
import { KyselyLogger } from '@/db/KyselyLogger.ts'; import type { DittoDatabase, DittoDatabaseOpts } from '../DittoDatabase.ts';
import type { DittoTables } from '../DittoTables.ts';
export class DittoPostgres { export class DittoPostgres {
static create(databaseUrl: string, opts?: DittoDatabaseOpts): DittoDatabase { static create(databaseUrl: string, opts?: DittoDatabaseOpts): DittoDatabase {

6
packages/db/deno.json Normal file
View file

@ -0,0 +1,6 @@
{
"name": "@ditto/db",
"exports": {
".": "./mod.ts"
}
}

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(_db: Kysely<unknown>): Promise<void> { export async function up(_db: Kysely<unknown>): Promise<void> {
// This migration used to create an FTS table for SQLite, but SQLite support was removed. // This migration used to create an FTS table for SQLite, but SQLite support was removed.

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(_db: Kysely<unknown>): Promise<void> { export async function up(_db: Kysely<unknown>): Promise<void> {
} }

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(_db: Kysely<unknown>): Promise<void> { export async function up(_db: Kysely<unknown>): Promise<void> {
} }

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(_db: Kysely<unknown>): Promise<void> { export async function up(_db: Kysely<unknown>): Promise<void> {
} }

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(_db: Kysely<unknown>): Promise<void> { export async function up(_db: Kysely<unknown>): Promise<void> {
} }

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('users').ifExists().execute(); await db.schema.dropTable('users').ifExists().execute();

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.dropIndex('idx_tags_tag').execute(); await db.schema.dropIndex('idx_tags_tag').execute();

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('events').addColumn('deleted_at', 'integer').execute(); await db.schema.alterTable('events').addColumn('deleted_at', 'integer').execute();

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.createIndex('idx_author_stats_pubkey').on('author_stats').column('pubkey').execute(); await db.schema.createIndex('idx_author_stats_pubkey').on('author_stats').column('pubkey').execute();

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('relays').execute(); await db.schema.dropTable('relays').execute();

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('events').renameTo('nostr_events').execute(); await db.schema.alterTable('events').renameTo('nostr_events').execute();

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
export async function up(db: Kysely<any>): Promise<void> { export async function up(db: Kysely<any>): Promise<void> {

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.createTable('nostr_pgfts') await db.schema.createTable('nostr_pgfts')

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
// Create new table and indexes. // Create new table and indexes.

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('unattached_media').execute(); await db.schema.dropTable('unattached_media').execute();

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('nostr_events').addColumn('language', 'char(2)').execute(); await db.schema.alterTable('nostr_events').addColumn('language', 'char(2)').execute();

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
export async function up(db: Kysely<any>): Promise<void> { export async function up(db: Kysely<any>): Promise<void> {

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
export async function up(db: Kysely<any>): Promise<void> { export async function up(db: Kysely<any>): Promise<void> {

View file

@ -1,8 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
import { Conf } from '@/config.ts';
import { aesEncrypt } from '@/utils/aes.ts';
import { getTokenHash } from '@/utils/auth.ts';
interface DB { interface DB {
nip46_tokens: { nip46_tokens: {
@ -32,19 +28,6 @@ export async function up(db: Kysely<DB>): Promise<void> {
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`)) .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql`CURRENT_TIMESTAMP`))
.execute(); .execute();
// There are probably not that many tokens in the database yet, so this should be fine.
const tokens = await db.selectFrom('nip46_tokens').selectAll().execute();
for (const token of tokens) {
await db.insertInto('auth_tokens').values({
token_hash: await getTokenHash(token.api_token),
pubkey: token.user_pubkey,
nip46_sk_enc: await aesEncrypt(Conf.seckey, token.server_seckey),
nip46_relays: JSON.parse(token.relays),
created_at: token.connected_at,
}).execute();
}
await db.schema.dropTable('nip46_tokens').execute(); await db.schema.dropTable('nip46_tokens').execute();
} }

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await sql` await sql`

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
export async function up(db: Kysely<any>): Promise<void> { export async function up(db: Kysely<any>): Promise<void> {

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await sql`DROP TRIGGER IF EXISTS nostr_event_trigger ON nostr_events`.execute(db); await sql`DROP TRIGGER IF EXISTS nostr_event_trigger ON nostr_events`.execute(db);

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('nostr_events').dropColumn('language').execute(); await db.schema.alterTable('nostr_events').dropColumn('language').execute();

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.alterTable('nostr_events').alterColumn('search_ext', (col) => col.dropDefault()).execute(); await db.schema.alterTable('nostr_events').alterColumn('search_ext', (col) => col.dropDefault()).execute();

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema await db.schema

View file

@ -1,4 +1,4 @@
import { Kysely } from 'kysely'; import type { Kysely } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('pubkey_domains').execute(); await db.schema.dropTable('pubkey_domains').execute();

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
export async function up(db: Kysely<any>): Promise<void> { export async function up(db: Kysely<any>): Promise<void> {

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await sql`DROP TRIGGER IF EXISTS nostr_event_trigger ON nostr_events`.execute(db); await sql`DROP TRIGGER IF EXISTS nostr_event_trigger ON nostr_events`.execute(db);

View file

@ -1,4 +1,4 @@
import { Kysely, sql } from 'kysely'; import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<unknown>): Promise<void> { export async function up(db: Kysely<unknown>): Promise<void> {
await sql` await sql`

4
packages/db/mod.ts Normal file
View file

@ -0,0 +1,4 @@
export { DittoDB } from './DittoDB.ts';
export type { DittoDatabase } from './DittoDatabase.ts';
export type { DittoTables } from './DittoTables.ts';

View file

@ -1,14 +1,16 @@
import { assertEquals } from '@std/assert'; import { assertEquals } from '@std/assert';
import { isWorker } from '@/utils/worker.ts'; import { isWorker } from './worker.ts';
Deno.test('isWorker from the main thread returns false', () => { Deno.test('isWorker from the main thread returns false', () => {
assertEquals(isWorker(), false); assertEquals(isWorker(), false);
}); });
Deno.test('isWorker from a worker thread returns true', async () => { Deno.test('isWorker from a worker thread returns true', async () => {
const url = new URL('./worker.ts', import.meta.url);
const script = ` const script = `
import { isWorker } from '@/utils/worker.ts'; import { isWorker } from '${url.href}';
postMessage(isWorker()); postMessage(isWorker());
self.close(); self.close();
`; `;

View file

@ -1,5 +1,6 @@
import { confMw } from '@ditto/api/middleware'; import { confMw } from '@ditto/api/middleware';
import { type DittoConf } from '@ditto/conf'; import { type DittoConf } from '@ditto/conf';
import { DittoTables } from '@ditto/db';
import { type Context, Env as HonoEnv, Handler, Hono, Input as HonoInput, MiddlewareHandler } from '@hono/hono'; import { type Context, Env as HonoEnv, Handler, Hono, Input as HonoInput, MiddlewareHandler } from '@hono/hono';
import { every } from '@hono/hono/combine'; import { every } from '@hono/hono/combine';
import { cors } from '@hono/hono/cors'; import { cors } from '@hono/hono/cors';
@ -9,7 +10,6 @@ import { Kysely } from 'kysely';
import '@/startup.ts'; import '@/startup.ts';
import { DittoTables } from '@/db/DittoTables.ts';
import { Time } from '@/utils/time.ts'; import { Time } from '@/utils/time.ts';
import { import {

View file

@ -1,14 +1,14 @@
import {
streamingClientMessagesCounter,
streamingConnectionsGauge,
streamingServerMessagesCounter,
} from '@ditto/metrics';
import TTLCache from '@isaacs/ttlcache'; import TTLCache from '@isaacs/ttlcache';
import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import { NostrEvent, NostrFilter } from '@nostrify/nostrify';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { z } from 'zod'; import { z } from 'zod';
import { type AppController } from '@/app.ts'; import { type AppController } from '@/app.ts';
import {
streamingClientMessagesCounter,
streamingConnectionsGauge,
streamingServerMessagesCounter,
} from '@/metrics.ts';
import { MuteListPolicy } from '@/policies/MuteListPolicy.ts'; import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
import { getFeedPubkeys } from '@/queries.ts'; import { getFeedPubkeys } from '@/queries.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';

View file

@ -1,10 +1,10 @@
import { cachedTranslationsSizeGauge } from '@ditto/metrics';
import { LanguageCode } from 'iso-639-1'; import { LanguageCode } from 'iso-639-1';
import { z } from 'zod'; import { z } from 'zod';
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { translationCache } from '@/caches/translationCache.ts'; import { translationCache } from '@/caches/translationCache.ts';
import { MastodonTranslation } from '@/entities/MastodonTranslation.ts'; import { MastodonTranslation } from '@/entities/MastodonTranslation.ts';
import { cachedTranslationsSizeGauge } from '@/metrics.ts';
import { getEvent } from '@/queries.ts'; import { getEvent } from '@/queries.ts';
import { localeSchema } from '@/schema.ts'; import { localeSchema } from '@/schema.ts';
import { parseBody } from '@/utils/api.ts'; import { parseBody } from '@/utils/api.ts';

View file

@ -1,12 +1,12 @@
import { register } from 'prom-client';
import { AppController } from '@/app.ts';
import { import {
dbAvailableConnectionsGauge, dbAvailableConnectionsGauge,
dbPoolSizeGauge, dbPoolSizeGauge,
relayPoolRelaysSizeGauge, relayPoolRelaysSizeGauge,
relayPoolSubscriptionsSizeGauge, relayPoolSubscriptionsSizeGauge,
} from '@/metrics.ts'; } from '@ditto/metrics';
import { register } from 'prom-client';
import { AppController } from '@/app.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
/** Prometheus/OpenMetrics controller. */ /** Prometheus/OpenMetrics controller. */

View file

@ -1,4 +1,5 @@
import { type DittoConf } from '@ditto/conf'; import { type DittoConf } from '@ditto/conf';
import { relayConnectionsGauge, relayEventsCounter, relayMessagesCounter } from '@ditto/metrics';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { JsonValue } from '@std/json'; import { JsonValue } from '@std/json';
import { import {
@ -14,7 +15,6 @@ import {
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { relayInfoController } from '@/controllers/nostr/relay-info.ts'; import { relayInfoController } from '@/controllers/nostr/relay-info.ts';
import { relayConnectionsGauge, relayEventsCounter, relayMessagesCounter } from '@/metrics.ts';
import * as pipeline from '@/pipeline.ts'; import * as pipeline from '@/pipeline.ts';
import { RelayError } from '@/RelayError.ts'; import { RelayError } from '@/RelayError.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
@ -64,9 +64,14 @@ function connectStream(socket: WebSocket, ip: string | undefined, conf: DittoCon
} }
const result = n.json().pipe(n.clientMsg()).safeParse(e.data); const result = n.json().pipe(n.clientMsg()).safeParse(e.data);
if (result.success) { if (result.success) {
logi({ level: 'trace', ns: 'ditto.relay.message', data: result.data as JsonValue, ip }); const msg = result.data;
relayMessagesCounter.inc({ verb: result.data[0] }); const verb = msg[0];
logi({ level: 'trace', ns: 'ditto.relay.msg', verb, msg: msg as JsonValue, ip });
relayMessagesCounter.inc({ verb });
handleMsg(result.data); handleMsg(result.data);
} else { } else {
relayMessagesCounter.inc(); relayMessagesCounter.inc();

View file

@ -1,9 +1,9 @@
{ {
"name": "@ditto/ditto", "name": "@ditto/ditto",
"version": "1.1.0",
"exports": {}, "exports": {},
"imports": { "imports": {
"deno.json": "./deno.json" "@/": "./",
"deno.json": "../../deno.json"
}, },
"lint": { "lint": {
"rules": { "rules": {

View file

@ -49,6 +49,7 @@ export interface MastodonAccount {
days: number; days: number;
start: string | null; start: string | null;
end: string | null; end: string | null;
expires: string | null;
}; };
}; };
domain?: string; domain?: string;

View file

@ -1,8 +1,8 @@
import { firehoseEventsCounter } from '@ditto/metrics';
import { Semaphore } from '@core/asyncutil'; import { Semaphore } from '@core/asyncutil';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { firehoseEventsCounter } from '@/metrics.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { nostrNow } from '@/utils.ts'; import { nostrNow } from '@/utils.ts';

View file

@ -1,149 +0,0 @@
import { Counter, Gauge, Histogram } from 'prom-client';
export const httpRequestsCounter = new Counter({
name: 'ditto_http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method'],
});
export const httpResponsesCounter = new Counter({
name: 'ditto_http_responses_total',
help: 'Total number of HTTP responses',
labelNames: ['method', 'path', 'status'],
});
export const httpResponseDurationHistogram = new Histogram({
name: 'ditto_http_response_duration_seconds',
help: 'Histogram of HTTP response times in seconds',
labelNames: ['method', 'path', 'status'],
});
export const streamingConnectionsGauge = new Gauge({
name: 'ditto_streaming_connections',
help: 'Number of active connections to the streaming API',
});
export const streamingServerMessagesCounter = new Counter({
name: 'ditto_streaming_server_messages_total',
help: 'Total number of messages sent from the streaming API',
});
export const streamingClientMessagesCounter = new Counter({
name: 'ditto_streaming_client_messages_total',
help: 'Total number of messages received by the streaming API',
});
export const fetchResponsesCounter = new Counter({
name: 'ditto_fetch_responses_total',
help: 'Total number of fetch requests',
labelNames: ['method', 'status'],
});
export const firehoseEventsCounter = new Counter({
name: 'ditto_firehose_events_total',
help: 'Total number of Nostr events processed by the firehose',
labelNames: ['kind'],
});
export const pipelineEventsCounter = new Counter({
name: 'ditto_pipeline_events_total',
help: 'Total number of Nostr events processed by the pipeline',
labelNames: ['kind'],
});
export const policyEventsCounter = new Counter({
name: 'ditto_policy_events_total',
help: 'Total number of policy OK responses',
labelNames: ['ok'],
});
export const relayEventsCounter = new Counter({
name: 'ditto_relay_events_total',
help: 'Total number of EVENT messages processed by the relay',
labelNames: ['kind'],
});
export const relayMessagesCounter = new Counter({
name: 'ditto_relay_messages_total',
help: 'Total number of Nostr messages processed by the relay',
labelNames: ['verb'],
});
export const relayConnectionsGauge = new Gauge({
name: 'ditto_relay_connections',
help: 'Number of active connections to the relay',
});
export const dbQueriesCounter = new Counter({
name: 'ditto_db_queries_total',
help: 'Total number of database queries',
labelNames: ['kind'],
});
export const dbEventsCounter = new Counter({
name: 'ditto_db_events_total',
help: 'Total number of database inserts',
labelNames: ['kind'],
});
export const dbPoolSizeGauge = new Gauge({
name: 'ditto_db_pool_size',
help: 'Number of connections in the database pool',
});
export const dbAvailableConnectionsGauge = new Gauge({
name: 'ditto_db_available_connections',
help: 'Number of available connections in the database pool',
});
export const dbQueryDurationHistogram = new Histogram({
name: 'ditto_db_query_duration_seconds',
help: 'Duration of database queries',
});
export const cachedFaviconsSizeGauge = new Gauge({
name: 'ditto_cached_favicons_size',
help: 'Number of domain favicons in cache',
});
export const cachedLnurlsSizeGauge = new Gauge({
name: 'ditto_cached_lnurls_size',
help: 'Number of LNURL details in cache',
});
export const cachedNip05sSizeGauge = new Gauge({
name: 'ditto_cached_nip05s_size',
help: 'Number of NIP-05 results in cache',
});
export const cachedLinkPreviewSizeGauge = new Gauge({
name: 'ditto_cached_link_previews_size',
help: 'Number of link previews in cache',
});
export const cachedTranslationsSizeGauge = new Gauge({
name: 'ditto_cached_translations_size',
help: 'Number of translated statuses in cache',
});
export const internalSubscriptionsSizeGauge = new Gauge({
name: 'ditto_internal_subscriptions_size',
help: "Number of active subscriptions to Ditto's internal relay",
});
export const relayPoolRelaysSizeGauge = new Gauge({
name: 'ditto_relay_pool_relays_size',
help: 'Number of relays in the relay pool',
labelNames: ['ready_state'],
});
export const relayPoolSubscriptionsSizeGauge = new Gauge({
name: 'ditto_relay_pool_subscriptions_size',
help: 'Number of active subscriptions to the relay pool',
});
export const webPushNotificationsCounter = new Counter({
name: 'ditto_web_push_notifications_total',
help: 'Total number of Web Push notifications sent',
labelNames: ['type'],
});

View file

@ -1,8 +1,7 @@
import { httpRequestsCounter, httpResponseDurationHistogram, httpResponsesCounter } from '@ditto/metrics';
import { ScopedPerformance } from '@esroyo/scoped-performance'; import { ScopedPerformance } from '@esroyo/scoped-performance';
import { MiddlewareHandler } from '@hono/hono'; import { MiddlewareHandler } from '@hono/hono';
import { httpRequestsCounter, httpResponseDurationHistogram, httpResponsesCounter } from '@/metrics.ts';
/** Prometheus metrics middleware that tracks HTTP requests by methods and responses by status code. */ /** Prometheus metrics middleware that tracks HTTP requests by methods and responses by status code. */
export const metricsMiddleware: MiddlewareHandler = async (c, next) => { export const metricsMiddleware: MiddlewareHandler = async (c, next) => {
// Start a timer to measure the duration of the response. // Start a timer to measure the duration of the response.

View file

@ -1,3 +1,5 @@
import { DittoTables } from '@ditto/db';
import { pipelineEventsCounter, policyEventsCounter, webPushNotificationsCounter } from '@ditto/metrics';
import { NKinds, NostrEvent, NSchema as n } from '@nostrify/nostrify'; import { NKinds, NostrEvent, NSchema as n } from '@nostrify/nostrify';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { Kysely, UpdateObject } from 'kysely'; import { Kysely, UpdateObject } from 'kysely';
@ -6,10 +8,8 @@ import { z } from 'zod';
import { pipelineEncounters } from '@/caches/pipelineEncounters.ts'; import { pipelineEncounters } from '@/caches/pipelineEncounters.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DittoTables } from '@/db/DittoTables.ts';
import { DittoPush } from '@/DittoPush.ts'; import { DittoPush } from '@/DittoPush.ts';
import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts';
import { pipelineEventsCounter, policyEventsCounter, webPushNotificationsCounter } from '@/metrics.ts';
import { RelayError } from '@/RelayError.ts'; import { RelayError } from '@/RelayError.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts';
import { hydrateEvents } from '@/storages/hydrate.ts'; import { hydrateEvents } from '@/storages/hydrate.ts';

View file

@ -1,10 +1,9 @@
// deno-lint-ignore-file require-await // deno-lint-ignore-file require-await
import { type DittoDatabase, DittoDB } from '@ditto/db';
import { internalSubscriptionsSizeGauge } from '@ditto/metrics';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DittoDatabase } from '@/db/DittoDatabase.ts';
import { DittoDB } from '@/db/DittoDB.ts';
import { internalSubscriptionsSizeGauge } from '@/metrics.ts';
import { wsUrlSchema } from '@/schema.ts'; import { wsUrlSchema } from '@/schema.ts';
import { AdminStore } from '@/storages/AdminStore.ts'; import { AdminStore } from '@/storages/AdminStore.ts';
import { EventsDB } from '@/storages/EventsDB.ts'; import { EventsDB } from '@/storages/EventsDB.ts';

View file

@ -1,6 +1,8 @@
// deno-lint-ignore-file require-await // deno-lint-ignore-file require-await
import { DittoTables } from '@ditto/db';
import { NPostgres, NPostgresSchema } from '@nostrify/db'; import { NPostgres, NPostgresSchema } from '@nostrify/db';
import { dbEventsCounter } from '@ditto/metrics';
import { NIP50, NKinds, NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import { NIP50, NKinds, NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { JsonValue } from '@std/json'; import { JsonValue } from '@std/json';
@ -11,8 +13,6 @@ import { nip27 } from 'nostr-tools';
import tldts from 'tldts'; import tldts from 'tldts';
import { z } from 'zod'; import { z } from 'zod';
import { DittoTables } from '@/db/DittoTables.ts';
import { dbEventsCounter } from '@/metrics.ts';
import { RelayError } from '@/RelayError.ts'; import { RelayError } from '@/RelayError.ts';
import { isNostrId } from '@/utils.ts'; import { isNostrId } from '@/utils.ts';
import { abortError } from '@/utils/abort.ts'; import { abortError } from '@/utils/abort.ts';

View file

@ -1,10 +1,10 @@
import { DittoTables } from '@ditto/db';
import { NStore } from '@nostrify/nostrify'; import { NStore } from '@nostrify/nostrify';
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { matchFilter } from 'nostr-tools'; import { matchFilter } from 'nostr-tools';
import { NSchema as n } from '@nostrify/nostrify'; import { NSchema as n } from '@nostrify/nostrify';
import { z } from 'zod'; import { z } from 'zod';
import { DittoTables } from '@/db/DittoTables.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { fallbackAuthor } from '@/utils.ts'; import { fallbackAuthor } from '@/utils.ts';

View file

@ -1,10 +1,10 @@
import { DittoDB } from '@ditto/db';
import ISO6391, { LanguageCode } from 'iso-639-1'; import ISO6391, { LanguageCode } from 'iso-639-1';
import lande from 'lande'; import lande from 'lande';
import { NostrEvent } from '@nostrify/nostrify'; import { NostrEvent } from '@nostrify/nostrify';
import { finalizeEvent, generateSecretKey } from 'nostr-tools'; import { finalizeEvent, generateSecretKey } from 'nostr-tools';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DittoDB } from '@/db/DittoDB.ts';
import { EventsDB } from '@/storages/EventsDB.ts'; import { EventsDB } from '@/storages/EventsDB.ts';
import { purifyEvent } from '@/utils/purify.ts'; import { purifyEvent } from '@/utils/purify.ts';
import { sql } from 'kysely'; import { sql } from 'kysely';

View file

@ -1,9 +1,9 @@
import { DittoTables } from '@ditto/db';
import { NostrFilter } from '@nostrify/nostrify'; import { NostrFilter } from '@nostrify/nostrify';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { Kysely, sql } from 'kysely'; import { Kysely, sql } from 'kysely';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DittoTables } from '@/db/DittoTables.ts';
import { handleEvent } from '@/pipeline.ts'; import { handleEvent } from '@/pipeline.ts';
import { AdminSigner } from '@/signers/AdminSigner.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';

View file

@ -1,12 +1,12 @@
import { DOMParser } from '@b-fuze/deno-dom'; import { DOMParser } from '@b-fuze/deno-dom';
import { DittoTables } from '@ditto/db';
import { cachedFaviconsSizeGauge } from '@ditto/metrics';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { safeFetch } from '@soapbox/safe-fetch'; import { safeFetch } from '@soapbox/safe-fetch';
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import tldts from 'tldts'; import tldts from 'tldts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DittoTables } from '@/db/DittoTables.ts';
import { cachedFaviconsSizeGauge } from '@/metrics.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { nostrNow } from '@/utils.ts'; import { nostrNow } from '@/utils.ts';
import { SimpleLRU } from '@/utils/SimpleLRU.ts'; import { SimpleLRU } from '@/utils/SimpleLRU.ts';

View file

@ -1,10 +1,10 @@
import { cachedLnurlsSizeGauge } from '@ditto/metrics';
import { NostrEvent } from '@nostrify/nostrify'; import { NostrEvent } from '@nostrify/nostrify';
import { LNURL, LNURLDetails } from '@nostrify/nostrify/ln'; import { LNURL, LNURLDetails } from '@nostrify/nostrify/ln';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { safeFetch } from '@soapbox/safe-fetch'; import { safeFetch } from '@soapbox/safe-fetch';
import { JsonValue } from '@std/json'; import { JsonValue } from '@std/json';
import { cachedLnurlsSizeGauge } from '@/metrics.ts';
import { SimpleLRU } from '@/utils/SimpleLRU.ts'; import { SimpleLRU } from '@/utils/SimpleLRU.ts';
import { errorJson } from '@/utils/log.ts'; import { errorJson } from '@/utils/log.ts';
import { Time } from '@/utils/time.ts'; import { Time } from '@/utils/time.ts';

View file

@ -1,11 +1,11 @@
import { nip19 } from 'nostr-tools'; import { cachedNip05sSizeGauge } from '@ditto/metrics';
import { NIP05, NStore } from '@nostrify/nostrify'; import { NIP05, NStore } from '@nostrify/nostrify';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { safeFetch } from '@soapbox/safe-fetch'; import { safeFetch } from '@soapbox/safe-fetch';
import { nip19 } from 'nostr-tools';
import tldts from 'tldts'; import tldts from 'tldts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { cachedNip05sSizeGauge } from '@/metrics.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { errorJson } from '@/utils/log.ts'; import { errorJson } from '@/utils/log.ts';
import { SimpleLRU } from '@/utils/SimpleLRU.ts'; import { SimpleLRU } from '@/utils/SimpleLRU.ts';

View file

@ -1,7 +1,6 @@
import { DittoTables } from '@ditto/db';
import { Kysely, sql } from 'kysely'; import { Kysely, sql } from 'kysely';
import { DittoTables } from '@/db/DittoTables.ts';
/** Get pubkeys whose name and NIP-05 is similar to 'q' */ /** Get pubkeys whose name and NIP-05 is similar to 'q' */
export async function getPubkeysBySearch( export async function getPubkeysBySearch(
kysely: Kysely<DittoTables>, kysely: Kysely<DittoTables>,

View file

@ -1,10 +1,10 @@
import { DittoTables } from '@ditto/db';
import { NostrEvent, NSchema as n, NStore } from '@nostrify/nostrify'; import { NostrEvent, NSchema as n, NStore } from '@nostrify/nostrify';
import { Insertable, Kysely, UpdateObject } from 'kysely'; import { Insertable, Kysely, UpdateObject } from 'kysely';
import { SetRequired } from 'type-fest'; import { SetRequired } from 'type-fest';
import { z } from 'zod'; import { z } from 'zod';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { DittoTables } from '@/db/DittoTables.ts';
import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
interface UpdateStatsOpts { interface UpdateStatsOpts {

View file

@ -1,3 +1,4 @@
import { cachedLinkPreviewSizeGauge } from '@ditto/metrics';
import TTLCache from '@isaacs/ttlcache'; import TTLCache from '@isaacs/ttlcache';
import { logi } from '@soapbox/logi'; import { logi } from '@soapbox/logi';
import { safeFetch } from '@soapbox/safe-fetch'; import { safeFetch } from '@soapbox/safe-fetch';
@ -6,7 +7,6 @@ import { unfurl } from 'unfurl.js';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { PreviewCard } from '@/entities/PreviewCard.ts'; import { PreviewCard } from '@/entities/PreviewCard.ts';
import { cachedLinkPreviewSizeGauge } from '@/metrics.ts';
import { errorJson } from '@/utils/log.ts'; import { errorJson } from '@/utils/log.ts';
async function unfurlCard(url: string, signal: AbortSignal): Promise<PreviewCard | null> { async function unfurlCard(url: string, signal: AbortSignal): Promise<PreviewCard | null> {

View file

@ -121,6 +121,7 @@ function renderAccount(event: Omit<DittoEvent, 'id' | 'sig'>, opts: ToAccountOpt
days: streakDays, days: streakDays,
start: streakStart ? nostrDate(streakStart).toISOString() : null, start: streakStart ? nostrDate(streakStart).toISOString() : null,
end: streakEnd ? nostrDate(streakEnd).toISOString() : null, end: streakEnd ? nostrDate(streakEnd).toISOString() : null,
expires: streakEnd ? nostrDate(streakEnd + streakWindow).toISOString() : null,
}, },
}, },
domain: parsed05?.domain, domain: parsed05?.domain,

View file

@ -1,9 +1,9 @@
import { DittoDB } from '@ditto/db';
import '@soapbox/safe-fetch/load'; import '@soapbox/safe-fetch/load';
import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/nostrify'; import { NostrEvent, NostrRelayOK, NPolicy } from '@nostrify/nostrify';
import { ReadOnlyPolicy } from '@nostrify/policies'; import { ReadOnlyPolicy } from '@nostrify/policies';
import * as Comlink from 'comlink'; import * as Comlink from 'comlink';
import { DittoDB } from '@/db/DittoDB.ts';
import { EventsDB } from '@/storages/EventsDB.ts'; import { EventsDB } from '@/storages/EventsDB.ts';
// @ts-ignore Don't try to access the env from this worker. // @ts-ignore Don't try to access the env from this worker.

View file

@ -0,0 +1,6 @@
{
"name": "@ditto/metrics",
"exports": {
".": "./metrics.ts"
}
}

151
packages/metrics/metrics.ts Normal file
View file

@ -0,0 +1,151 @@
import { Counter, Gauge, Histogram } from 'prom-client';
const prefix = 'ditto';
export const httpRequestsCounter: Counter<'method'> = new Counter({
name: `${prefix}_http_requests_total`,
help: 'Total number of HTTP requests',
labelNames: ['method'],
});
export const httpResponsesCounter: Counter<'method' | 'path' | 'status'> = new Counter({
name: `${prefix}_http_responses_total`,
help: 'Total number of HTTP responses',
labelNames: ['method', 'path', 'status'],
});
export const httpResponseDurationHistogram: Histogram<'method' | 'path' | 'status'> = new Histogram({
name: `${prefix}_http_response_duration_seconds`,
help: 'Histogram of HTTP response times in seconds',
labelNames: ['method', 'path', 'status'],
});
export const streamingConnectionsGauge: Gauge = new Gauge({
name: `${prefix}_streaming_connections`,
help: 'Number of active connections to the streaming API',
});
export const streamingServerMessagesCounter: Counter = new Counter({
name: `${prefix}_streaming_server_messages_total`,
help: 'Total number of messages sent from the streaming API',
});
export const streamingClientMessagesCounter: Counter = new Counter({
name: `${prefix}_streaming_client_messages_total`,
help: 'Total number of messages received by the streaming API',
});
export const fetchResponsesCounter: Counter<'method' | 'status'> = new Counter({
name: `${prefix}_fetch_responses_total`,
help: 'Total number of fetch requests',
labelNames: ['method', 'status'],
});
export const firehoseEventsCounter: Counter<'kind'> = new Counter({
name: `${prefix}_firehose_events_total`,
help: 'Total number of Nostr events processed by the firehose',
labelNames: ['kind'],
});
export const pipelineEventsCounter: Counter<'kind'> = new Counter({
name: `${prefix}_pipeline_events_total`,
help: 'Total number of Nostr events processed by the pipeline',
labelNames: ['kind'],
});
export const policyEventsCounter: Counter<'ok'> = new Counter({
name: `${prefix}_policy_events_total`,
help: 'Total number of policy OK responses',
labelNames: ['ok'],
});
export const relayEventsCounter: Counter<'kind'> = new Counter({
name: `${prefix}_relay_events_total`,
help: 'Total number of EVENT messages processed by the relay',
labelNames: ['kind'],
});
export const relayMessagesCounter: Counter<'verb'> = new Counter({
name: `${prefix}_relay_messages_total`,
help: 'Total number of Nostr messages processed by the relay',
labelNames: ['verb'],
});
export const relayConnectionsGauge: Gauge = new Gauge({
name: `${prefix}_relay_connections`,
help: 'Number of active connections to the relay',
});
export const dbQueriesCounter: Counter<'kind'> = new Counter({
name: `${prefix}_db_queries_total`,
help: 'Total number of database queries',
labelNames: ['kind'],
});
export const dbEventsCounter: Counter<'kind'> = new Counter({
name: `${prefix}_db_events_total`,
help: 'Total number of database inserts',
labelNames: ['kind'],
});
export const dbPoolSizeGauge: Gauge = new Gauge({
name: `${prefix}_db_pool_size`,
help: 'Number of connections in the database pool',
});
export const dbAvailableConnectionsGauge: Gauge = new Gauge({
name: `${prefix}_db_available_connections`,
help: 'Number of available connections in the database pool',
});
export const dbQueryDurationHistogram: Histogram = new Histogram({
name: `${prefix}_db_query_duration_seconds`,
help: 'Duration of database queries',
});
export const cachedFaviconsSizeGauge: Gauge = new Gauge({
name: `${prefix}_cached_favicons_size`,
help: 'Number of domain favicons in cache',
});
export const cachedLnurlsSizeGauge: Gauge = new Gauge({
name: `${prefix}_cached_lnurls_size`,
help: 'Number of LNURL details in cache',
});
export const cachedNip05sSizeGauge: Gauge = new Gauge({
name: `${prefix}_cached_nip05s_size`,
help: 'Number of NIP-05 results in cache',
});
export const cachedLinkPreviewSizeGauge: Gauge = new Gauge({
name: `${prefix}_cached_link_previews_size`,
help: 'Number of link previews in cache',
});
export const cachedTranslationsSizeGauge: Gauge = new Gauge({
name: `${prefix}_cached_translations_size`,
help: 'Number of translated statuses in cache',
});
export const internalSubscriptionsSizeGauge: Gauge = new Gauge({
name: `${prefix}_internal_subscriptions_size`,
help: "Number of active subscriptions to Ditto's internal relay",
});
export const relayPoolRelaysSizeGauge: Gauge<'ready_state'> = new Gauge({
name: `${prefix}_relay_pool_relays_size`,
help: 'Number of relays in the relay pool',
labelNames: ['ready_state'],
});
export const relayPoolSubscriptionsSizeGauge: Gauge = new Gauge({
name: `${prefix}_relay_pool_subscriptions_size`,
help: 'Number of active subscriptions to the relay pool',
});
export const webPushNotificationsCounter: Counter<'type'> = new Counter({
name: `${prefix}_web_push_notifications_total`,
help: 'Total number of Web Push notifications sent',
labelNames: ['type'],
});

View file

@ -1,10 +1,10 @@
import { JsonParseStream } from '@std/json/json-parse-stream'; import { JsonParseStream } from '@std/json/json-parse-stream';
import { TextLineStream } from '@std/streams/text-line-stream'; import { TextLineStream } from '@std/streams/text-line-stream';
import { AdminSigner } from '@/signers/AdminSigner.ts'; import { AdminSigner } from '../packages/ditto/signers/AdminSigner.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '../packages/ditto/storages.ts';
import { type EventStub } from '@/utils/api.ts'; import { type EventStub } from '../packages/ditto/utils/api.ts';
import { nostrNow } from '@/utils.ts'; import { nostrNow } from '../packages/ditto/utils.ts';
const signer = new AdminSigner(); const signer = new AdminSigner();
const store = await Storages.db(); const store = await Storages.db();

View file

@ -1,9 +1,9 @@
import { NSchema } from '@nostrify/nostrify'; import { NSchema } from '@nostrify/nostrify';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { AdminSigner } from '@/signers/AdminSigner.ts'; import { AdminSigner } from '../packages/ditto/signers/AdminSigner.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '../packages/ditto/storages.ts';
import { nostrNow } from '@/utils.ts'; import { nostrNow } from '../packages/ditto/utils.ts';
const store = await Storages.db(); const store = await Storages.db();

View file

@ -1,4 +1,5 @@
import { assertEquals, assertThrows } from '@std/assert'; import { assertEquals, assertThrows } from '@std/assert';
import { buildFilter } from './db-export.ts'; import { buildFilter } from './db-export.ts';
Deno.test('buildFilter should return an empty filter when no arguments are provided', () => { Deno.test('buildFilter should return an empty filter when no arguments are provided', () => {

View file

@ -1,7 +1,8 @@
import { Storages } from '@/storages.ts';
import { NostrFilter } from '@nostrify/nostrify'; import { NostrFilter } from '@nostrify/nostrify';
import { Command, InvalidOptionArgumentError } from 'commander'; import { Command, InvalidOptionArgumentError } from 'commander';
import { Storages } from '../packages/ditto/storages.ts';
interface ExportFilter { interface ExportFilter {
authors?: string[]; authors?: string[];
ids?: string[]; ids?: string[];

View file

@ -3,8 +3,8 @@ import { NostrEvent } from '@nostrify/nostrify';
import { JsonParseStream } from '@std/json/json-parse-stream'; import { JsonParseStream } from '@std/json/json-parse-stream';
import { TextLineStream } from '@std/streams/text-line-stream'; import { TextLineStream } from '@std/streams/text-line-stream';
import { Conf } from '@/config.ts'; import { Conf } from '../packages/ditto/config.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '../packages/ditto/storages.ts';
const store = await Storages.db(); const store = await Storages.db();
const sem = new Semaphore(Conf.pg.poolSize); const sem = new Semaphore(Conf.pg.poolSize);

View file

@ -1,4 +1,4 @@
import { Storages } from '@/storages.ts'; import { Storages } from '../packages/ditto/storages.ts';
// This migrates kysely internally. // This migrates kysely internally.
const kysely = await Storages.kysely(); const kysely = await Storages.kysely();

Some files were not shown because too many files have changed in this diff Show more