Merge branch 'fe-config' into 'develop'
Implement frontend configurations Closes #45 See merge request soapbox-pub/ditto!34
This commit is contained in:
commit
c54f9aa7b1
|
@ -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);
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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'));
|
||||||
|
|
|
@ -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 };
|
|
@ -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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
Loading…
Reference in New Issue