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 { 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);
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -39,7 +39,7 @@ interface UserRow {
|
|||
pubkey: string;
|
||||
username: string;
|
||||
inserted_at: Date;
|
||||
admin: boolean;
|
||||
admin: 0 | 1;
|
||||
}
|
||||
|
||||
interface RelayRow {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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 { 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in New Issue