From 072f4b460865320ae437c0c838f588a632086db7 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 00:49:45 -0400 Subject: add /notes/following endpoint --- packages/backend/src/server/api/EndpointsModule.ts | 4 + packages/backend/src/server/api/endpoints.ts | 2 + .../src/server/api/endpoints/notes/following.ts | 88 ++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 packages/backend/src/server/api/endpoints/notes/following.ts (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 4a08410ceb..c90a23d7b5 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -290,6 +290,7 @@ import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; +import * as ep___notes_following from './endpoints/notes/following.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; @@ -686,6 +687,7 @@ const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___not const $notes_favorites_create: Provider = { provide: 'ep:notes/favorites/create', useClass: ep___notes_favorites_create.default }; const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete', useClass: ep___notes_favorites_delete.default }; const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default }; +const $notes_following: Provider = { provide: 'ep:notes/following', useClass: ep___notes_following.default }; const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default }; const $notes_bubbleTimeline: Provider = { provide: 'ep:notes/bubble-timeline', useClass: ep___notes_bubbleTimeline.default }; const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default }; @@ -1086,6 +1088,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_favorites_create, $notes_favorites_delete, $notes_featured, + $notes_following, $notes_globalTimeline, $notes_bubbleTimeline, $notes_hybridTimeline, @@ -1480,6 +1483,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $notes_favorites_create, $notes_favorites_delete, $notes_featured, + $notes_following, $notes_globalTimeline, $notes_bubbleTimeline, $notes_hybridTimeline, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index e2fcd1a9d0..e93e57f907 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -296,6 +296,7 @@ import * as ep___notes_delete from './endpoints/notes/delete.js'; import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js'; import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js'; import * as ep___notes_featured from './endpoints/notes/featured.js'; +import * as ep___notes_following from './endpoints/notes/following.js'; import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js'; import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js'; import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js'; @@ -690,6 +691,7 @@ const eps = [ ['notes/favorites/create', ep___notes_favorites_create], ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], + ['notes/following', ep___notes_following], ['notes/global-timeline', ep___notes_globalTimeline], ['notes/bubble-timeline', ep___notes_bubbleTimeline], ['notes/hybrid-timeline', ep___notes_hybridTimeline], diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts new file mode 100644 index 0000000000..57ab5a6aeb --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { LatestNote, MiFollowing, MiBlocking, MiMuting } from '@/models/_.js'; +import type { NotesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { QueryService } from '@/core/QueryService.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + kind: 'read:account', + allowGet: true, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + ref: 'Note', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + sinceDate: { type: 'integer' }, + untilDate: { type: 'integer' }, + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private noteEntityService: NoteEntityService, + private queryService: QueryService, + ) { + super(meta, paramDef, async (ps, me) => { + const query = this.notesRepository + .createQueryBuilder('note') + + // Limit to latest notes + .innerJoin(LatestNote, 'latest', 'note.id = latest.note_id') + + // Avoid N+1 queries from the "pack" method + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser') + .leftJoinAndSelect('note.channel', 'channel') + + // Respect blocks and mutes + .leftJoin(MiBlocking, 'b', 'note."userId" = b."blockerId"') + .leftJoin(MiMuting, 'm', 'note."userId" = m."muteeId"') + .where('b.id IS NULL AND m.id IS NULL') + + // Limit to followers + .innerJoin(MiFollowing, 'following', 'latest.user_id = following."followeeId"') + .andWhere('following."followerId" = :me', { me: me.id }) + + // Support pagination + .orderBy('note.id', 'DESC') + .take(ps.limit); + + // Query and return the next page + const notes = await this.queryService + .makePaginationQuery(query, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .getMany(); + return await this.noteEntityService.packMany(notes, me); + }); + } +} -- cgit v1.2.3-freya From 3479c2c13a64e71900644b65acce33226d3f1213 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 01:12:29 -0400 Subject: add mutuals-only option --- .../src/server/api/endpoints/notes/following.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index 57ab5a6aeb..a317e8e8b1 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -32,6 +32,7 @@ export const meta = { export const paramDef = { type: 'object', properties: { + mutualsOnly: { type: 'boolean', default: false }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -51,8 +52,9 @@ export default class extends Endpoint { // eslint- private queryService: QueryService, ) { super(meta, paramDef, async (ps, me) => { - const query = this.notesRepository + let query = this.notesRepository .createQueryBuilder('note') + .setParameter('me', me.id) // Limit to latest notes .innerJoin(LatestNote, 'latest', 'note.id = latest.note_id') @@ -72,16 +74,22 @@ export default class extends Endpoint { // eslint- // Limit to followers .innerJoin(MiFollowing, 'following', 'latest.user_id = following."followeeId"') - .andWhere('following."followerId" = :me', { me: me.id }) + .andWhere('following."followerId" = :me'); - // Support pagination + // Limit to mutuals, if requested + if (ps.mutualsOnly) { + query = query + .innerJoin(MiFollowing, 'mutuals', 'latest.user_id = mutuals."followerId" AND mutuals."followeeId" = :me'); + } + + // Support pagination + query = this.queryService + .makePaginationQuery(query, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .orderBy('note.id', 'DESC') .take(ps.limit); // Query and return the next page - const notes = await this.queryService - .makePaginationQuery(query, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) - .getMany(); + const notes = await query.getMany(); return await this.noteEntityService.packMany(notes, me); }); } -- cgit v1.2.3-freya From 168ff64b03ebaf048542956b2d0b0dd19166aa71 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 30 Sep 2024 12:06:44 -0400 Subject: fix copyright header --- packages/backend/src/server/api/endpoints/notes/following.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index a317e8e8b1..189c4b7ce6 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors * SPDX-License-Identifier: AGPL-3.0-only */ -- cgit v1.2.3-freya From 7603ecddac761138e4d21bafeca6ec52961df852 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Wed, 2 Oct 2024 12:28:41 -0400 Subject: respect domain mutes --- packages/backend/src/server/api/endpoints/notes/following.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index 189c4b7ce6..71ed364d65 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { LatestNote, MiFollowing, MiBlocking, MiMuting } from '@/models/_.js'; +import { LatestNote, MiFollowing, MiBlocking, MiMuting, MiUserProfile } from '@/models/_.js'; import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -54,6 +54,7 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { let query = this.notesRepository .createQueryBuilder('note') + .innerJoin(MiUserProfile, 'user_profile', ':me = user_profile."userId"') .setParameter('me', me.id) // Limit to latest notes @@ -70,7 +71,8 @@ export default class extends Endpoint { // eslint- // Respect blocks and mutes .leftJoin(MiBlocking, 'b', 'note."userId" = b."blockerId"') .leftJoin(MiMuting, 'm', 'note."userId" = m."muteeId"') - .where('b.id IS NULL AND m.id IS NULL') + .andWhere('b.id IS NULL AND m.id IS NULL') + .andWhere('note."userHost" IS NULL OR NOT user_profile."mutedInstances" ? note."userHost"') // Limit to followers .innerJoin(MiFollowing, 'following', 'latest.user_id = following."followeeId"') -- cgit v1.2.3-freya From d7f1e3f823f546aae0bf3f7a2d0e1add7bb97fc1 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Wed, 2 Oct 2024 13:23:01 -0400 Subject: respect blocks and mutes for replies and renotes --- .../src/server/api/endpoints/notes/following.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index 71ed364d65..ac9730f631 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -68,11 +68,23 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel') - // Respect blocks and mutes - .leftJoin(MiBlocking, 'b', 'note."userId" = b."blockerId"') - .leftJoin(MiMuting, 'm', 'note."userId" = m."muteeId"') - .andWhere('b.id IS NULL AND m.id IS NULL') - .andWhere('note."userHost" IS NULL OR NOT user_profile."mutedInstances" ? note."userHost"') + // Respect blocks and mutes for latest note + .leftJoin(MiBlocking, 'blocking_note', 'note."userId" = blocking_note."blockerId"') + .leftJoin(MiMuting, 'muting_note', 'note."userId" = muting_note."muteeId"') + .andWhere('blocking_note.id IS NULL AND muting_note.id IS NULL') + .andWhere('(note."userHost" IS NULL OR NOT user_profile."mutedInstances" ? note."userHost")') + + // Respect blocks and mutes for renote + .leftJoin(MiBlocking, 'blocking_renote', 'renote."userId" IS NOT NULL AND renote."userId" = blocking_renote."blockerId"') + .leftJoin(MiMuting, 'muting_renote', 'renote."userId" IS NOT NULL AND renote."userId" = muting_renote."muteeId"') + .andWhere('blocking_renote.id IS NULL AND muting_renote.id IS NULL') + .andWhere('(renote."userHost" IS NULL OR NOT user_profile."mutedInstances" ? renote."userHost")') + + // Respect blocks and mutes for reply + .leftJoin(MiBlocking, 'blocking_reply', 'reply."userId" IS NOT NULL AND reply."userId" = blocking_reply."blockerId"') + .leftJoin(MiMuting, 'muting_reply', 'reply."userId" IS NOT NULL AND reply."userId" = muting_reply."muteeId"') + .andWhere('blocking_reply.id IS NULL AND muting_reply.id IS NULL') + .andWhere('(reply."userHost" IS NULL OR NOT user_profile."mutedInstances" ? reply."userHost")') // Limit to followers .innerJoin(MiFollowing, 'following', 'latest.user_id = following."followeeId"') -- cgit v1.2.3-freya From 19d7d44d0e3d2df2dfe717eed8716404523cf989 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Wed, 2 Oct 2024 15:02:22 -0400 Subject: use QueryService for blocks and mutes --- .../src/server/api/endpoints/notes/following.ts | 23 ++++------------------ 1 file changed, 4 insertions(+), 19 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index ac9730f631..89ffc83604 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -54,7 +54,6 @@ export default class extends Endpoint { // eslint- super(meta, paramDef, async (ps, me) => { let query = this.notesRepository .createQueryBuilder('note') - .innerJoin(MiUserProfile, 'user_profile', ':me = user_profile."userId"') .setParameter('me', me.id) // Limit to latest notes @@ -68,24 +67,6 @@ export default class extends Endpoint { // eslint- .leftJoinAndSelect('renote.user', 'renoteUser') .leftJoinAndSelect('note.channel', 'channel') - // Respect blocks and mutes for latest note - .leftJoin(MiBlocking, 'blocking_note', 'note."userId" = blocking_note."blockerId"') - .leftJoin(MiMuting, 'muting_note', 'note."userId" = muting_note."muteeId"') - .andWhere('blocking_note.id IS NULL AND muting_note.id IS NULL') - .andWhere('(note."userHost" IS NULL OR NOT user_profile."mutedInstances" ? note."userHost")') - - // Respect blocks and mutes for renote - .leftJoin(MiBlocking, 'blocking_renote', 'renote."userId" IS NOT NULL AND renote."userId" = blocking_renote."blockerId"') - .leftJoin(MiMuting, 'muting_renote', 'renote."userId" IS NOT NULL AND renote."userId" = muting_renote."muteeId"') - .andWhere('blocking_renote.id IS NULL AND muting_renote.id IS NULL') - .andWhere('(renote."userHost" IS NULL OR NOT user_profile."mutedInstances" ? renote."userHost")') - - // Respect blocks and mutes for reply - .leftJoin(MiBlocking, 'blocking_reply', 'reply."userId" IS NOT NULL AND reply."userId" = blocking_reply."blockerId"') - .leftJoin(MiMuting, 'muting_reply', 'reply."userId" IS NOT NULL AND reply."userId" = muting_reply."muteeId"') - .andWhere('blocking_reply.id IS NULL AND muting_reply.id IS NULL') - .andWhere('(reply."userHost" IS NULL OR NOT user_profile."mutedInstances" ? reply."userHost")') - // Limit to followers .innerJoin(MiFollowing, 'following', 'latest.user_id = following."followeeId"') .andWhere('following."followerId" = :me'); @@ -96,6 +77,10 @@ export default class extends Endpoint { // eslint- .innerJoin(MiFollowing, 'mutuals', 'latest.user_id = mutuals."followerId" AND mutuals."followeeId" = :me'); } + // Respect blocks and mutes + this.queryService.generateBlockedUserQuery(query, me) + this.queryService.generateMutedUserQuery(query, me); + // Support pagination query = this.queryService .makePaginationQuery(query, ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) -- cgit v1.2.3-freya From 40179a55910b46dadf45aa1fa0a88167643944c2 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Wed, 2 Oct 2024 22:00:32 -0400 Subject: lint fixes --- packages/backend/src/postgres.ts | 2 +- packages/backend/src/server/api/endpoints/notes/following.ts | 4 ++-- packages/frontend/src/pages/following-feed.vue | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'packages/backend/src/server/api') diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index de7353dc8c..0d17b3d046 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -83,7 +83,7 @@ import { MiReversiGame } from '@/models/ReversiGame.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; import { bindThis } from '@/decorators.js'; -import {LatestNote} from "@/models/LatestNote.js"; +import { LatestNote } from '@/models/LatestNote.js'; pg.types.setTypeParser(20, Number); diff --git a/packages/backend/src/server/api/endpoints/notes/following.ts b/packages/backend/src/server/api/endpoints/notes/following.ts index 89ffc83604..436160f250 100644 --- a/packages/backend/src/server/api/endpoints/notes/following.ts +++ b/packages/backend/src/server/api/endpoints/notes/following.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { LatestNote, MiFollowing, MiBlocking, MiMuting, MiUserProfile } from '@/models/_.js'; +import { LatestNote, MiFollowing } from '@/models/_.js'; import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -78,7 +78,7 @@ export default class extends Endpoint { // eslint- } // Respect blocks and mutes - this.queryService.generateBlockedUserQuery(query, me) + this.queryService.generateBlockedUserQuery(query, me); this.queryService.generateMutedUserQuery(query, me); // Support pagination diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index 769c8dcd86..d79aeebf4d 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only