summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/backend/src/core/activitypub/ApRendererService.ts180
-rw-r--r--packages/backend/src/core/activitypub/ApResolverService.ts29
-rw-r--r--packages/backend/test/unit/activitypub.ts53
3 files changed, 22 insertions, 240 deletions
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index b075d0f803..77c5f207f8 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -32,6 +32,8 @@ import { IdService } from '@/core/IdService.js';
import { appendContentWarning } from '@/misc/append-content-warning.js';
import { QueryService } from '@/core/QueryService.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { CacheService } from '@/core/CacheService.js';
+import { isPureRenote, isQuote, isRenote } from '@/misc/is-renote.js';
import { JsonLdService } from './JsonLdService.js';
import { ApMfmService } from './ApMfmService.js';
import { CONTEXT } from './misc/contexts.js';
@@ -75,6 +77,7 @@ export class ApRendererService {
private idService: IdService,
private readonly queryService: QueryService,
private utilityService: UtilityService,
+ private readonly cacheService: CacheService,
) {
}
@@ -232,7 +235,7 @@ export class ApRendererService {
*/
@bindThis
public async renderFollowUser(id: MiUser['id']): Promise<string> {
- const user = await this.usersRepository.findOneByOrFail({ id: id }) as MiPartialLocalUser | MiPartialRemoteUser;
+ const user = await this.cacheService.findUserById(id) as MiPartialLocalUser | MiPartialRemoteUser;
return this.userEntityService.getUserUri(user);
}
@@ -402,7 +405,7 @@ export class ApRendererService {
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
if (inReplyToNote != null) {
- const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
+ const inReplyToUser = await this.cacheService.findUserById(inReplyToNote.userId);
if (inReplyToUser) {
if (inReplyToNote.uri) {
@@ -422,7 +425,7 @@ export class ApRendererService {
let quote: string | undefined = undefined;
- if (note.renoteId) {
+ if (isRenote(note) && isQuote(note)) {
const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
if (renote) {
@@ -542,6 +545,7 @@ export class ApRendererService {
attributedTo,
summary: summary ?? undefined,
content: content ?? undefined,
+ updated: note.updatedAt?.toISOString(),
_misskey_content: text,
source: {
content: text,
@@ -757,176 +761,6 @@ export class ApRendererService {
}
@bindThis
- public async renderUpNote(note: MiNote, author: MiUser, dive = true): Promise<IPost> {
- const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
- if (ids.length === 0) return [];
- const items = await this.driveFilesRepository.findBy({ id: In(ids) });
- return ids.map(id => items.find(item => item.id === id)).filter((item): item is MiDriveFile => item != null);
- };
-
- let inReplyTo;
- let inReplyToNote: MiNote | null;
-
- if (note.replyId) {
- inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
-
- if (inReplyToNote != null) {
- const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
-
- if (inReplyToUser) {
- if (inReplyToNote.uri) {
- inReplyTo = inReplyToNote.uri;
- } else {
- if (dive) {
- inReplyTo = await this.renderUpNote(inReplyToNote, inReplyToUser, false);
- } else {
- inReplyTo = `${this.config.url}/notes/${inReplyToNote.id}`;
- }
- }
- }
- }
- } else {
- inReplyTo = null;
- }
-
- let quote: string | undefined = undefined;
-
- if (note.renoteId) {
- const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
-
- if (renote) {
- quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`;
- }
- }
-
- const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
-
- const mentions = note.mentionedRemoteUsers ? (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri) : [];
-
- let to: string[] = [];
- let cc: string[] = [];
-
- if (note.visibility === 'public') {
- to = ['https://www.w3.org/ns/activitystreams#Public'];
- cc = [`${attributedTo}/followers`].concat(mentions);
- } else if (note.visibility === 'home') {
- to = [`${attributedTo}/followers`];
- cc = ['https://www.w3.org/ns/activitystreams#Public'].concat(mentions);
- } else if (note.visibility === 'followers') {
- to = [`${attributedTo}/followers`];
- cc = mentions;
- } else {
- to = mentions;
- }
-
- const mentionedUsers = note.mentions && note.mentions.length > 0 ? await this.usersRepository.findBy({
- id: In(note.mentions),
- }) : [];
-
- const hashtagTags = note.tags.map(tag => this.renderHashtag(tag));
- const mentionTags = mentionedUsers.map(u => this.renderMention(u as MiLocalUser | MiRemoteUser));
-
- const files = await getPromisedFiles(note.fileIds);
-
- const text = note.text ?? '';
- let poll: MiPoll | null = null;
-
- if (note.hasPoll) {
- poll = await this.pollsRepository.findOneBy({ noteId: note.id });
- }
-
- const apAppend: Appender[] = [];
-
- if (quote) {
- // Append quote link as `<br><br><span class="quote-inline">RE: <a href="...">...</a></span>`
- // the claas name `quote-inline` is used in non-misskey clients for styling quote notes.
- // For compatibility, the span part should be kept as possible.
- apAppend.push((doc, body) => {
- body.childNodes.push(new Element('br', {}));
- body.childNodes.push(new Element('br', {}));
- const span = new Element('span', {
- class: 'quote-inline',
- });
- span.childNodes.push(new Text('RE: '));
- const link = new Element('a', {
- href: quote,
- });
- link.childNodes.push(new Text(quote));
- span.childNodes.push(link);
- body.childNodes.push(span);
- });
- }
-
- let summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
-
- // Apply mandatory CW, if applicable
- if (author.mandatoryCW) {
- summary = appendContentWarning(summary, author.mandatoryCW);
- }
-
- const { content } = this.apMfmService.getNoteHtml(note, apAppend);
-
- const emojis = await this.getEmojis(note.emojis);
- const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
-
- const tag: IObject[] = [
- ...hashtagTags,
- ...mentionTags,
- ...apemojis,
- ];
-
- // https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
- if (quote) {
- tag.push({
- type: 'Link',
- mediaType: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
- rel: 'https://misskey-hub.net/ns#_misskey_quote',
- href: quote,
- } satisfies ILink);
- }
-
- const asPoll = poll ? {
- type: 'Question',
- [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
- [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
- type: 'Note',
- name: text,
- replies: {
- type: 'Collection',
- totalItems: poll!.votes[i],
- },
- })),
- } as const : {};
-
- return {
- id: `${this.config.url}/notes/${note.id}`,
- type: 'Note',
- attributedTo,
- summary: summary ?? undefined,
- content: content ?? undefined,
- updated: note.updatedAt?.toISOString(),
- _misskey_content: text,
- source: {
- content: text,
- mediaType: 'text/x.misskeymarkdown',
- },
- _misskey_quote: quote,
- quoteUrl: quote,
- quoteUri: quote,
- // https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md
- quote: quote,
- published: this.idService.parse(note.id).date.toISOString(),
- to,
- cc,
- inReplyTo,
- attachment: files.map(x => this.renderDocument(x)),
- sensitive: note.cw != null || files.some(file => file.isSensitive),
- tag,
- ...asPoll,
- };
- }
-
- @bindThis
public renderVote(user: { id: MiUser['id'] }, vote: MiPollVote, note: MiNote, poll: MiPoll, pollOwner: MiRemoteUser): ICreate {
return {
id: `${this.config.url}/users/${user.id}#votes/${vote.id}/activity`,
diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts
index 201920612c..d53e265d36 100644
--- a/packages/backend/src/core/activitypub/ApResolverService.ts
+++ b/packages/backend/src/core/activitypub/ApResolverService.ts
@@ -21,6 +21,8 @@ import { ApUtilityService } from '@/core/activitypub/ApUtilityService.js';
import { SystemAccountService } from '@/core/SystemAccountService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { toArray } from '@/misc/prelude/array.js';
+import { isPureRenote } from '@/misc/is-renote.js';
+import { CacheService } from '@/core/CacheService.js';
import { AnyCollection, getApId, getNullableApId, IObjectWithId, isCollection, isCollectionOrOrderedCollection, isCollectionPage, isOrderedCollection, isOrderedCollectionPage } from './type.js';
import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js';
@@ -49,6 +51,7 @@ export class Resolver {
private loggerService: LoggerService,
private readonly apLogService: ApLogService,
private readonly apUtilityService: ApUtilityService,
+ private readonly cacheService: CacheService,
private recursionLimit = 256,
) {
this.history = new Set();
@@ -355,18 +358,20 @@ export class Resolver {
switch (parsed.type) {
case 'notes':
- return this.notesRepository.findOneByOrFail({ id: parsed.id, userHost: IsNull() })
+ return this.notesRepository.findOneOrFail({ where: { id: parsed.id, userHost: IsNull() }, relations: { user: true, renote: true } })
.then(async note => {
- const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
+ const author = note.user ?? await this.cacheService.findUserById(note.userId);
if (parsed.rest === 'activity') {
- // this refers to the create activity and not the note itself
- return this.apRendererService.addContext(this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author), note));
+ return await this.apRendererService.renderNoteOrRenoteActivity(note, author);
+ } else if (!isPureRenote(note)) {
+ const apNote = await this.apRendererService.renderNote(note, author);
+ return this.apRendererService.addContext(apNote);
} else {
- return this.apRendererService.renderNote(note, author);
+ throw new IdentifiableError('732c2633-3395-4d51-a9b7-c7084774e3e7', `Failed to resolve local ${url}: cannot resolve a boost as note`);
}
}) as Promise<IObjectWithId>;
case 'users':
- return this.usersRepository.findOneByOrFail({ id: parsed.id, host: IsNull() })
+ return this.cacheService.findLocalUserById(parsed.id)
.then(user => this.apRendererService.renderPerson(user as MiLocalUser));
case 'questions':
// Polls are indexed by the note they are attached to.
@@ -387,14 +392,8 @@ export class Resolver {
.then(async followRequest => {
if (followRequest == null) throw new IdentifiableError('a9d946e5-d276-47f8-95fb-f04230289bb0', `failed to resolve local ${url}: invalid follow request ID`);
const [follower, followee] = await Promise.all([
- this.usersRepository.findOneBy({
- id: followRequest.followerId,
- host: IsNull(),
- }),
- this.usersRepository.findOneBy({
- id: followRequest.followeeId,
- host: Not(IsNull()),
- }),
+ this.cacheService.findLocalUserById(followRequest.followerId),
+ this.cacheService.findLocalUserById(followRequest.followeeId),
]);
if (follower == null || followee == null) {
throw new IdentifiableError('06ae3170-1796-4d93-a697-2611ea6d83b6', `failed to resolve local ${url}: follower or followee does not exist`);
@@ -440,6 +439,7 @@ export class ApResolverService {
private loggerService: LoggerService,
private readonly apLogService: ApLogService,
private readonly apUtilityService: ApUtilityService,
+ private readonly cacheService: CacheService,
) {
}
@@ -465,6 +465,7 @@ export class ApResolverService {
this.loggerService,
this.apLogService,
this.apUtilityService,
+ this.cacheService,
opts?.recursionLimit,
);
}
diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts
index 3ad9cdc4d5..ff93e1be07 100644
--- a/packages/backend/test/unit/activitypub.ts
+++ b/packages/backend/test/unit/activitypub.ts
@@ -674,59 +674,6 @@ describe('ActivityPub', () => {
});
});
- describe('renderUpnote', () => {
- describe('summary', () => {
- // I actually don't know why it does this, but the logic was already there so I've preserved it.
- it('should be zero-width space when CW is empty string', async () => {
- note.cw = '';
-
- const result = await rendererService.renderUpNote(note, author, false);
-
- expect(result.summary).toBe(String.fromCharCode(0x200B));
- });
-
- it('should be undefined when CW is null', async () => {
- const result = await rendererService.renderUpNote(note, author, false);
-
- expect(result.summary).toBeUndefined();
- });
-
- it('should be CW when present without mandatoryCW', async () => {
- note.cw = 'original';
-
- const result = await rendererService.renderUpNote(note, author, false);
-
- expect(result.summary).toBe('original');
- });
-
- it('should be mandatoryCW when present without CW', async () => {
- author.mandatoryCW = 'mandatory';
-
- const result = await rendererService.renderUpNote(note, author, false);
-
- expect(result.summary).toBe('mandatory');
- });
-
- it('should be merged when CW and mandatoryCW are both present', async () => {
- note.cw = 'original';
- author.mandatoryCW = 'mandatory';
-
- const result = await rendererService.renderUpNote(note, author, false);
-
- expect(result.summary).toBe('original, mandatory');
- });
-
- it('should be CW when CW includes mandatoryCW', async () => {
- note.cw = 'original and mandatory';
- author.mandatoryCW = 'mandatory';
-
- const result = await rendererService.renderUpNote(note, author, false);
-
- expect(result.summary).toBe('original and mandatory');
- });
- });
- });
-
describe('renderPersonRedacted', () => {
it('should include minimal properties', async () => {
const result = await rendererService.renderPersonRedacted(author);