Implement frontend configurations

This commit is contained in:
Alex Gleason 2023-09-03 18:49:45 -05:00
parent 2f645920f5
commit 7686371183
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
8 changed files with 123 additions and 12 deletions

View File

@ -30,7 +30,7 @@ import { emptyArrayController, emptyObjectController } from './controllers/api/f
import { instanceController } from './controllers/api/instance.ts';
import { notificationsController } from './controllers/api/notifications.ts';
import { createTokenController, oauthAuthorizeController, oauthController } from './controllers/api/oauth.ts';
import { frontendConfigController } from './controllers/api/pleroma.ts';
import { frontendConfigController, updateConfigController } from './controllers/api/pleroma.ts';
import { preferencesController } from './controllers/api/preferences.ts';
import { relayController } from './controllers/nostr/relay.ts';
import { searchController } from './controllers/api/search.ts';
@ -55,7 +55,7 @@ import { nodeInfoController, nodeInfoSchemaController } from './controllers/well
import { nostrController } from './controllers/well-known/nostr.ts';
import { webfingerController } from './controllers/well-known/webfinger.ts';
import { auth19, requirePubkey } from './middleware/auth19.ts';
import { auth98 } from './middleware/auth98.ts';
import { auth98, requireAdmin } from './middleware/auth98.ts';
interface AppEnv extends HonoEnv {
Variables: {
@ -136,6 +136,8 @@ app.get('/api/v1/trends', trendingTagsController);
app.get('/api/v1/notifications', requirePubkey, notificationsController);
app.get('/api/v1/favourites', requirePubkey, favouritesController);
app.post('/api/v1/pleroma/admin/config', requireAdmin, updateConfigController);
// Not (yet) implemented.
app.get('/api/v1/bookmarks', emptyArrayController);
app.get('/api/v1/custom_emojis', emptyArrayController);

View File

@ -1,7 +1,46 @@
import { type AppController } from '@/app.ts';
import * as eventsDB from '@/db/events.ts';
import { z } from '@/deps.ts';
import { configSchema, elixirTupleSchema } from '@/schemas/pleroma-api.ts';
import { createAdminEvent } from '@/utils/web.ts';
import { Conf } from '@/config.ts';
const frontendConfigController: AppController = async (c) => {
const [event] = await eventsDB.getFilters(
[{ kinds: [30078], authors: [Conf.pubkey], limit: 1 }],
);
if (event) {
const data = JSON.parse(event.content);
return c.json(data);
}
const frontendConfigController: AppController = (c) => {
return c.json({});
};
export { frontendConfigController };
/** Pleroma admin config controller. */
const updateConfigController: AppController = async (c) => {
const json = await c.req.json();
const { configs } = z.object({ configs: z.array(configSchema) }).parse(json);
for (const { group, key, value } of configs) {
if (group === ':pleroma' && key === ':frontend_configurations') {
const schema = elixirTupleSchema.transform(({ tuple }) => tuple).array();
const data = schema.parse(value).reduce<Record<string, unknown>>((result, [name, data]) => {
result[name.replace(/^:/, '')] = data;
return result;
}, {});
await createAdminEvent({
kind: 30078,
content: JSON.stringify(data),
tags: [['d', 'pub.ditto.frontendConfig']],
}, c);
}
}
return c.json([]);
};
export { frontendConfigController, updateConfigController };

View File

@ -39,7 +39,7 @@ interface UserRow {
pubkey: string;
username: string;
inserted_at: Date;
admin: boolean;
admin: 0 | 1;
}
interface RelayRow {

View File

@ -2,6 +2,13 @@ import { type Insertable } from '@/deps.ts';
import { db, type UserRow } from '../db.ts';
interface User {
pubkey: string;
username: string;
inserted_at: Date;
admin: boolean;
}
/** Adds a user to the database. */
function insertUser(user: Insertable<UserRow>) {
return db.insertInto('users').values(user).execute();
@ -14,14 +21,21 @@ function insertUser(user: Insertable<UserRow>) {
* await findUser({ username: 'alex' });
* ```
*/
function findUser(user: Partial<Insertable<UserRow>>) {
async function findUser(user: Partial<Insertable<UserRow>>): Promise<User | undefined> {
let query = db.selectFrom('users').selectAll();
for (const [key, value] of Object.entries(user)) {
query = query.where(key as keyof UserRow, '=', value);
}
return query.executeTakeFirst();
const row = await query.executeTakeFirst();
if (row) {
return {
...row,
admin: row.admin === 1,
};
}
}
export { findUser, insertUser };
export { findUser, insertUser, type User };

View File

@ -1,3 +1,4 @@
import { Conf } from '@/config.ts';
import * as eventsDB from '@/db/events.ts';
import { addRelays } from '@/db/relays.ts';
import { findUser } from '@/db/users.ts';
@ -42,10 +43,13 @@ async function getEventData({ pubkey }: Event): Promise<EventData> {
return { user };
}
/** Check if the pubkey is the `DITTO_NSEC` pubkey. */
const isAdminEvent = ({ pubkey }: Event): boolean => pubkey === Conf.pubkey;
/** Maybe store the event, if eligible. */
async function storeEvent(event: Event, data: EventData): Promise<void> {
if (isEphemeralKind(event.kind)) return;
if (data.user || await isLocallyFollowed(event.pubkey)) {
if (data.user || isAdminEvent(event) || await isLocallyFollowed(event.pubkey)) {
await eventsDB.insertEvent(event).catch(console.warn);
} else {
return Promise.reject(new RelayError('blocked', 'only registered users can post'));

View File

@ -0,0 +1,46 @@
import { z } from '@/deps.ts';
type ElixirValue =
| string
| number
| boolean
| null
| ElixirTuple
| ElixirValue[]
| { [key: string]: ElixirValue };
interface ElixirTuple {
tuple: [string, ElixirValue];
}
interface Config {
group: string;
key: string;
value: ElixirValue;
}
const baseElixirValueSchema: z.ZodType<ElixirValue> = z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.lazy(() => elixirValueSchema.array()),
z.lazy(() => z.record(z.string(), elixirValueSchema)),
]);
const elixirTupleSchema: z.ZodType<ElixirTuple> = z.object({
tuple: z.tuple([z.string(), z.lazy(() => elixirValueSchema)]),
});
const elixirValueSchema: z.ZodType<ElixirValue> = z.union([
baseElixirValueSchema,
elixirTupleSchema,
]);
const configSchema: z.ZodType<Config> = z.object({
group: z.string(),
key: z.string(),
value: elixirValueSchema,
});
export { type Config, configSchema, type ElixirTuple, elixirTupleSchema, type ElixirValue };

View File

@ -9,6 +9,7 @@ import { emojiTagSchema, filteredArray } from '@/schema.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { isFollowing, type Nip05, nostrDate, parseNip05, Time } from '@/utils.ts';
import { verifyNip05Cached } from '@/utils/nip05.ts';
import { findUser } from '@/db/users.ts';
const DEFAULT_AVATAR = 'https://gleasonator.com/images/avi.png';
const DEFAULT_BANNER = 'https://gleasonator.com/images/banner.png';
@ -24,7 +25,8 @@ async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) {
const { name, nip05, picture, banner, about } = jsonMetaContentSchema.parse(event.content);
const npub = nip19.npubEncode(pubkey);
const [parsed05, followersCount, followingCount, statusesCount] = await Promise.all([
const [user, parsed05, followersCount, followingCount, statusesCount] = await Promise.all([
findUser({ pubkey }),
parseAndVerifyNip05(nip05, pubkey),
eventsDB.countFilters([{ kinds: [3], '#p': [pubkey] }]),
getFollowedPubkeys(pubkey).then((pubkeys) => pubkeys.length),
@ -65,6 +67,10 @@ async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) {
statuses_count: statusesCount,
url: Conf.local(`/users/${pubkey}`),
username: parsed05?.nickname || npub.substring(0, 8),
pleroma: {
is_admin: user?.admin || false,
is_moderator: user?.admin || false,
},
};
}

View File

@ -1,6 +1,6 @@
import { UserRow } from '@/db.ts';
import { User } from '@/db/users.ts';
interface EventData {
user: UserRow | undefined;
user: User | undefined;
}
export type { EventData };