feat(api): Add profile fields with emojis and Markdown to users

This commit is contained in:
Jesse Wierzbinski 2024-04-24 18:37:55 -10:00
parent 6373b8ae78
commit cde106a5db
No known key found for this signature in database
9 changed files with 2045 additions and 183 deletions

View file

@ -0,0 +1 @@
ALTER TABLE "Users" ADD COLUMN "fields" jsonb DEFAULT '[]' NOT NULL;

File diff suppressed because it is too large Load diff

View file

@ -1,146 +1,153 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1712805159664,
"tag": "0000_illegal_living_lightning",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1713055774123,
"tag": "0001_salty_night_thrasher",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1713056370431,
"tag": "0002_stiff_ares",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1713056528340,
"tag": "0003_spicy_arachne",
"breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1713056712218,
"tag": "0004_burly_lockjaw",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1713056917973,
"tag": "0005_sleepy_puma",
"breakpoints": true
},
{
"idx": 6,
"version": "5",
"when": 1713057159867,
"tag": "0006_messy_network",
"breakpoints": true
},
{
"idx": 7,
"version": "5",
"when": 1713227918208,
"tag": "0007_naive_sleeper",
"breakpoints": true
},
{
"idx": 8,
"version": "5",
"when": 1713246700119,
"tag": "0008_flawless_brother_voodoo",
"breakpoints": true
},
{
"idx": 9,
"version": "5",
"when": 1713327832438,
"tag": "0009_easy_slyde",
"breakpoints": true
},
{
"idx": 10,
"version": "5",
"when": 1713327880929,
"tag": "0010_daffy_frightful_four",
"breakpoints": true
},
{
"idx": 11,
"version": "5",
"when": 1713333611707,
"tag": "0011_special_the_fury",
"breakpoints": true
},
{
"idx": 12,
"version": "5",
"when": 1713336108114,
"tag": "0012_certain_thor_girl",
"breakpoints": true
},
{
"idx": 13,
"version": "5",
"when": 1713336611301,
"tag": "0013_wandering_celestials",
"breakpoints": true
},
{
"idx": 14,
"version": "5",
"when": 1713389937821,
"tag": "0014_wonderful_sandman",
"breakpoints": true
},
{
"idx": 15,
"version": "5",
"when": 1713399438164,
"tag": "0015_easy_mojo",
"breakpoints": true
},
{
"idx": 16,
"version": "5",
"when": 1713413369623,
"tag": "0016_keen_mindworm",
"breakpoints": true
},
{
"idx": 17,
"version": "5",
"when": 1713417089150,
"tag": "0017_dusty_black_knight",
"breakpoints": true
},
{
"idx": 18,
"version": "5",
"when": 1713418575392,
"tag": "0018_rapid_hairball",
"breakpoints": true
},
{
"idx": 19,
"version": "5",
"when": 1713421706451,
"tag": "0019_mushy_lorna_dane",
"breakpoints": true
}
]
}
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1712805159664,
"tag": "0000_illegal_living_lightning",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1713055774123,
"tag": "0001_salty_night_thrasher",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1713056370431,
"tag": "0002_stiff_ares",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1713056528340,
"tag": "0003_spicy_arachne",
"breakpoints": true
},
{
"idx": 4,
"version": "5",
"when": 1713056712218,
"tag": "0004_burly_lockjaw",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1713056917973,
"tag": "0005_sleepy_puma",
"breakpoints": true
},
{
"idx": 6,
"version": "5",
"when": 1713057159867,
"tag": "0006_messy_network",
"breakpoints": true
},
{
"idx": 7,
"version": "5",
"when": 1713227918208,
"tag": "0007_naive_sleeper",
"breakpoints": true
},
{
"idx": 8,
"version": "5",
"when": 1713246700119,
"tag": "0008_flawless_brother_voodoo",
"breakpoints": true
},
{
"idx": 9,
"version": "5",
"when": 1713327832438,
"tag": "0009_easy_slyde",
"breakpoints": true
},
{
"idx": 10,
"version": "5",
"when": 1713327880929,
"tag": "0010_daffy_frightful_four",
"breakpoints": true
},
{
"idx": 11,
"version": "5",
"when": 1713333611707,
"tag": "0011_special_the_fury",
"breakpoints": true
},
{
"idx": 12,
"version": "5",
"when": 1713336108114,
"tag": "0012_certain_thor_girl",
"breakpoints": true
},
{
"idx": 13,
"version": "5",
"when": 1713336611301,
"tag": "0013_wandering_celestials",
"breakpoints": true
},
{
"idx": 14,
"version": "5",
"when": 1713389937821,
"tag": "0014_wonderful_sandman",
"breakpoints": true
},
{
"idx": 15,
"version": "5",
"when": 1713399438164,
"tag": "0015_easy_mojo",
"breakpoints": true
},
{
"idx": 16,
"version": "5",
"when": 1713413369623,
"tag": "0016_keen_mindworm",
"breakpoints": true
},
{
"idx": 17,
"version": "5",
"when": 1713417089150,
"tag": "0017_dusty_black_knight",
"breakpoints": true
},
{
"idx": 18,
"version": "5",
"when": 1713418575392,
"tag": "0018_rapid_hairball",
"breakpoints": true
},
{
"idx": 19,
"version": "5",
"when": 1713421706451,
"tag": "0019_mushy_lorna_dane",
"breakpoints": true
},
{
"idx": 20,
"version": "5",
"when": 1714017186457,
"tag": "0020_giant_the_stranger",
"breakpoints": true
}
]
}

