refactor: remove note.mentionedRemoteUsers column #84
9 changed files with 69 additions and 74 deletions
|
@ -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")`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
|
||||||
}[];
|
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
|
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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];
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 投稿を作成
|
// 投稿を作成
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue