mirror of
https://iceshrimp.dev/limepotato/jormungandr-bite.git
synced 2024-11-25 03:17:38 -07:00
[backend] Add mentions column to user_profile table
This commit is contained in:
parent
bc08d8c92b
commit
82e0ef7414
12 changed files with 109 additions and 13 deletions
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -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[];
|
||||
|
|
|
@ -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;
|
||||
}[]
|
|
@ -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);
|
||||
|
|
42
packages/backend/src/models/repositories/user-profile.ts
Normal file
42
packages/backend/src/models/repositories/user-profile.ts
Normal 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);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue