mirror of
https://github.com/aljazceru/ditto.git
synced 2026-01-03 13:44:24 +01:00
Merge remote-tracking branch 'origin/main' into nip05-request
This commit is contained in:
13
src/app.ts
13
src/app.ts
@@ -26,7 +26,7 @@ import {
|
||||
updateCredentialsController,
|
||||
verifyCredentialsController,
|
||||
} from '@/controllers/api/accounts.ts';
|
||||
import { adminAccountAction, adminAccountsController } from '@/controllers/api/admin.ts';
|
||||
import { adminAccountsController, adminActionController } from '@/controllers/api/admin.ts';
|
||||
import { appCredentialsController, createAppController } from '@/controllers/api/apps.ts';
|
||||
import { blocksController } from '@/controllers/api/blocks.ts';
|
||||
import { bookmarksController } from '@/controllers/api/bookmarks.ts';
|
||||
@@ -42,6 +42,10 @@ import {
|
||||
configController,
|
||||
frontendConfigController,
|
||||
pleromaAdminDeleteStatusController,
|
||||
pleromaAdminSuggestController,
|
||||
pleromaAdminTagController,
|
||||
pleromaAdminUnsuggestController,
|
||||
pleromaAdminUntagController,
|
||||
updateConfigController,
|
||||
} from '@/controllers/api/pleroma.ts';
|
||||
import { preferencesController } from '@/controllers/api/preferences.ts';
|
||||
@@ -252,7 +256,12 @@ app.post(
|
||||
adminReportResolveController,
|
||||
);
|
||||
|
||||
app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requireSigner, requireRole('admin'), adminAccountAction);
|
||||
app.post('/api/v1/admin/accounts/:id{[0-9a-f]{64}}/action', requireSigner, requireRole('admin'), adminActionController);
|
||||
|
||||
app.put('/api/v1/pleroma/admin/users/tag', requireRole('admin'), pleromaAdminTagController);
|
||||
app.delete('/api/v1/pleroma/admin/users/tag', requireRole('admin'), pleromaAdminUntagController);
|
||||
app.patch('/api/v1/pleroma/admin/users/suggest', requireRole('admin'), pleromaAdminSuggestController);
|
||||
app.patch('/api/v1/pleroma/admin/users/unsuggest', requireRole('admin'), pleromaAdminUnsuggestController);
|
||||
|
||||
// Not (yet) implemented.
|
||||
app.get('/api/v1/custom_emojis', emptyArrayController);
|
||||
|
||||
@@ -5,8 +5,7 @@ import { type AppController } from '@/app.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
import { booleanParamSchema } from '@/schema.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { paginated, paginationSchema, parseBody, updateListAdminEvent } from '@/utils/api.ts';
|
||||
import { addTag } from '@/utils/tags.ts';
|
||||
import { paginated, paginationSchema, parseBody, updateUser } from '@/utils/api.ts';
|
||||
import { renderAdminAccount, renderAdminAccountFromPubkey } from '@/views/mastodon/admin-accounts.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
|
||||
@@ -91,7 +90,7 @@ const adminAccountActionSchema = z.object({
|
||||
type: z.enum(['none', 'sensitive', 'disable', 'silence', 'suspend']),
|
||||
});
|
||||
|
||||
const adminAccountAction: AppController = async (c) => {
|
||||
const adminActionController: AppController = async (c) => {
|
||||
const body = await parseBody(c.req.raw);
|
||||
const result = adminAccountActionSchema.safeParse(body);
|
||||
const authorId = c.req.param('id');
|
||||
@@ -102,17 +101,24 @@ const adminAccountAction: AppController = async (c) => {
|
||||
|
||||
const { data } = result;
|
||||
|
||||
if (data.type !== 'disable') {
|
||||
return c.json({ error: 'Record invalid' }, 422);
|
||||
const n: Record<string, boolean> = {};
|
||||
|
||||
if (data.type === 'sensitive') {
|
||||
n.sensitive = true;
|
||||
}
|
||||
if (data.type === 'disable') {
|
||||
n.disable = true;
|
||||
}
|
||||
if (data.type === 'silence') {
|
||||
n.silence = true;
|
||||
}
|
||||
if (data.type === 'suspend') {
|
||||
n.suspend = true;
|
||||
}
|
||||
|
||||
await updateListAdminEvent(
|
||||
{ kinds: [10000], authors: [Conf.pubkey], limit: 1 },
|
||||
(tags) => addTag(tags, ['p', authorId]),
|
||||
c,
|
||||
);
|
||||
await updateUser(authorId, n, c);
|
||||
|
||||
return c.json({}, 200);
|
||||
};
|
||||
|
||||
export { adminAccountAction, adminAccountsController };
|
||||
export { adminAccountsController, adminActionController };
|
||||
|
||||
@@ -6,7 +6,8 @@ import { Conf } from '@/config.ts';
|
||||
import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/pleroma-api.ts';
|
||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { createAdminEvent } from '@/utils/api.ts';
|
||||
import { createAdminEvent, updateAdminEvent, updateUser } from '@/utils/api.ts';
|
||||
import { lookupPubkey } from '@/utils/lookup.ts';
|
||||
|
||||
const frontendConfigController: AppController = async (c) => {
|
||||
const store = await Storages.db();
|
||||
@@ -87,4 +88,100 @@ async function getConfigs(store: NStore, signal: AbortSignal): Promise<PleromaCo
|
||||
}
|
||||
}
|
||||
|
||||
export { configController, frontendConfigController, pleromaAdminDeleteStatusController, updateConfigController };
|
||||
const pleromaAdminTagSchema = z.object({
|
||||
nicknames: z.string().array(),
|
||||
tags: z.string().array(),
|
||||
});
|
||||
|
||||
const pleromaAdminTagController: AppController = async (c) => {
|
||||
const params = pleromaAdminTagSchema.parse(await c.req.json());
|
||||
|
||||
for (const nickname of params.nicknames) {
|
||||
const pubkey = await lookupPubkey(nickname);
|
||||
if (!pubkey) continue;
|
||||
|
||||
await updateAdminEvent(
|
||||
{ kinds: [30382], authors: [Conf.pubkey], '#d': [pubkey], limit: 1 },
|
||||
(prev) => {
|
||||
const tags = prev?.tags ?? [['d', pubkey]];
|
||||
|
||||
for (const tag of params.tags) {
|
||||
const existing = prev?.tags.some(([name, value]) => name === 't' && value === tag);
|
||||
if (!existing) {
|
||||
tags.push(['t', tag]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 30382,
|
||||
content: prev?.content ?? '',
|
||||
tags,
|
||||
};
|
||||
},
|
||||
c,
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
};
|
||||
|
||||
const pleromaAdminUntagController: AppController = async (c) => {
|
||||
const params = pleromaAdminTagSchema.parse(await c.req.json());
|
||||
|
||||
for (const nickname of params.nicknames) {
|
||||
const pubkey = await lookupPubkey(nickname);
|
||||
if (!pubkey) continue;
|
||||
|
||||
await updateAdminEvent(
|
||||
{ kinds: [30382], authors: [Conf.pubkey], '#d': [pubkey], limit: 1 },
|
||||
(prev) => ({
|
||||
kind: 30382,
|
||||
content: prev?.content ?? '',
|
||||
tags: (prev?.tags ?? [['d', pubkey]])
|
||||
.filter(([name, value]) => !(name === 't' && params.tags.includes(value))),
|
||||
}),
|
||||
c,
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
};
|
||||
|
||||
const pleromaAdminSuggestSchema = z.object({
|
||||
nicknames: z.string().array(),
|
||||
});
|
||||
|
||||
const pleromaAdminSuggestController: AppController = async (c) => {
|
||||
const { nicknames } = pleromaAdminSuggestSchema.parse(await c.req.json());
|
||||
|
||||
for (const nickname of nicknames) {
|
||||
const pubkey = await lookupPubkey(nickname);
|
||||
if (!pubkey) continue;
|
||||
await updateUser(pubkey, { suggest: true }, c);
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
};
|
||||
|
||||
const pleromaAdminUnsuggestController: AppController = async (c) => {
|
||||
const { nicknames } = pleromaAdminSuggestSchema.parse(await c.req.json());
|
||||
|
||||
for (const nickname of nicknames) {
|
||||
const pubkey = await lookupPubkey(nickname);
|
||||
if (!pubkey) continue;
|
||||
await updateUser(pubkey, { suggest: false }, c);
|
||||
}
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
};
|
||||
|
||||
export {
|
||||
configController,
|
||||
frontendConfigController,
|
||||
pleromaAdminDeleteStatusController,
|
||||
pleromaAdminSuggestController,
|
||||
pleromaAdminTagController,
|
||||
pleromaAdminUnsuggestController,
|
||||
pleromaAdminUntagController,
|
||||
updateConfigController,
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, s
|
||||
const pubkey = await signer?.getPublicKey();
|
||||
|
||||
const filters: NostrFilter[] = [
|
||||
{ kinds: [3], authors: [Conf.pubkey], limit: 1 },
|
||||
{ kinds: [30382], authors: [Conf.pubkey], '#n': ['suggest'], limit },
|
||||
{ kinds: [1985], '#L': ['pub.ditto.trends'], '#l': [`#p`], authors: [Conf.pubkey], limit: 1 },
|
||||
];
|
||||
|
||||
@@ -42,8 +42,8 @@ async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, s
|
||||
|
||||
const events = await store.query(filters, { signal });
|
||||
|
||||
const [suggestedEvent, followsEvent, mutesEvent, trendingEvent] = [
|
||||
events.find((event) => matchFilter({ kinds: [3], authors: [Conf.pubkey] }, event)),
|
||||
const [userEvents, followsEvent, mutesEvent, trendingEvent] = [
|
||||
events.filter((event) => matchFilter({ kinds: [30382], authors: [Conf.pubkey], '#n': ['suggest'] }, event)),
|
||||
pubkey ? events.find((event) => matchFilter({ kinds: [3], authors: [pubkey] }, event)) : undefined,
|
||||
pubkey ? events.find((event) => matchFilter({ kinds: [10000], authors: [pubkey] }, event)) : undefined,
|
||||
events.find((event) =>
|
||||
@@ -51,8 +51,13 @@ async function renderV2Suggestions(c: AppContext, params: PaginatedListParams, s
|
||||
),
|
||||
];
|
||||
|
||||
const [suggested, trending, follows, mutes] = [
|
||||
getTagSet(suggestedEvent?.tags ?? [], 'p'),
|
||||
const suggested = new Set(
|
||||
userEvents
|
||||
.map((event) => event.tags.find(([name]) => name === 'd')?.[1])
|
||||
.filter((pubkey): pubkey is string => !!pubkey),
|
||||
);
|
||||
|
||||
const [trending, follows, mutes] = [
|
||||
getTagSet(trendingEvent?.tags ?? [], 'p'),
|
||||
getTagSet(followsEvent?.tags ?? [], 'p'),
|
||||
getTagSet(mutesEvent?.tags ?? [], 'p'),
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { NostrFilter } from '@nostrify/nostrify';
|
||||
import Debug from '@soapbox/stickynotes/debug';
|
||||
|
||||
import { Conf } from '@/config.ts';
|
||||
import * as pipeline from '@/pipeline.ts';
|
||||
import { AdminSigner } from '@/signers/AdminSigner.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
|
||||
const debug = Debug('ditto:users');
|
||||
|
||||
interface User {
|
||||
pubkey: string;
|
||||
inserted_at: Date;
|
||||
admin: boolean;
|
||||
}
|
||||
|
||||
function buildUserEvent(user: User) {
|
||||
const { origin, host } = Conf.url;
|
||||
const signer = new AdminSigner();
|
||||
|
||||
return signer.signEvent({
|
||||
kind: 30361,
|
||||
tags: [
|
||||
['d', user.pubkey],
|
||||
['role', user.admin ? 'admin' : 'user'],
|
||||
['origin', origin],
|
||||
// NIP-31: https://github.com/nostr-protocol/nips/blob/master/31.md
|
||||
['alt', `User's account was updated by the admins of ${host}`],
|
||||
],
|
||||
content: '',
|
||||
created_at: Math.floor(user.inserted_at.getTime() / 1000),
|
||||
});
|
||||
}
|
||||
|
||||
/** Adds a user to the database. */
|
||||
async function insertUser(user: User) {
|
||||
debug('insertUser', JSON.stringify(user));
|
||||
const event = await buildUserEvent(user);
|
||||
return pipeline.handleEvent(event, AbortSignal.timeout(1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a single user based on one or more properties.
|
||||
*
|
||||
* ```ts
|
||||
* await findUser({ username: 'alex' });
|
||||
* ```
|
||||
*/
|
||||
async function findUser(user: Partial<User>, signal?: AbortSignal): Promise<User | undefined> {
|
||||
const filter: NostrFilter = { kinds: [30361], authors: [Conf.pubkey], limit: 1 };
|
||||
|
||||
for (const [key, value] of Object.entries(user)) {
|
||||
switch (key) {
|
||||
case 'pubkey':
|
||||
filter['#d'] = [String(value)];
|
||||
break;
|
||||
case 'admin':
|
||||
filter['#role'] = [value ? 'admin' : 'user'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const store = await Storages.db();
|
||||
const [event] = await store.query([filter], { signal });
|
||||
|
||||
if (event) {
|
||||
return {
|
||||
pubkey: event.tags.find(([name]) => name === 'd')?.[1]!,
|
||||
inserted_at: new Date(event.created_at * 1000),
|
||||
admin: event.tags.find(([name]) => name === 'role')?.[1] === 'admin',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { buildUserEvent, findUser, insertUser, type User };
|
||||
@@ -2,8 +2,8 @@ import { NostrEvent } from '@nostrify/nostrify';
|
||||
import { HTTPException } from 'hono';
|
||||
|
||||
import { type AppContext, type AppMiddleware } from '@/app.ts';
|
||||
import { findUser, User } from '@/db/users.ts';
|
||||
import { ReadOnlySigner } from '@/signers/ReadOnlySigner.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
import { localRequest } from '@/utils/api.ts';
|
||||
import {
|
||||
buildAuthEventTemplate,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type ParseAuthRequestOpts,
|
||||
validateAuthEvent,
|
||||
} from '@/utils/nip98.ts';
|
||||
import { Conf } from '@/config.ts';
|
||||
|
||||
/**
|
||||
* NIP-98 auth.
|
||||
@@ -35,7 +36,14 @@ type UserRole = 'user' | 'admin';
|
||||
/** Require the user to prove their role before invoking the controller. */
|
||||
function requireRole(role: UserRole, opts?: ParseAuthRequestOpts): AppMiddleware {
|
||||
return withProof(async (_c, proof, next) => {
|
||||
const user = await findUser({ pubkey: proof.pubkey });
|
||||
const store = await Storages.db();
|
||||
|
||||
const [user] = await store.query([{
|
||||
kinds: [30382],
|
||||
authors: [Conf.pubkey],
|
||||
'#d': [proof.pubkey],
|
||||
limit: 1,
|
||||
}]);
|
||||
|
||||
if (user && matchesRole(user, role)) {
|
||||
await next();
|
||||
@@ -53,15 +61,8 @@ function requireProof(opts?: ParseAuthRequestOpts): AppMiddleware {
|
||||
}
|
||||
|
||||
/** Check whether the user fulfills the role. */
|
||||
function matchesRole(user: User, role: UserRole): boolean {
|
||||
switch (role) {
|
||||
case 'user':
|
||||
return true;
|
||||
case 'admin':
|
||||
return user.admin;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
function matchesRole(user: NostrEvent, role: UserRole): boolean {
|
||||
return user.tags.some(([tag, value]) => tag === 'n' && value === role);
|
||||
}
|
||||
|
||||
/** HOC to obtain proof in middleware. */
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { NKinds, NostrEvent, NSchema as n } from '@nostrify/nostrify';
|
||||
import { PipePolicy } from '@nostrify/nostrify/policies';
|
||||
import Debug from '@soapbox/stickynotes/debug';
|
||||
import { sql } from 'kysely';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
|
||||
import { Conf } from '@/config.ts';
|
||||
import { DittoDB } from '@/db/DittoDB.ts';
|
||||
import { deleteAttachedMedia } from '@/db/unattached-media.ts';
|
||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { MuteListPolicy } from '@/policies/MuteListPolicy.ts';
|
||||
import { RelayError } from '@/RelayError.ts';
|
||||
import { hydrateEvents } from '@/storages/hydrate.ts';
|
||||
import { Storages } from '@/storages.ts';
|
||||
@@ -35,6 +32,7 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise<void
|
||||
}
|
||||
if (!(await verifyEventWorker(event))) return;
|
||||
if (encounterEvent(event)) return;
|
||||
if (await existsInDB(event)) return;
|
||||
debug(`NostrEvent<${event.kind}> ${event.id}`);
|
||||
|
||||
if (event.kind !== 24133) {
|
||||
@@ -43,6 +41,15 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise<void
|
||||
|
||||
await hydrateEvent(event, signal);
|
||||
|
||||
const n = getTagSet(event.user?.tags ?? [], 'n');
|
||||
|
||||
if (n.has('disable')) {
|
||||
throw new RelayError('blocked', 'user is disabled');
|
||||
}
|
||||
if (n.has('suspend')) {
|
||||
throw new RelayError('blocked', 'user is suspended');
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
storeEvent(event, signal),
|
||||
parseMetadata(event, signal),
|
||||
@@ -54,13 +61,8 @@ async function handleEvent(event: DittoEvent, signal: AbortSignal): Promise<void
|
||||
async function policyFilter(event: NostrEvent): Promise<void> {
|
||||
const debug = Debug('ditto:policy');
|
||||
|
||||
const policy = new PipePolicy([
|
||||
new MuteListPolicy(Conf.pubkey, await Storages.admin()),
|
||||
policyWorker,
|
||||
]);
|
||||
|
||||
try {
|
||||
const result = await policy.call(event);
|
||||
const result = await policyWorker.call(event);
|
||||
debug(JSON.stringify(result));
|
||||
RelayError.assert(result);
|
||||
} catch (e) {
|
||||
@@ -84,6 +86,13 @@ function encounterEvent(event: NostrEvent): boolean {
|
||||
return encountered;
|
||||
}
|
||||
|
||||
/** Check if the event already exists in the database. */
|
||||
async function existsInDB(event: DittoEvent): Promise<boolean> {
|
||||
const store = await Storages.db();
|
||||
const events = await store.query([{ ids: [event.id], limit: 1 }]);
|
||||
return events.length > 0;
|
||||
}
|
||||
|
||||
/** Hydrate the event with the user, if applicable. */
|
||||
async function hydrateEvent(event: DittoEvent, signal: AbortSignal): Promise<void> {
|
||||
await hydrateEvents({ events: [event], store: await Storages.db(), signal });
|
||||
|
||||
@@ -3,15 +3,15 @@ import { RelayPoolWorker } from 'nostr-relaypool';
|
||||
|
||||
import { Conf } from '@/config.ts';
|
||||
import { DittoDB } from '@/db/DittoDB.ts';
|
||||
import { AdminStore } from '@/storages/AdminStore.ts';
|
||||
import { EventsDB } from '@/storages/EventsDB.ts';
|
||||
import { PoolStore } from '@/storages/pool-store.ts';
|
||||
import { SearchStore } from '@/storages/search-store.ts';
|
||||
import { InternalRelay } from '@/storages/InternalRelay.ts';
|
||||
import { UserStore } from '@/storages/UserStore.ts';
|
||||
|
||||
export class Storages {
|
||||
private static _db: Promise<EventsDB> | undefined;
|
||||
private static _admin: Promise<UserStore> | undefined;
|
||||
private static _admin: Promise<AdminStore> | undefined;
|
||||
private static _client: Promise<PoolStore> | undefined;
|
||||
private static _pubsub: Promise<InternalRelay> | undefined;
|
||||
private static _search: Promise<SearchStore> | undefined;
|
||||
@@ -28,9 +28,9 @@ export class Storages {
|
||||
}
|
||||
|
||||
/** Admin user storage. */
|
||||
public static async admin(): Promise<UserStore> {
|
||||
public static async admin(): Promise<AdminStore> {
|
||||
if (!this._admin) {
|
||||
this._admin = Promise.resolve(new UserStore(Conf.pubkey, await this.db()));
|
||||
this._admin = Promise.resolve(new AdminStore(await this.db()));
|
||||
}
|
||||
return this._admin;
|
||||
}
|
||||
|
||||
40
src/storages/AdminStore.ts
Normal file
40
src/storages/AdminStore.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify';
|
||||
|
||||
import { Conf } from '@/config.ts';
|
||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { getTagSet } from '@/utils/tags.ts';
|
||||
|
||||
/** A store that prevents banned users from being displayed. */
|
||||
export class AdminStore implements NStore {
|
||||
constructor(private store: NStore) {}
|
||||
|
||||
async event(event: NostrEvent, opts?: { signal?: AbortSignal }): Promise<void> {
|
||||
return await this.store.event(event, opts);
|
||||
}
|
||||
|
||||
async query(filters: NostrFilter[], opts: { signal?: AbortSignal; limit?: number } = {}): Promise<DittoEvent[]> {
|
||||
const events = await this.store.query(filters, opts);
|
||||
|
||||
const users = await this.store.query([{
|
||||
kinds: [30382],
|
||||
authors: [Conf.pubkey],
|
||||
'#d': events.map((event) => event.pubkey),
|
||||
limit: 1,
|
||||
}]);
|
||||
|
||||
return events.filter((event) => {
|
||||
const user = users.find(
|
||||
({ kind, pubkey, tags }) =>
|
||||
kind === 30382 && pubkey === Conf.pubkey && tags.find(([name]) => name === 'd')?.[1] === event.pubkey,
|
||||
);
|
||||
|
||||
const n = getTagSet(user?.tags ?? [], 'n');
|
||||
|
||||
if (n.has('disable') || n.has('suspend')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { RelayError } from '@/RelayError.ts';
|
||||
import { purifyEvent } from '@/storages/hydrate.ts';
|
||||
import { isNostrId, isURL } from '@/utils.ts';
|
||||
import { abortError } from '@/utils/abort.ts';
|
||||
import { getTagSet } from '@/utils/tags.ts';
|
||||
|
||||
/** Function to decide whether or not to index a tag. */
|
||||
type TagCondition = ({ event, count, value }: {
|
||||
@@ -27,6 +26,7 @@ class EventsDB implements NStore {
|
||||
|
||||
/** Conditions for when to index certain tags. */
|
||||
static tagConditions: Record<string, TagCondition> = {
|
||||
'a': ({ count }) => count < 15,
|
||||
'd': ({ event, count }) => count === 0 && NKinds.parameterizedReplaceable(event.kind),
|
||||
'e': ({ event, count, value }) => ((event.kind === 10003) || count < 15) && isNostrId(value),
|
||||
'L': ({ event, count }) => event.kind === 1985 || count === 0,
|
||||
@@ -39,8 +39,6 @@ class EventsDB implements NStore {
|
||||
'q': ({ event, count, value }) => count === 0 && event.kind === 1 && isNostrId(value),
|
||||
'r': ({ event, count, value }) => (event.kind === 1985 ? count < 20 : count < 3) && isURL(value),
|
||||
't': ({ event, count, value }) => (event.kind === 1985 ? count < 20 : count < 5) && value.length < 50,
|
||||
'name': ({ event, count }) => event.kind === 30361 && count === 0,
|
||||
'role': ({ event, count }) => event.kind === 30361 && count === 0,
|
||||
};
|
||||
|
||||
constructor(private kysely: Kysely<DittoTables>) {
|
||||
@@ -77,17 +75,62 @@ class EventsDB implements NStore {
|
||||
|
||||
/** Check if an event has been deleted by the admin. */
|
||||
private async isDeletedAdmin(event: NostrEvent): Promise<boolean> {
|
||||
const [deletion] = await this.query([
|
||||
const filters: NostrFilter[] = [
|
||||
{ kinds: [5], authors: [Conf.pubkey], '#e': [event.id], limit: 1 },
|
||||
]);
|
||||
return !!deletion;
|
||||
];
|
||||
|
||||
if (NKinds.replaceable(event.kind) || NKinds.parameterizedReplaceable(event.kind)) {
|
||||
const d = event.tags.find(([tag]) => tag === 'd')?.[1] ?? '';
|
||||
|
||||
filters.push({
|
||||
kinds: [5],
|
||||
authors: [Conf.pubkey],
|
||||
'#a': [`${event.kind}:${event.pubkey}:${d}`],
|
||||
since: event.created_at,
|
||||
limit: 1,
|
||||
});
|
||||
}
|
||||
|
||||
const events = await this.query(filters);
|
||||
return events.length > 0;
|
||||
}
|
||||
|
||||
/** The DITTO_NSEC can delete any event from the database. NDatabase already handles user deletions. */
|
||||
private async deleteEventsAdmin(event: NostrEvent): Promise<void> {
|
||||
if (event.kind === 5 && event.pubkey === Conf.pubkey) {
|
||||
const ids = getTagSet(event.tags, 'e');
|
||||
await this.remove([{ ids: [...ids] }]);
|
||||
const ids = new Set(event.tags.filter(([name]) => name === 'e').map(([_name, value]) => value));
|
||||
const addrs = new Set(event.tags.filter(([name]) => name === 'a').map(([_name, value]) => value));
|
||||
|
||||
const filters: NostrFilter[] = [];
|
||||
|
||||
if (ids.size) {
|
||||
filters.push({ ids: [...ids] });
|
||||
}
|
||||
|
||||
for (const addr of addrs) {
|
||||
const [k, pubkey, d] = addr.split(':');
|
||||
const kind = Number(k);
|
||||
|
||||
if (!(Number.isInteger(kind) && kind >= 0)) continue;
|
||||
if (!isNostrId(pubkey)) continue;
|
||||
if (d === undefined) continue;
|
||||
|
||||
const filter: NostrFilter = {
|
||||
kinds: [kind],
|
||||
authors: [pubkey],
|
||||
until: event.created_at,
|
||||
};
|
||||
|
||||
if (d) {
|
||||
filter['#d'] = [d];
|
||||
}
|
||||
|
||||
filters.push(filter);
|
||||
}
|
||||
|
||||
if (filters.length) {
|
||||
await this.remove(filters);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export function assembleEvents(
|
||||
|
||||
for (const event of a) {
|
||||
event.author = b.find((e) => matchFilter({ kinds: [0], authors: [event.pubkey] }, e));
|
||||
event.user = b.find((e) => matchFilter({ kinds: [30361], authors: [admin], '#d': [event.pubkey] }, e));
|
||||
event.user = b.find((e) => matchFilter({ kinds: [30382], authors: [admin], '#d': [event.pubkey] }, e));
|
||||
|
||||
if (event.kind === 1) {
|
||||
const id = findQuoteTag(event.tags)?.[1] || findQuoteInContent(event.content);
|
||||
@@ -201,7 +201,7 @@ function gatherUsers({ events, store, signal }: HydrateOpts): Promise<DittoEvent
|
||||
const pubkeys = new Set(events.map((event) => event.pubkey));
|
||||
|
||||
return store.query(
|
||||
[{ kinds: [30361], authors: [Conf.pubkey], '#d': [...pubkeys], limit: pubkeys.size }],
|
||||
[{ kinds: [30382], authors: [Conf.pubkey], '#d': [...pubkeys], limit: pubkeys.size }],
|
||||
{ signal },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,36 @@ async function updateAdminEvent<E extends EventStub>(
|
||||
return createAdminEvent(fn(prev), c);
|
||||
}
|
||||
|
||||
async function updateUser(pubkey: string, n: Record<string, boolean>, c: AppContext): Promise<NostrEvent> {
|
||||
const signer = new AdminSigner();
|
||||
const admin = await signer.getPublicKey();
|
||||
|
||||
return updateAdminEvent(
|
||||
{ kinds: [30382], authors: [admin], '#d': [pubkey], limit: 1 },
|
||||
(prev) => {
|
||||
const prevNames = prev?.tags.reduce((acc, [name, value]) => {
|
||||
if (name === 'n') acc[value] = true;
|
||||
return acc;
|
||||
}, {} as Record<string, boolean>);
|
||||
|
||||
const names = { ...prevNames, ...n };
|
||||
const nTags = Object.entries(names).filter(([, value]) => value).map(([name]) => ['n', name]);
|
||||
const other = prev?.tags.filter(([name]) => !['d', 'n'].includes(name)) ?? [];
|
||||
|
||||
return {
|
||||
kind: 30382,
|
||||
content: prev?.content ?? '',
|
||||
tags: [
|
||||
['d', pubkey],
|
||||
...nTags,
|
||||
...other,
|
||||
],
|
||||
};
|
||||
},
|
||||
c,
|
||||
);
|
||||
}
|
||||
|
||||
/** Push the event through the pipeline, rethrowing any RelayError. */
|
||||
async function publishEvent(event: NostrEvent, c: AppContext): Promise<NostrEvent> {
|
||||
debug('EVENT', event);
|
||||
@@ -264,7 +294,9 @@ export {
|
||||
type PaginationParams,
|
||||
paginationSchema,
|
||||
parseBody,
|
||||
updateAdminEvent,
|
||||
updateEvent,
|
||||
updateListAdminEvent,
|
||||
updateListEvent,
|
||||
updateUser,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Conf } from '@/config.ts';
|
||||
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||
import { getLnurl } from '@/utils/lnurl.ts';
|
||||
import { nip05Cache } from '@/utils/nip05.ts';
|
||||
import { getTagSet } from '@/utils/tags.ts';
|
||||
import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
|
||||
import { renderEmojis } from '@/views/mastodon/emojis.ts';
|
||||
|
||||
@@ -33,7 +34,7 @@ async function renderAccount(
|
||||
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const parsed05 = await parseAndVerifyNip05(nip05, pubkey);
|
||||
const role = event.user?.tags.find(([name]) => name === 'role')?.[1] ?? 'user';
|
||||
const names = getTagSet(event.user?.tags ?? [], 'n');
|
||||
|
||||
return {
|
||||
id: pubkey,
|
||||
@@ -74,13 +75,14 @@ async function renderAccount(
|
||||
username: parsed05?.nickname || npub.substring(0, 8),
|
||||
ditto: {
|
||||
accepts_zaps: Boolean(getLnurl({ lud06, lud16 })),
|
||||
is_registered: Boolean(event.user),
|
||||
},
|
||||
pleroma: {
|
||||
is_admin: role === 'admin',
|
||||
is_moderator: ['admin', 'moderator'].includes(role),
|
||||
is_admin: names.has('admin'),
|
||||
is_moderator: names.has('admin') || names.has('moderator'),
|
||||
is_suggested: names.has('suggest'),
|
||||
is_local: parsed05?.domain === Conf.url.host,
|
||||
settings_store: undefined as unknown,
|
||||
tags: [...getTagSet(event.user?.tags ?? [], 't')],
|
||||
},
|
||||
nostr: {
|
||||
pubkey,
|
||||
|
||||
Reference in New Issue
Block a user