Merge branch 'fe-config' into 'develop'

Implement frontend configurations

Closes #45

See merge request soapbox-pub/ditto!34
This commit is contained in:
Alex Gleason 2023-09-04 00:14:40 +00:00
commit c54f9aa7b1
No known key found for this signature in database
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 { instanceController } from './controllers/api/instance.ts';
import { notificationsController } from './controllers/api/notifications.ts'; import { notificationsController } from './controllers/api/notifications.ts';
import { createTokenController, oauthAuthorizeController, oauthController } from './controllers/api/oauth.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 { preferencesController } from './controllers/api/preferences.ts';
import { relayController } from './controllers/nostr/relay.ts'; import { relayController } from './controllers/nostr/relay.ts';
import { searchController } from './controllers/api/search.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 { nostrController } from './controllers/well-known/nostr.ts';
import { webfingerController } from './controllers/well-known/webfinger.ts'; import { webfingerController } from './controllers/well-known/webfinger.ts';
import { auth19, requirePubkey } from './middleware/auth19.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 { interface AppEnv extends HonoEnv {
Variables: { Variables: {
@ -136,6 +136,8 @@ app.get('/api/v1/trends', trendingTagsController);
app.get('/api/v1/notifications', requirePubkey, notificationsController); app.get('/api/v1/notifications', requirePubkey, notificationsController);
app.get('/api/v1/favourites', requirePubkey, favouritesController); app.get('/api/v1/favourites', requirePubkey, favouritesController);
app.post('/api/v1/pleroma/admin/config', requireAdmin, updateConfigController);
// Not (yet) implemented. // Not (yet) implemented.
app.get('/api/v1/bookmarks', emptyArrayController); app.get('/api/v1/bookmarks', emptyArrayController);
app.get('/api/v1/custom_emojis', emptyArrayController); app.get('/api/v1/custom_emojis', emptyArrayController);

View File

@ -1,7 +1,46 @@
import { type AppController } from '@/app.ts'; 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({}); 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; pubkey: string;
username: string; username: string;
inserted_at: Date; inserted_at: Date;
admin: boolean; admin: 0 | 1;
} }
interface RelayRow { interface RelayRow {

View File

@ -2,6 +2,13 @@ import { type Insertable } from '@/deps.ts';
import { db, type UserRow } from '../db.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. */ /** Adds a user to the database. */
function insertUser(user: Insertable<UserRow>) { function insertUser(user: Insertable<UserRow>) {
return db.insertInto('users').values(user).execute(); return db.insertInto('users').values(user).execute();
@ -14,14 +21,21 @@ function insertUser(user: Insertable<UserRow>) {
* await findUser({ username: 'alex' }); * 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(); let query = db.selectFrom('users').selectAll();
for (const [key, value] of Object.entries(user)) { for (const [key, value] of Object.entries(user)) {
query = query.where(key as keyof UserRow, '=', value); 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 * as eventsDB from '@/db/events.ts';
import { addRelays } from '@/db/relays.ts'; import { addRelays } from '@/db/relays.ts';
import { findUser } from '@/db/users.ts'; import { findUser } from '@/db/users.ts';
@ -42,10 +43,13 @@ async function getEventData({ pubkey }: Event): Promise<EventData> {
return { user }; 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. */ /** Maybe store the event, if eligible. */
async function storeEvent(event: Event, data: EventData): Promise<void> { async function storeEvent(event: Event, data: EventData): Promise<void> {
if (isEphemeralKind(event.kind)) return; if (isEphemeralKind(event.kind)) return;
if (data.user || await isLocallyFollowed(event.pubkey)) { if (data.user || isAdminEvent(event) || await isLocallyFollowed(event.pubkey)) {
await eventsDB.insertEvent(event).catch(console.warn); await eventsDB.insertEvent(event).catch(console.warn);
} else { } else {
return Promise.reject(new RelayError('blocked', 'only registered users can post')); return Promise.reject(new RelayError('blocked', 'only registered users can post'));

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 { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { isFollowing, type Nip05, nostrDate, parseNip05, Time } from '@/utils.ts'; import { isFollowing, type Nip05, nostrDate, parseNip05, Time } from '@/utils.ts';
import { verifyNip05Cached } from '@/utils/nip05.ts'; import { verifyNip05Cached } from '@/utils/nip05.ts';
import { findUser } from '@/db/users.ts';
const DEFAULT_AVATAR = 'https://gleasonator.com/images/avi.png'; const DEFAULT_AVATAR = 'https://gleasonator.com/images/avi.png';
const DEFAULT_BANNER = 'https://gleasonator.com/images/banner.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 { name, nip05, picture, banner, about } = jsonMetaContentSchema.parse(event.content);
const npub = nip19.npubEncode(pubkey); 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), parseAndVerifyNip05(nip05, pubkey),
eventsDB.countFilters([{ kinds: [3], '#p': [pubkey] }]), eventsDB.countFilters([{ kinds: [3], '#p': [pubkey] }]),
getFollowedPubkeys(pubkey).then((pubkeys) => pubkeys.length), getFollowedPubkeys(pubkey).then((pubkeys) => pubkeys.length),
@ -65,6 +67,10 @@ async function toAccount(event: Event<0>, opts: ToAccountOpts = {}) {
statuses_count: statusesCount, statuses_count: statusesCount,
url: Conf.local(`/users/${pubkey}`), url: Conf.local(`/users/${pubkey}`),
username: parsed05?.nickname || npub.substring(0, 8), 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 { interface EventData {
user: UserRow | undefined; user: User | undefined;
} }
export type { EventData }; export type { EventData };