mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
feat(api): ✨ Add profile fields with emojis and Markdown to users
This commit is contained in:
parent
6373b8ae78
commit
cde106a5db
1
drizzle/0020_giant_the_stranger.sql
Normal file
1
drizzle/0020_giant_the_stranger.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE "Users" ADD COLUMN "fields" jsonb DEFAULT '[]' NOT NULL;
|
||||||
1800
drizzle/meta/0020_snapshot.json
Normal file
1800
drizzle/meta/0020_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,146 +1,153 @@
|
||||||
{
|
{
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"dialect": "pg",
|
"dialect": "pg",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1712805159664,
|
"when": 1712805159664,
|
||||||
"tag": "0000_illegal_living_lightning",
|
"tag": "0000_illegal_living_lightning",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713055774123,
|
"when": 1713055774123,
|
||||||
"tag": "0001_salty_night_thrasher",
|
"tag": "0001_salty_night_thrasher",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 2,
|
"idx": 2,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713056370431,
|
"when": 1713056370431,
|
||||||
"tag": "0002_stiff_ares",
|
"tag": "0002_stiff_ares",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 3,
|
"idx": 3,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713056528340,
|
"when": 1713056528340,
|
||||||
"tag": "0003_spicy_arachne",
|
"tag": "0003_spicy_arachne",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 4,
|
"idx": 4,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713056712218,
|
"when": 1713056712218,
|
||||||
"tag": "0004_burly_lockjaw",
|
"tag": "0004_burly_lockjaw",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 5,
|
"idx": 5,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713056917973,
|
"when": 1713056917973,
|
||||||
"tag": "0005_sleepy_puma",
|
"tag": "0005_sleepy_puma",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 6,
|
"idx": 6,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713057159867,
|
"when": 1713057159867,
|
||||||
"tag": "0006_messy_network",
|
"tag": "0006_messy_network",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 7,
|
"idx": 7,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713227918208,
|
"when": 1713227918208,
|
||||||
"tag": "0007_naive_sleeper",
|
"tag": "0007_naive_sleeper",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 8,
|
"idx": 8,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713246700119,
|
"when": 1713246700119,
|
||||||
"tag": "0008_flawless_brother_voodoo",
|
"tag": "0008_flawless_brother_voodoo",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 9,
|
"idx": 9,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713327832438,
|
"when": 1713327832438,
|
||||||
"tag": "0009_easy_slyde",
|
"tag": "0009_easy_slyde",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 10,
|
"idx": 10,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713327880929,
|
"when": 1713327880929,
|
||||||
"tag": "0010_daffy_frightful_four",
|
"tag": "0010_daffy_frightful_four",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 11,
|
"idx": 11,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713333611707,
|
"when": 1713333611707,
|
||||||
"tag": "0011_special_the_fury",
|
"tag": "0011_special_the_fury",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 12,
|
"idx": 12,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713336108114,
|
"when": 1713336108114,
|
||||||
"tag": "0012_certain_thor_girl",
|
"tag": "0012_certain_thor_girl",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 13,
|
"idx": 13,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713336611301,
|
"when": 1713336611301,
|
||||||
"tag": "0013_wandering_celestials",
|
"tag": "0013_wandering_celestials",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 14,
|
"idx": 14,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713389937821,
|
"when": 1713389937821,
|
||||||
"tag": "0014_wonderful_sandman",
|
"tag": "0014_wonderful_sandman",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 15,
|
"idx": 15,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713399438164,
|
"when": 1713399438164,
|
||||||
"tag": "0015_easy_mojo",
|
"tag": "0015_easy_mojo",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 16,
|
"idx": 16,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713413369623,
|
"when": 1713413369623,
|
||||||
"tag": "0016_keen_mindworm",
|
"tag": "0016_keen_mindworm",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 17,
|
"idx": 17,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713417089150,
|
"when": 1713417089150,
|
||||||
"tag": "0017_dusty_black_knight",
|
"tag": "0017_dusty_black_knight",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 18,
|
"idx": 18,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713418575392,
|
"when": 1713418575392,
|
||||||
"tag": "0018_rapid_hairball",
|
"tag": "0018_rapid_hairball",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 19,
|
"idx": 19,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1713421706451,
|
"when": 1713421706451,
|
||||||
"tag": "0019_mushy_lorna_dane",
|
"tag": "0019_mushy_lorna_dane",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
},
|
||||||
]
|
{
|
||||||
|
"idx": 20,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1714017186457,
|
||||||
|
"tag": "0020_giant_the_stranger",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
uuid,
|
uuid,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import type { Source as APISource } from "~types/mastodon/source";
|
import type { Source as APISource } from "~types/mastodon/source";
|
||||||
|
import type * as Lysand from "lysand-types";
|
||||||
|
|
||||||
export const Emojis = pgTable("Emojis", {
|
export const Emojis = pgTable("Emojis", {
|
||||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||||
|
|
@ -362,6 +363,12 @@ export const Users = pgTable(
|
||||||
email: text("email"),
|
email: text("email"),
|
||||||
note: text("note").default("").notNull(),
|
note: text("note").default("").notNull(),
|
||||||
isAdmin: boolean("is_admin").default(false).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<{
|
endpoints: jsonb("endpoints").$type<Partial<{
|
||||||
dislikes: string;
|
dislikes: string;
|
||||||
featured: string;
|
featured: string;
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,7 @@ export class User {
|
||||||
inbox: data.inbox,
|
inbox: data.inbox,
|
||||||
outbox: data.outbox,
|
outbox: data.outbox,
|
||||||
},
|
},
|
||||||
|
fields: data.fields ?? [],
|
||||||
updatedAt: new Date(data.created_at).toISOString(),
|
updatedAt: new Date(data.created_at).toISOString(),
|
||||||
instanceId: instance.id,
|
instanceId: instance.id,
|
||||||
avatar: data.avatar
|
avatar: data.avatar
|
||||||
|
|
@ -312,6 +313,7 @@ export class User {
|
||||||
header: data.header ?? config.defaults.avatar,
|
header: data.header ?? config.defaults.avatar,
|
||||||
isAdmin: data.admin ?? false,
|
isAdmin: data.admin ?? false,
|
||||||
publicKey: keys.public_key,
|
publicKey: keys.public_key,
|
||||||
|
fields: [],
|
||||||
privateKey: keys.private_key,
|
privateKey: keys.private_key,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
source: {
|
source: {
|
||||||
|
|
@ -373,8 +375,10 @@ export class User {
|
||||||
following_count: user.followingCount,
|
following_count: user.followingCount,
|
||||||
statuses_count: user.statusCount,
|
statuses_count: user.statusCount,
|
||||||
emojis: user.emojis.map((emoji) => emojiToAPI(emoji)),
|
emojis: user.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||||
// TODO: Add fields
|
fields: user.fields.map((field) => ({
|
||||||
fields: [],
|
name: htmlToText(getBestContentType(field.key).content),
|
||||||
|
value: getBestContentType(field.value).content,
|
||||||
|
})),
|
||||||
bot: user.isBot,
|
bot: user.isBot,
|
||||||
source: isOwnAccount ? user.source : undefined,
|
source: isOwnAccount ? user.source : undefined,
|
||||||
// TODO: Add static avatar and header
|
// TODO: Add static avatar and header
|
||||||
|
|
@ -450,24 +454,7 @@ export class User {
|
||||||
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
|
avatar: urlToContentFormat(this.getAvatarUrl(config)) ?? undefined,
|
||||||
header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined,
|
header: urlToContentFormat(this.getHeaderUrl(config)) ?? undefined,
|
||||||
display_name: user.displayName,
|
display_name: user.displayName,
|
||||||
fields: user.source.fields.map((field) => ({
|
fields: user.fields,
|
||||||
key: {
|
|
||||||
"text/html": {
|
|
||||||
content: field.name,
|
|
||||||
},
|
|
||||||
"text/plain": {
|
|
||||||
content: htmlToText(field.name),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
"text/html": {
|
|
||||||
content: field.value,
|
|
||||||
},
|
|
||||||
"text/plain": {
|
|
||||||
content: htmlToText(field.value),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
public_key: {
|
public_key: {
|
||||||
actor: new URL(
|
actor: new URL(
|
||||||
`/users/${user.id}`,
|
`/users/${user.id}`,
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@ export const createServer = (
|
||||||
headers: {
|
headers: {
|
||||||
// Include for SSR
|
// Include for SSR
|
||||||
"X-Forwarded-Host": `${config.http.bind}:${config.http.bind_port}`,
|
"X-Forwarded-Host": `${config.http.bind}:${config.http.bind_port}`,
|
||||||
|
"Accept-Encoding": "identity",
|
||||||
},
|
},
|
||||||
}).catch(async (e) => {
|
}).catch(async (e) => {
|
||||||
await logger.logError(
|
await logger.logError(
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import type { MediaBackend } from "media-manager";
|
||||||
import { LocalMediaBackend, S3MediaBackend } from "media-manager";
|
import { LocalMediaBackend, S3MediaBackend } from "media-manager";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { getUrl } from "~database/entities/Attachment";
|
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 { contentToHtml } from "~database/entities/Status";
|
||||||
import { db } from "~drizzle/db";
|
import { db } from "~drizzle/db";
|
||||||
import { EmojiToUser, Users } from "~drizzle/schema";
|
import { EmojiToUser, Users } from "~drizzle/schema";
|
||||||
|
|
@ -40,12 +40,25 @@ export const schema = z.object({
|
||||||
locked: z.boolean().optional(),
|
locked: z.boolean().optional(),
|
||||||
bot: z.boolean().optional(),
|
bot: z.boolean().optional(),
|
||||||
discoverable: z.boolean().optional(),
|
discoverable: z.boolean().optional(),
|
||||||
"source[privacy]": z
|
source: z
|
||||||
.enum(["public", "unlisted", "private", "direct"])
|
.object({
|
||||||
|
privacy: z
|
||||||
|
.enum(["public", "unlisted", "private", "direct"])
|
||||||
|
.optional(),
|
||||||
|
sensitive: z.boolean().optional(),
|
||||||
|
language: z
|
||||||
|
.enum(ISO6391.getAllCodes() as [string, ...string[]])
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
"source[sensitive]": z.boolean().optional(),
|
fields_attributes: z
|
||||||
"source[language]": z
|
.array(
|
||||||
.enum(ISO6391.getAllCodes() as [string, ...string[]])
|
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(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -66,9 +79,8 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
locked,
|
locked,
|
||||||
bot,
|
bot,
|
||||||
discoverable,
|
discoverable,
|
||||||
"source[privacy]": source_privacy,
|
source,
|
||||||
"source[sensitive]": source_sensitive,
|
fields_attributes,
|
||||||
"source[language]": source_language,
|
|
||||||
} = extraData.parsedRequest;
|
} = extraData.parsedRequest;
|
||||||
|
|
||||||
const sanitizedNote = await sanitizeHtml(note ?? "");
|
const sanitizedNote = await sanitizeHtml(note ?? "");
|
||||||
|
|
@ -133,16 +145,16 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source_privacy && self.source) {
|
if (source?.privacy) {
|
||||||
self.source.privacy = source_privacy;
|
self.source.privacy = source.privacy;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source_sensitive && self.source) {
|
if (source?.sensitive) {
|
||||||
self.source.sensitive = source_sensitive;
|
self.source.sensitive = source.sensitive;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source_language && self.source) {
|
if (source?.language) {
|
||||||
self.source.language = source_language;
|
self.source.language = source.language;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
|
|
@ -185,11 +197,57 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
self.isDiscoverable = discoverable;
|
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
|
// Parse emojis
|
||||||
const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
|
const displaynameEmojis = await parseEmojis(sanitizedDisplayName);
|
||||||
const noteEmojis = await parseEmojis(sanitizedNote);
|
const noteEmojis = await parseEmojis(sanitizedNote);
|
||||||
|
|
||||||
self.emojis = [...displaynameEmojis, ...noteEmojis];
|
self.emojis = [...displaynameEmojis, ...noteEmojis, ...fieldEmojis];
|
||||||
|
|
||||||
// Deduplicate emojis
|
// Deduplicate emojis
|
||||||
self.emojis = self.emojis.filter(
|
self.emojis = self.emojis.filter(
|
||||||
|
|
@ -204,6 +262,7 @@ export default apiRoute<typeof meta, typeof schema>(
|
||||||
note: self.note,
|
note: self.note,
|
||||||
avatar: self.avatar,
|
avatar: self.avatar,
|
||||||
header: self.header,
|
header: self.header,
|
||||||
|
fields: self.fields,
|
||||||
isLocked: self.isLocked,
|
isLocked: self.isLocked,
|
||||||
isBot: self.isBot,
|
isBot: self.isBot,
|
||||||
isDiscoverable: self.isDiscoverable,
|
isDiscoverable: self.isDiscoverable,
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
urls: {
|
urls: {
|
||||||
streaming_api: "",
|
streaming_api: "",
|
||||||
},
|
},
|
||||||
version: `4.3.0+glitch (compatible; Mastodon ${version}})`,
|
version: "4.3.0",
|
||||||
pleroma: {
|
pleroma: {
|
||||||
metadata: {
|
metadata: {
|
||||||
account_activation_required: false,
|
account_activation_required: false,
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
domain: new URL(config.http.base_url).hostname,
|
domain: new URL(config.http.base_url).hostname,
|
||||||
title: config.instance.name,
|
title: config.instance.name,
|
||||||
version: `4.3.0+glitch (compatible; Mastodon ${version}})`,
|
version: "4.3.0",
|
||||||
source_url: "https://github.com/lysand-org/lysand",
|
source_url: "https://github.com/lysand-org/lysand",
|
||||||
description: config.instance.description,
|
description: config.instance.description,
|
||||||
usage: {
|
usage: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue