[backend] Add mentions column to user_profile table

This commit is contained in:
Laura Hausmann 2023-10-14 18:26:49 +02:00
parent bc08d8c92b
commit 82e0ef7414
Signed by: zotan
GPG key ID: D044E84C5BE01605
12 changed files with 109 additions and 13 deletions

View file

@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddUserProfileMentions1697302438587 implements MigrationInterface {
name = 'AddUserProfileMentions1697302438587'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "mentions" jsonb NOT NULL DEFAULT '[]'`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "mentions"`);
}
}

View file

@ -10,4 +10,4 @@ export function extractMentions(
const mentions = mentionNodes.map((x) => x.props);
return mentions;
}
}

View file

@ -269,9 +269,11 @@ export class Note {
}
}
export type IMentionedRemoteUsers = {
export type IMentionedRemoteUser = {
uri: string;
url?: string;
username: string;
host: string;
}[];
};
export type IMentionedRemoteUsers = IMentionedRemoteUser[];

View file

@ -54,6 +54,11 @@ export class UserProfile {
verified?: boolean;
}[];
@Column("jsonb", {
default: [],
})
public mentions: IMentionedRemoteUsers;
@Column("varchar", {
length: 32,
nullable: true,
@ -257,3 +262,10 @@ export class UserProfile {
}
}
}
type IMentionedRemoteUsers = {
uri: string;
url?: string;
username: string;
host: string;
}[]

View file

@ -68,6 +68,7 @@ import { UserIp } from "./entities/user-ip.js";
import { NoteEdit } from "./entities/note-edit.js";
import { OAuthApp } from "@/models/entities/oauth-app.js";
import { OAuthToken } from "@/models/entities/oauth-token.js";
import { UserProfileRepository } from "@/models/repositories/user-profile.js";
export const Announcements = db.getRepository(Announcement);
export const AnnouncementReads = db.getRepository(AnnouncementRead);
@ -82,7 +83,7 @@ export const NoteUnreads = db.getRepository(NoteUnread);
export const Polls = db.getRepository(Poll);
export const PollVotes = db.getRepository(PollVote);
export const Users = UserRepository;
export const UserProfiles = db.getRepository(UserProfile);
export const UserProfiles = UserProfileRepository;
export const UserKeypairs = db.getRepository(UserKeypair);
export const UserPendings = db.getRepository(UserPending);
export const AttestationChallenges = db.getRepository(AttestationChallenge);

View file

@ -0,0 +1,42 @@
import { db } from "@/db/postgre.js";
import { UserProfile } from "@/models/entities/user-profile.js";
import mfm from "mfm-js";
import { extractMentions } from "@/misc/extract-mentions.js";
import { resolveMentionToUserAndProfile } from "@/remote/resolve-user.js";
import { IMentionedRemoteUsers } from "@/models/entities/note.js";
import { unique } from "@/prelude/array.js";
export const UserProfileRepository = db.getRepository(UserProfile).extend({
async updateMentions(id: UserProfile["userId"]){
const profile = await this.findOneBy({ userId: id });
if (!profile) return;
const tokens: mfm.MfmNode[] = [];
if (profile.description)
tokens.push(...mfm.parse(profile.description));
if (profile.fields.length > 0)
tokens.push(...profile.fields.map(p => mfm.parse(p.value).concat(mfm.parse(p.name))).flat());
const partial = {
mentions: await populateMentions(tokens, profile.userHost)
};
return UserProfileRepository.update(profile.userId, partial)
},
});
async function populateMentions(tokens: mfm.MfmNode[], objectHost: string | null): Promise<IMentionedRemoteUsers> {
const mentions = extractMentions(tokens);
const resolved = await Promise.all(mentions.map(m => resolveMentionToUserAndProfile(m.username, m.host, objectHost)));
const remote = resolved.filter(p => p && p.data.host !== null).map(p => p!);
const res = remote.map(m => {
return {
uri: m.user.uri!,
url: m.profile?.url ?? undefined,
username: m.data.username,
host: m.data.host!
};
});
return unique(res);
}

View file