View file

@ -12,6 +12,7 @@ import {
uuid,
} from "drizzle-orm/pg-core";
import type { Source as APISource } from "~types/mastodon/source";
import type * as Lysand from "lysand-types";
export const Emojis = pgTable("Emojis", {
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
@ -362,6 +363,12 @@ export const Users = pgTable(
email: text("email"),
note: text("note").default("").notNull(),
isAdmin: boolean("is_admin").default(false).notNull(),
fields: jsonb("fields").notNull().default("[]").$type<
{
key: Lysand.ContentFormat;
value: Lysand.ContentFormat;
}[]
>(),
endpoints: jsonb("endpoints").$type<Partial<{
dislikes: string;
featured: string;

View file

@ -206,6 +206,7 @@ export class User {
inbox: data.inbox,
outbox: data.outbox,
},
fields: data.fields ?? [],
updatedAt: new Date(data.created_at).toISOString(),
instanceId: instance.id,
avatar: data.avatar
@ -312,6 +313,7 @@ export class User {
header: data.header ?? config.defaults.avatar,
isAdmin: data.admin ?? false,
publicKey: keys.public_key,
fields: [],
privateKey: keys.private_key,
updatedAt: new Date().toISOString(),
source: {
@ -373,8 +375,10 @@ export class User {
following_count: user.followingCount,
statuses_count: user.statusCount,
emojis: user.emojis.map((emoji) => emojiToAPI(emoji)),
// TODO: Add fields
fields: [],
fields: user.fields.map((field) => ({
name: htmlToText(getBestContentType(field.key).content),
value: getBestContentType(field.value).content,
})),
bot: user.isBot,
source: isOwnAccount ? user.source : undefined,
// TODO: Add static avatar and header
@ -450,24 +454,7 @@ export class User {
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined,
display_name: user.displayName,
fields: user.source.fields.map((field) => ({
key: {
"text/html": {
content: field.name,
},
"text/plain": {
content: htmlToText(field.name),
},
},
value: {
"text/html": {
content: field.value,
},
"text/plain": {
content: htmlToText(field.value),
},
},
})),
fields: user.fields,
public_key: {
actor: new URL(
`/users/${user.id}`,

View file

@ -190,6 +190,7 @@ export const createServer = (
headers: {
// Include for SSR
"X-Forwarded-Host": `${config.http.bind}:${config.http.bind_port}`,
"Accept-Encoding": "identity",
},
}).catch(async (e) => {
await logger.logError(

View file

@ -9,7 +9,7 @@ import type { MediaBackend } from "media-manager";
import { LocalMediaBackend, S3MediaBackend } from "media-manager";
import { z } from "zod";
import { getUrl } from "~database/entities/Attachment";
import { parseEmojis } from "~database/entities/Emoji";
import { parseEmojis, type EmojiWithInstance } from "~database/entities/Emoji";
import { contentToHtml } from "~database/entities/Status";
import { db } from "~drizzle/db";
import { EmojiToUser, Users } from "~drizzle/schema";
@ -40,12 +40,25 @@ export const schema = z.object({
locked: z.boolean().optional(),
bot: z.boolean().optional(),
discoverable: z.boolean().optional(),
"source[privacy]": z
.enum(["public", "unlisted", "private", "direct"])
source: z
.object({
privacy: z
.enum(["public", "unlisted", "private", "direct"])
.optional(),
sensitive: z.boolean().optional(),
language: z
.enum(ISO6391.getAllCodes() as [string, ...string[]])
.optional(),
})
.optional(),
"source[sensitive]": z.boolean().optional(),
"source[language]": z
.enum(ISO6391.getAllCodes() as [string, ...string[]])
fields_attributes: z
.array(
z.object({
name: z.string().max(config.validation.max_field_name_size),
value: z.string().max(config.validation.max_field_value_size),
}),
)
.max(config.validation.max_field_count)
.optional(),
});
@ -66,9 +79,8 @@ export default apiRoute<typeof meta, typeof schema>(
locked,
bot,
discoverable,
"source[privacy]": source_privacy,
"source[sensitive]": source_sensitive,
"source[language]": source_language,
source,
fields_attributes,
} = extraData.parsedRequest;
const sanitizedNote = await sanitizeHtml(note ?? "");
@ -133,16 +145,16 @@ export default apiRoute<typeof meta, typeof schema>(
});
}
if (source_privacy && self.source) {
self.source.privacy = source_privacy;
if (source?.privacy) {
self.source.privacy = source.privacy;
}
if (source_sensitive && self.source) {
self.source.sensitive = source_sensitive;
if (source?.sensitive) {
self.source.sensitive = source.sensitive;
}
if (source_language && self.source) {
self.source.language = source_language;
if (source?.language) {
self.source.language = source.language;
}
if (avatar) {
@ -185,11 +197,57 @@ export default apiRoute<typeof meta, typeof schema>(
self.isDiscoverable = discoverable;
}
const fieldEmojis: EmojiWithInstance[] = [];
if (fields_attributes) {
self.fields = [];
self.source.fields = [];
for (const field of fields_attributes) {
// Can be Markdown or plaintext, also has emojis
const parsedName = await contentToHtml({
"text/markdown": {
content: field.name,
},
});
const parsedValue = await contentToHtml({
"text/markdown": {
content: field.value,
},
});
// Parse emojis
const nameEmojis = await parseEmojis(parsedName);
const valueEmojis = await parseEmojis(parsedValue);
fieldEmojis.push(...nameEmojis, ...valueEmojis);
// Replace fields
self.fields.push({
key: {
"text/html": {
content: parsedName,
},
},
value: {
"text/html": {
content: parsedValue,
},
},
});
self.source.fields.push({
name: field.name,
value: field.value,
});
}
}
// Parse emojis
const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
const noteEmojis = await parseEmojis(sanitizedNote);
self.emojis = [...displaynameEmojis, ...noteEmojis];
self.emojis = [...displaynameEmojis, ...noteEmojis, ...fieldEmojis];
// Deduplicate emojis
self.emojis = self.emojis.filter(
@ -204,6 +262,7 @@ export default apiRoute<typeof meta, typeof schema>(
note: self.note,
avatar: self.avatar,
header: self.header,
fields: self.fields,
isLocked: self.isLocked,
isBot: self.isBot,
isDiscoverable: self.isDiscoverable,

View file

@ -114,7 +114,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
urls: {
streaming_api: "",
},
version: `4.3.0+glitch (compatible; Mastodon ${version}})`,
version: "4.3.0",
pleroma: {
metadata: {
account_activation_required: false,

View file

@ -52,7 +52,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
return jsonResponse({
domain: new URL(config.http.base_url).hostname,
title: config.instance.name,
version: `4.3.0+glitch (compatible; Mastodon ${version}})`,
version: "4.3.0",
source_url: "https://github.com/lysand-org/lysand",
description: config.instance.description,
usage: {