summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rw-r--r--packages/backend/src/core/FanoutTimelineEndpointService.ts6
-rw-r--r--packages/backend/src/core/NoteCreateService.ts23
-rw-r--r--packages/backend/src/core/NoteDeleteService.ts4
-rw-r--r--packages/backend/src/misc/is-pure-renote.ts15
-rw-r--r--packages/backend/src/misc/is-quote.ts12
-rw-r--r--packages/backend/src/misc/is-renote.ts36
-rw-r--r--packages/backend/src/server/ActivityPubServerService.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/notes/create.ts6
-rw-r--r--packages/backend/test/unit/NoteCreateService.ts144
-rw-r--r--packages/backend/test/unit/misc/is-renote.ts88
10 files changed, 295 insertions, 43 deletions
diff --git a/packages/backend/src/core/FanoutTimelineEndpointService.ts b/packages/backend/src/core/FanoutTimelineEndpointService.ts
index 9c239b4dfc..006433df7a 100644
--- a/packages/backend/src/core/FanoutTimelineEndpointService.ts
+++ b/packages/backend/src/core/FanoutTimelineEndpointService.ts
@@ -13,7 +13,7 @@ import type { NotesRepository } from '@/models/_.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { FanoutTimelineName, FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { isUserRelated } from '@/misc/is-user-related.js';
-import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
import { CacheService } from '@/core/CacheService.js';
import { isReply } from '@/misc/is-reply.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
@@ -95,7 +95,7 @@ export class FanoutTimelineEndpointService {
if (ps.excludePureRenotes) {
const parentFilter = filter;
- filter = (note) => !isPureRenote(note) && parentFilter(note);
+ filter = (note) => (!isRenote(note) || isQuote(note)) && parentFilter(note);
}
if (ps.me) {
@@ -116,7 +116,7 @@ export class FanoutTimelineEndpointService {
filter = (note) => {
if (isUserRelated(note, userIdsWhoBlockingMe, ps.ignoreAuthorFromBlock)) return false;
if (isUserRelated(note, userIdsWhoMeMuting, ps.ignoreAuthorFromMute)) return false;
- if (isPureRenote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false;
+ if (isRenote(note) && !isQuote(note) && isUserRelated(note, userIdsWhoMeMutingRenotes, ps.ignoreAuthorFromMute)) return false;
if (isInstanceMuted(note, userMutedInstances)) return false;
return parentFilter(note);
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 81ae2908d3..32104fea90 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -306,7 +306,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// Check blocking
- if (data.renote && !this.isQuote(data)) {
+ if (this.isRenote(data) && !this.isQuote(data)) {
if (data.renote.userHost === null) {
if (data.renote.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
@@ -641,7 +641,7 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// If it is renote
- if (data.renote) {
+ if (this.isRenote(data)) {
const type = this.isQuote(data) ? 'quote' : 'renote';
// Notify
@@ -725,9 +725,20 @@ export class NoteCreateService implements OnApplicationShutdown {
}
@bindThis
- private isQuote(note: Option): note is Option & { renote: MiNote } {
- // sync with misc/is-quote.ts
- return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll);
+ private isRenote(note: Option): note is Option & { renote: MiNote } {
+ return note.renote != null;
+ }
+
+ @bindThis
+ private isQuote(note: Option & { renote: MiNote }): note is Option & { renote: MiNote } & (
+ { text: string } | { cw: string } | { reply: MiNote } | { poll: IPoll } | { files: MiDriveFile[] }
+ ) {
+ // NOTE: SYNC WITH misc/is-quote.ts
+ return note.text != null ||
+ note.reply != null ||
+ note.cw != null ||
+ note.poll != null ||
+ (note.files != null && note.files.length > 0);
}
@bindThis
@@ -795,7 +806,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
if (data.localOnly) return null;
- const content = data.renote && !this.isQuote(data)
+ const content = this.isRenote(data) && !this.isQuote(data)
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts
index fdf843c3e8..801ed02e00 100644
--- a/packages/backend/src/core/NoteDeleteService.ts
+++ b/packages/backend/src/core/NoteDeleteService.ts
@@ -24,7 +24,7 @@ import { bindThis } from '@/decorators.js';
import { MetaService } from '@/core/MetaService.js';
import { SearchService } from '@/core/SearchService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
-import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
@Injectable()
export class NoteDeleteService {
@@ -79,7 +79,7 @@ export class NoteDeleteService {
let renote: MiNote | null = null;
// if deleted note is renote
- if (isPureRenote(note)) {
+ if (isRenote(note) && !isQuote(note)) {
renote = await this.notesRepository.findOneBy({
id: note.renoteId,
});
diff --git a/packages/backend/src/misc/is-pure-renote.ts b/packages/backend/src/misc/is-pure-renote.ts
deleted file mode 100644
index f9c2243a06..0000000000
--- a/packages/backend/src/misc/is-pure-renote.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { MiNote } from '@/models/Note.js';
-
-export function isPureRenote(note: MiNote): note is MiNote & { renoteId: NonNullable<MiNote['renoteId']> } {
- if (!note.renoteId) return false;
-
- if (note.text) return false; // it's quoted with text
- if (note.fileIds.length !== 0) return false; // it's quoted with files
- if (note.hasPoll) return false; // it's quoted with poll
- return true;
-}
diff --git a/packages/backend/src/misc/is-quote.ts b/packages/backend/src/misc/is-quote.ts
deleted file mode 100644
index 75b29f63f4..0000000000
--- a/packages/backend/src/misc/is-quote.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import type { MiNote } from '@/models/Note.js';
-
-// eslint-disable-next-line import/no-default-export
-export default function(note: MiNote): boolean {
- // sync with NoteCreateService.isQuote
- return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
-}
diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts
new file mode 100644
index 0000000000..5d48aba360
--- /dev/null
+++ b/packages/backend/src/misc/is-renote.ts
@@ -0,0 +1,36 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { MiNote } from '@/models/Note.js';
+
+type Renote =
+ MiNote & {
+ renoteId: NonNullable<MiNote['renoteId']>
+ };
+
+type Quote =
+ Renote & ({
+ text: NonNullable<MiNote['text']>
+ } | {
+ cw: NonNullable<MiNote['cw']>
+ } | {
+ replyId: NonNullable<MiNote['replyId']>
+ reply: NonNullable<MiNote['reply']>
+ } | {
+ hasPoll: true
+ });
+
+export function isRenote(note: MiNote): note is Renote {
+ return note.renoteId != null;
+}
+
+export function isQuote(note: Renote): note is Quote {
+ // NOTE: SYNC WITH NoteCreateService.isQuote
+ return note.text != null ||
+ note.cw != null ||
+ note.replyId != null ||
+ note.hasPoll ||
+ note.fileIds.length > 0;
+}
diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts
index 60366dd5c2..3255d64621 100644
--- a/packages/backend/src/server/ActivityPubServerService.ts
+++ b/packages/backend/src/server/ActivityPubServerService.ts
@@ -28,7 +28,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
-import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
@@ -91,7 +91,7 @@ export class ActivityPubServerService {
*/
@bindThis
private async packActivity(note: MiNote): Promise<any> {
- if (isPureRenote(note)) {
+ if (isRenote(note) && !isQuote(note)) {
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
}
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index bfb9214439..beb77ca7ab 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -16,7 +16,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
-import { isPureRenote } from '@/misc/is-pure-renote.js';
+import { isQuote, isRenote } from '@/misc/is-renote.js';
import { MetaService } from '@/core/MetaService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
@@ -275,7 +275,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget);
- } else if (isPureRenote(renote)) {
+ } else if (isRenote(renote) && !isQuote(renote)) {
throw new ApiError(meta.errors.cannotReRenote);
}
@@ -321,7 +321,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget);
- } else if (isPureRenote(reply)) {
+ } else if (isRenote(reply) && !isQuote(reply)) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
} else if (!await this.noteEntityService.isVisibleForMe(reply, me.id)) {
throw new ApiError(meta.errors.cannotReplyToInvisibleNote);
diff --git a/packages/backend/test/unit/NoteCreateService.ts b/packages/backend/test/unit/NoteCreateService.ts
new file mode 100644
index 0000000000..f2d4c8ffbb
--- /dev/null
+++ b/packages/backend/test/unit/NoteCreateService.ts
@@ -0,0 +1,144 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test } from '@nestjs/testing';
+
+import { CoreModule } from '@/core/CoreModule.js';
+import { NoteCreateService } from '@/core/NoteCreateService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { MiNote } from '@/models/Note.js';
+import { IPoll } from '@/models/Poll.js';
+import { MiDriveFile } from '@/models/DriveFile.js';
+
+describe('NoteCreateService', () => {
+ let noteCreateService: NoteCreateService;
+
+ beforeAll(async () => {
+ const app = await Test.createTestingModule({
+ imports: [GlobalModule, CoreModule],
+ }).compile();
+ noteCreateService = app.get<NoteCreateService>(NoteCreateService);
+ });
+
+ describe('is-renote', () => {
+ const base: MiNote = {
+ id: 'some-note-id',
+ replyId: null,
+ reply: null,
+ renoteId: null,
+ renote: null,
+ threadId: null,
+ text: null,
+ name: null,
+ cw: null,
+ userId: 'some-user-id',
+ user: null,
+ localOnly: false,
+ reactionAcceptance: null,
+ renoteCount: 0,
+ repliesCount: 0,
+ clippedCount: 0,
+ reactions: {},
+ visibility: 'public',
+ uri: null,
+ url: null,
+ fileIds: [],
+ attachedFileTypes: [],
+ visibleUserIds: [],
+ mentions: [],
+ mentionedRemoteUsers: '',
+ reactionAndUserPairCache: [],
+ emojis: [],
+ tags: [],
+ hasPoll: false,
+ channelId: null,
+ channel: null,
+ userHost: null,
+ replyUserId: null,
+ replyUserHost: null,
+ renoteUserId: null,
+ renoteUserHost: null,
+ };
+
+ const poll: IPoll = {
+ choices: ['kinoko', 'takenoko'],
+ multiple: false,
+ expiresAt: null,
+ };
+
+ const file: MiDriveFile = {
+ id: 'some-file-id',
+ userId: null,
+ user: null,
+ userHost: null,
+ md5: '',
+ name: '',
+ type: '',
+ size: 0,
+ comment: null,
+ blurhash: null,
+ properties: {},
+ storedInternal: false,
+ url: '',
+ thumbnailUrl: null,
+ webpublicUrl: null,
+ webpublicType: null,
+ accessKey: null,
+ thumbnailAccessKey: null,
+ webpublicAccessKey: null,
+ uri: null,
+ src: null,
+ folderId: null,
+ folder: null,
+ isSensitive: false,
+ maybeSensitive: false,
+ maybePorn: false,
+ isLink: false,
+ requestHeaders: null,
+ requestIp: null,
+ };
+
+ test('note without renote should not be Renote', () => {
+ const note = { renote: null };
+ expect(noteCreateService['isRenote'](note)).toBe(false);
+ });
+
+ test('note with renote should be Renote and not be Quote', () => {
+ const note = { renote: base };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(false);
+ });
+
+ test('note with renote and text should be Quote', () => {
+ const note = { renote: base, text: 'some-text' };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(true);
+ });
+
+ test('note with renote and cw should be Quote', () => {
+ const note = { renote: base, cw: 'some-cw' };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(true);
+ });
+
+ test('note with renote and reply should be Quote', () => {
+ const note = { renote: base, reply: { ...base, id: 'another-note-id' } };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(true);
+ });
+
+ test('note with renote and poll should be Quote', () => {
+ const note = { renote: base, poll };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(true);
+ });
+
+ test('note with renote and non-empty files should be Quote', () => {
+ const note = { renote: base, files: [file] };
+ expect(noteCreateService['isRenote'](note)).toBe(true);
+ expect(noteCreateService['isQuote'](note)).toBe(true);
+ });
+ });
+});
diff --git a/packages/backend/test/unit/misc/is-renote.ts b/packages/backend/test/unit/misc/is-renote.ts
new file mode 100644
index 0000000000..0b713e8bf6
--- /dev/null
+++ b/packages/backend/test/unit/misc/is-renote.ts
@@ -0,0 +1,88 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { isQuote, isRenote } from '@/misc/is-renote.js';
+import { MiNote } from '@/models/Note.js';
+
+const base: MiNote = {
+ id: 'some-note-id',
+ replyId: null,
+ reply: null,
+ renoteId: null,
+ renote: null,
+ threadId: null,
+ text: null,
+ name: null,
+ cw: null,
+ userId: 'some-user-id',
+ user: null,
+ localOnly: false,
+ reactionAcceptance: null,
+ renoteCount: 0,
+ repliesCount: 0,
+ clippedCount: 0,
+ reactions: {},
+ visibility: 'public',
+ uri: null,
+ url: null,
+ fileIds: [],
+ attachedFileTypes: [],
+ visibleUserIds: [],
+ mentions: [],
+ mentionedRemoteUsers: '',
+ reactionAndUserPairCache: [],
+ emojis: [],
+ tags: [],
+ hasPoll: false,
+ channelId: null,
+ channel: null,
+ userHost: null,
+ replyUserId: null,
+ replyUserHost: null,
+ renoteUserId: null,
+ renoteUserHost: null,
+};
+
+describe('misc:is-renote', () => {
+ test('note without renoteId should not be Renote', () => {
+ expect(isRenote(base)).toBe(false);
+ });
+
+ test('note with renoteId should be Renote and not be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id' };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(false);
+ });
+
+ test('note with renoteId and text should be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id', text: 'some-text' };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(true);
+ });
+
+ test('note with renoteId and cw should be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id', cw: 'some-cw' };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(true);
+ });
+
+ test('note with renoteId and replyId should be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id', replyId: 'some-reply-id' };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(true);
+ });
+
+ test('note with renoteId and poll should be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id', hasPoll: true };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(true);
+ });
+
+ test('note with renoteId and non-empty fileIds should be Quote', () => {
+ const note: MiNote = { ...base, renoteId: 'some-renote-id', fileIds: ['some-file-id'] };
+ expect(isRenote(note)).toBe(true);
+ expect(isQuote(note as any)).toBe(true);
+ });
+});