@ -73,7 +73,7 @@ export async function renderPerson(user: ILocalUser) {
preferredUsername: user.username,
name: user.name,
summary: profile.description
? await toHtml(mfm.parse(profile.description), [], profile.userHost)
? await toHtml(mfm.parse(profile.description), profile.mentions, profile.userHost)
: null,
icon: avatar ? renderImage(avatar) : null,
image: banner ? renderImage(banner) : null,

View file

@ -178,13 +178,33 @@ export async function resolveUser(
return user;
}
export async function resolveMentionWithFallback(username: string, host: string | null, objectHost: string | null, cache: IMentionedRemoteUsers): Promise<string> {
export async function resolveMentionToUserAndProfile(username: string, host: string | null, objectHost: string | null) {
try {
//const fallback = getMentionFallbackUri(username, host, objectHost);
const user = await resolveUser(username, host ?? objectHost, false);
const profile = await UserProfiles.findOneBy({ userId: user.id });
const data = { username, host: host ?? objectHost };
return { user, profile, data };
}
catch {
return null;
}
}
export function getMentionFallbackUri(username: string, host: string | null, objectHost: string | null): string {
let fallback = `${config.url}/@${username}`;
if (host !== null && host !== config.domain)
fallback += `@${host}`;
else if (objectHost !== null && objectHost !== config.domain && host !== config.domain)
fallback += `@${objectHost}`;
return fallback;
}
export async function resolveMentionWithFallback(username: string, host: string | null, objectHost: string | null, cache: IMentionedRemoteUsers): Promise<string> {
const fallback = getMentionFallbackUri(username, host, objectHost);
const cached = cache.find(r => r.username.toLowerCase() === username.toLowerCase() && r.host === host);
if (cached) return cached.url ?? cached.uri;
if ((host === null && objectHost === null) || host === config.domain) return fallback;

View file

@ -306,8 +306,10 @@ export default define(meta, paramDef, async (ps, _user, token) => {
//#endregion
if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
if (Object.keys(profileUpdates).length > 0)
if (Object.keys(profileUpdates).length > 0) {
await UserProfiles.update(user.id, profileUpdates);
await UserProfiles.updateMentions(user.id);
}
const iObj = await Users.pack<true, true>(user.id, user, {
detail: true,

View file

@ -9,6 +9,7 @@ import { awaitAll } from "@/prelude/await-all.js";
import { AccountCache } from "@/server/api/mastodon/helpers/user.js";
import { MfmHelpers } from "@/server/api/mastodon/helpers/mfm.js";
import { MastoContext } from "@/server/api/mastodon/index.js";
import { IMentionedRemoteUsers } from "@/models/entities/note.js";
type Field = {
name: string;
@ -31,7 +32,7 @@ export class UserConverter {
acctUrl = `https://${u.host}/@${u.username}`;
}
const profile = UserProfiles.findOneBy({ userId: u.id });
const bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), [], u.host).then(p => p ?? escapeMFM(profile?.description ?? "")));
const bio = profile.then(profile => MfmHelpers.toHtml(mfm.parse(profile?.description ?? ""), profile?.mentions, u.host).then(p => p ?? escapeMFM(profile?.description ?? "")));
const avatar = u.avatarId
? (DriveFiles.findOneBy({ id: u.avatarId }))
.then(p => p?.url ?? Users.getIdenticonUrl(u.id))
@ -92,7 +93,7 @@ export class UserConverter {
header_static: banner,
emojis: populateEmojis(u.emojis, u.host).then(emoji => emoji.map((e) => EmojiConverter.encode(e))),
moved: null, //FIXME
fields: profile.then(profile => Promise.all(profile?.fields.map(async p => this.encodeField(p, u.host)) ?? [])),
fields: profile.then(profile => Promise.all(profile?.fields.map(async p => this.encodeField(p, u.host, profile?.mentions)) ?? [])),
bot: u.isBot,
discoverable: u.isExplorable
}).then(p => {
@ -107,10 +108,10 @@ export class UserConverter {
return Promise.all(encoded);
}
private static async encodeField(f: Field, host: string | null): Promise<MastodonEntity.Field> {
private static async encodeField(f: Field, host: string | null, mentions: IMentionedRemoteUsers): Promise<MastodonEntity.Field> {
return {
name: f.name,
value: await MfmHelpers.toHtml(mfm.parse(f.value), [], host, true) ?? escapeMFM(f.value),
value: await MfmHelpers.toHtml(mfm.parse(f.value), mentions, host, true) ?? escapeMFM(f.value),
verified_at: f.verified ? (new Date()).toISOString() : null,
}
}

View file

@ -10,7 +10,7 @@ export class MfmHelpers {
nodes: mfm.MfmNode[] | null,
mentionedRemoteUsers: IMentionedRemoteUsers = [],
objectHost: string | null,
inline: boolean = false,
inline: boolean = false
) {
if (nodes == null) {
return null;

View file

@ -193,7 +193,10 @@ export class UserHelpers {
if (formData.discoverable) updates.isExplorable = formData.discoverable;
if (Object.keys(updates).length > 0) await Users.update(user.id, updates);
if (Object.keys(profileUpdates).length > 0) await UserProfiles.update({ userId: user.id }, profileUpdates);
if (Object.keys(profileUpdates).length > 0) {
await UserProfiles.update({ userId: user.id }, profileUpdates);
await UserProfiles.updateMentions(user.id);
}
return this.verifyCredentials(ctx);
}