refactor: remove note.mentionedRemoteUsers column #84

Closed
Johann150 wants to merge 6 commits from refactor/mentionedRemoteUsers into main
9 changed files with 69 additions and 74 deletions

View file

@ -0,0 +1,12 @@
export class removeMentionedRemoteUsersColumn1661376843000 {
name = 'removeMentionedRemoteUsersColumn1661376843000';
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "mentionedRemoteUsers"`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`);
await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`);
}
}

View file

@ -1,17 +1,29 @@
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { UserProfiles } from '@/models/index.js';
import { extractMentions } from '@/misc/extract-mentions.js';
import { intersperse } from '@/prelude/array.js'; import { intersperse } from '@/prelude/array.js';
import { IMentionedRemoteUsers } from '@/models/entities/note.js';
export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) { // Transforms MFM to HTML, given the MFM text and a list of user IDs that are
// mentioned in the text. If the list of mentions is not given, all mentions
// from the text will be extracted.
export async function toHtml(mfmText: string, mentions?: string[]): string | null {
const nodes = mfm.parse(mfmText);
if (nodes == null) { if (nodes == null) {
return null; return null;
} }
const { window } = new JSDOM(''); const mentionedUsers = await UserProfiles.createQueryBuilder("user_profiles")
.leftJoin('user_profile.user', 'user')
.select('user.username')
.addSelect('user.host')
// links should preferably use user friendly urls, only fall back to AP ids
.addSelect('COALESCE(user_profile.url, user.uri)', 'url')
.where('userId IN (:...ids)', { ids: mentions ?? extractMentions(nodes) })
.getManyRaw();
const doc = window.document; const doc = new JSDOM('').window.document;
function appendChildren(children: mfm.MfmNode[], targetElement: any): void { function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
if (children) { if (children) {
@ -106,11 +118,12 @@ export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMenti
}, },
mention(node) { mention(node) {
const { username, host, acct } = node.props;
const userInfo = mentionedUsers.find(user => user.username === username && user.host === host);
if (userInfo != null) {
// Mastodon microformat: span.h-card > a.u-url.mention // Mastodon microformat: span.h-card > a.u-url.mention
const a = doc.createElement('a'); const a = doc.createElement('a');
const { username, host, acct } = node.props; a.href = userInfo.url ?? `${config.url}/${acct}`;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
a.href = remoteUserInfo?.url ?? remoteUserInfo?.uri ?? `${config.url}/${acct}`;
a.className = 'u-url mention'; a.className = 'u-url mention';
a.textContent = acct; a.textContent = acct;
@ -118,6 +131,10 @@ export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMenti
card.className = 'h-card'; card.className = 'h-card';
card.appendChild(a); card.appendChild(a);
return card; return card;
} else {
// this user does not actually exist
return doc.createTextNode(acct);
}
}, },
quote(node) { quote(node) {

View file

@ -155,11 +155,6 @@ export class Note {
}) })
public mentions: User['id'][]; public mentions: User['id'][];
@Column('text', {
default: '[]',
})
public mentionedRemoteUsers: string;
@Column('varchar', { @Column('varchar', {
length: 128, array: true, default: '{}', length: 128, array: true, default: '{}',
}) })
@ -233,10 +228,3 @@ export class Note {
} }
} }
} }
export type IMentionedRemoteUsers = {
uri: string;
url?: string;
username: string;
host: string;
}[];

View file

@ -1,8 +0,0 @@
import * as mfm from 'mfm-js';
import { Note } from '@/models/entities/note.js';
import { toHtml } from '../../../mfm/to-html.js';
export default function(note: Note) {
if (!note.text) return '';
return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
}

View file

@ -1,11 +1,11 @@
import { In, IsNull } from 'typeorm'; import { In, IsNull } from 'typeorm';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js'; import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js';
import { Emoji } from '@/models/entities/emoji.js'; import { Emoji } from '@/models/entities/emoji.js';
import { Poll } from '@/models/entities/poll.js'; import { Poll } from '@/models/entities/poll.js';
import toHtml from '../misc/get-note-html.js'; import { toHtml } from '@/mfm/to-html.js';
import renderEmoji from './emoji.js'; import renderEmoji from './emoji.js';
import renderMention from './mention.js'; import renderMention from './mention.js';
import renderHashtag from './hashtag.js'; import renderHashtag from './hashtag.js';
@ -55,28 +55,31 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const attributedTo = `${config.url}/users/${note.userId}`; const attributedTo = `${config.url}/users/${note.userId}`;
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); const mentionedUsers = note.mentions.length > 0 ? await Users.findBy({
id: In(note.mentions),
}) : [];
const mentionUris = mentionedUsers
// only remote users
.filter(user => Users.isRemoteUser(user))
.map(user => user.uri);
let to: string[] = []; let to: string[] = [];
let cc: string[] = []; let cc: string[] = [];
if (note.visibility === 'public') { if (note.visibility === 'public') {
to = ['https://www.w3.org/ns/activitystreams#Public']; to = ['https://www.w3.org/ns/activitystreams#Public'];
cc = [`${attributedTo}/followers`].concat(mentions); cc = [`${attributedTo}/followers`].concat(mentionUris);
} else if (note.visibility === 'home') { } else if (note.visibility === 'home') {
to = [`${attributedTo}/followers`]; to = [`${attributedTo}/followers`];
cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions); cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentionUris);
} else if (note.visibility === 'followers') { } else if (note.visibility === 'followers') {
to = [`${attributedTo}/followers`]; to = [`${attributedTo}/followers`];
cc = mentions; cc = mentionUris;
} else { } else {
to = mentions; to = mentionUris;
} }
const mentionedUsers = note.mentions.length > 0 ? await Users.findBy({
id: In(note.mentions),
}) : [];
const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag)); const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag));
const mentionTags = mentionedUsers.map(u => renderMention(u)); const mentionTags = mentionedUsers.map(u => renderMention(u));
@ -97,9 +100,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
const content = toHtml(Object.assign({}, note, { const content = await toHtml(apText, note.mentions);
text: apText,
}));
const emojis = await getEmojis(note.emojis); const emojis = await getEmojis(note.emojis);
const apemojis = emojis.map(emoji => renderEmoji(emoji)); const apemojis = emojis.map(emoji => renderEmoji(emoji));
@ -112,7 +113,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const asPoll = poll ? { const asPoll = poll ? {
type: 'Question', type: 'Question',
content: toHtml(Object.assign({}, note, { text })), content: await toHtml(text, note.mentions),
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
type: 'Note', type: 'Note',

View file

@ -1,5 +1,4 @@
import { URL } from 'node:url'; import { URL } from 'node:url';
import * as mfm from 'mfm-js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { ILocalUser } from '@/models/entities/user.js'; import { ILocalUser } from '@/models/entities/user.js';
import { toHtml } from '@/mfm/to-html.js'; import { toHtml } from '@/mfm/to-html.js';
@ -66,7 +65,7 @@ export async function renderPerson(user: ILocalUser) {
url: `${config.url}/@${user.username}`, url: `${config.url}/@${user.username}`,
preferredUsername: user.username, preferredUsername: user.username,
name: user.name, name: user.name,
summary: profile.description ? toHtml(mfm.parse(profile.description)) : null, summary: profile.description ? await toHtml(profile.description) : null,
icon: avatar ? renderImage(avatar) : null, icon: avatar ? renderImage(avatar) : null,
image: banner ? renderImage(banner) : null, image: banner ? renderImage(banner) : null,
tag, tag,

View file

@ -93,11 +93,6 @@ export async function createMessage(user: { id: User['id']; host: User['host'];
userId: message.userId, userId: message.userId,
visibility: 'specified', visibility: 'specified',
mentions: [ recipientUser.id ], mentions: [ recipientUser.id ],
mentionedRemoteUsers: JSON.stringify([ recipientUser ].map(u => ({
uri: u.uri,
username: u.username,
host: u.host,
}))),
} as Note; } as Note;
const activity = renderActivity(renderCreate(await renderNote(note, false, true), note)); const activity = renderActivity(renderCreate(await renderNote(note, false, true), note));

View file

@ -15,7 +15,7 @@ import { insertNoteUnread } from '@/services/note/unread.js';
import { extractMentions } from '@/misc/extract-mentions.js'; import { extractMentions } from '@/misc/extract-mentions.js';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js'; import { extractHashtags } from '@/misc/extract-hashtags.js';
import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, MutedNotes, Channels, ChannelFollowings, NoteThreadMutings } from '@/models/index.js'; import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, MutedNotes, Channels, ChannelFollowings, NoteThreadMutings } from '@/models/index.js';
import { DriveFile } from '@/models/entities/drive-file.js'; import { DriveFile } from '@/models/entities/drive-file.js';
import { App } from '@/models/entities/app.js'; import { App } from '@/models/entities/app.js';
@ -537,16 +537,6 @@ async function insertNote(user: { id: User['id']; host: User['host']; }, data: O
// Append mentions data // Append mentions data
if (mentionedUsers.length > 0) { if (mentionedUsers.length > 0) {
insert.mentions = mentionedUsers.map(u => u.id); insert.mentions = mentionedUsers.map(u => u.id);
const profiles = await UserProfiles.findBy({ userId: In(insert.mentions) });
insert.mentionedRemoteUsers = JSON.stringify(mentionedUsers.filter(u => Users.isRemoteUser(u)).map(u => {
const profile = profiles.find(p => p.userId === u.id);
return {
uri: u.uri,
url: profile?.url,
username: u.username,
host: u.host,
} as IMentionedRemoteUsers[0];
}));
} }
// 投稿を作成 // 投稿を作成

View file

@ -1,4 +1,4 @@
import { Brackets, In } from 'typeorm'; import { Brackets, In, IsNull, Not } from 'typeorm';
import { publishNoteStream } from '@/services/stream.js'; import { publishNoteStream } from '@/services/stream.js';
import renderDelete from '@/remote/activitypub/renderer/delete.js'; import renderDelete from '@/remote/activitypub/renderer/delete.js';
import renderAnnounce from '@/remote/activitypub/renderer/announce.js'; import renderAnnounce from '@/remote/activitypub/renderer/announce.js';
@ -7,7 +7,7 @@ import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderTombstone from '@/remote/activitypub/renderer/tombstone.js'; import renderTombstone from '@/remote/activitypub/renderer/tombstone.js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js'; import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js'; import { Note } from '@/models/entities/note.js';
import { Notes, Users, Instances } from '@/models/index.js'; import { Notes, Users, Instances } from '@/models/index.js';
import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js'; import { notesChart, perUserNotesChart, instanceChart } from '@/services/chart/index.js';
import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-manager.js'; import { deliverToFollowers, deliverToUser } from '@/remote/activitypub/deliver-manager.js';
@ -109,11 +109,12 @@ async function getMentionedRemoteUsers(note: Note): Promise<IRemoteUser[]> {
const where = [] as any[]; const where = [] as any[];
// mention / reply / dm // mention / reply / dm
const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri); if (note.mentions > 0) {
if (uris.length > 0) { where.push({
where.push( id: In(note.mentions),
{ uri: In(uris) }, // only remote users, local users are on the server and do not need to be notified
); host: Not(IsNull()),
});
} }
// renote / quote // renote / quote