diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-09-29 18:11:30 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-09-29 18:11:30 +0900 |
| commit | 7adc8fcaf5e7a57edfd2c197fcccb510425bd82c (patch) | |
| tree | e60179e48010b0a389f87687e934f71ead92b27f /packages/backend/src/server/api | |
| parent | Merge pull request #11898 from misskey-dev/develop (diff) | |
| parent | fix (diff) | |
| download | misskey-7adc8fcaf5e7a57edfd2c197fcccb510425bd82c.tar.gz misskey-7adc8fcaf5e7a57edfd2c197fcccb510425bd82c.tar.bz2 misskey-7adc8fcaf5e7a57edfd2c197fcccb510425bd82c.zip | |
Merge pull request #11920 from misskey-dev/develop
Release: 2023.9.2
Diffstat (limited to 'packages/backend/src/server/api')
29 files changed, 251 insertions, 293 deletions
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 41a11bfb19..c883c96ba2 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -258,6 +258,7 @@ import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; +import * as ep___notes_update from './endpoints/notes/update.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'; @@ -606,6 +607,7 @@ const $notes_clips: Provider = { provide: 'ep:notes/clips', useClass: ep___notes const $notes_conversation: Provider = { provide: 'ep:notes/conversation', useClass: ep___notes_conversation.default }; const $notes_create: Provider = { provide: 'ep:notes/create', useClass: ep___notes_create.default }; const $notes_delete: Provider = { provide: 'ep:notes/delete', useClass: ep___notes_delete.default }; +const $notes_update: Provider = { provide: 'ep:notes/update', useClass: ep___notes_update.default }; 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 }; @@ -958,6 +960,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_conversation, $notes_create, $notes_delete, + $notes_update, $notes_favorites_create, $notes_favorites_delete, $notes_featured, @@ -1304,6 +1307,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $notes_conversation, $notes_create, $notes_delete, + $notes_update, $notes_favorites_create, $notes_favorites_delete, $notes_featured, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index ab20a708ef..b40d654f9c 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -258,6 +258,7 @@ import * as ep___notes_clips from './endpoints/notes/clips.js'; import * as ep___notes_conversation from './endpoints/notes/conversation.js'; import * as ep___notes_create from './endpoints/notes/create.js'; import * as ep___notes_delete from './endpoints/notes/delete.js'; +import * as ep___notes_update from './endpoints/notes/update.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'; @@ -604,6 +605,7 @@ const eps = [ ['notes/conversation', ep___notes_conversation], ['notes/create', ep___notes_create], ['notes/delete', ep___notes_delete], + ['notes/update', ep___notes_update], ['notes/favorites/create', ep___notes_favorites_create], ['notes/favorites/delete', ep___notes_favorites_delete], ['notes/featured', ep___notes_featured], diff --git a/packages/backend/src/server/api/endpoints/admin/ad/create.ts b/packages/backend/src/server/api/endpoints/admin/ad/create.ts index a13d08fd3a..e48dffecf4 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/create.ts @@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { AdsRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; export const meta = { tags: ['admin'], @@ -39,9 +40,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private adsRepository: AdsRepository, private idService: IdService, + private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { - await this.adsRepository.insert({ + const ad = await this.adsRepository.insert({ id: this.idService.genId(), createdAt: new Date(), expiresAt: new Date(ps.expiresAt), @@ -53,7 +55,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ratio: ps.ratio, place: ps.place, memo: ps.memo, + }).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id })); + + this.moderationLogService.log(me, 'createAd', { + adId: ad.id, + ad: ad, }); + + return ad; }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts index d3c53d4f67..8097133a4c 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/delete.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { AdsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -37,6 +38,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.adsRepository) private adsRepository: AdsRepository, + + private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const ad = await this.adsRepository.findOneBy({ id: ps.id }); @@ -44,6 +47,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ad == null) throw new ApiError(meta.errors.noSuchAd); await this.adsRepository.delete(ad.id); + + this.moderationLogService.log(me, 'deleteAd', { + adId: ad.id, + ad: ad, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/ad/list.ts b/packages/backend/src/server/api/endpoints/admin/ad/list.ts index adff3ed0ae..29eff89523 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/list.ts @@ -22,6 +22,7 @@ export const paramDef = { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, + publishing: { type: 'boolean', default: false }, }, required: [], } as const; @@ -36,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId); + if (ps.publishing) { + query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() }); + } const ads = await query.limit(ps.limit).getMany(); return ads; diff --git a/packages/backend/src/server/api/endpoints/admin/ad/update.ts b/packages/backend/src/server/api/endpoints/admin/ad/update.ts index 5b77f67e10..d065f9ec50 100644 --- a/packages/backend/src/server/api/endpoints/admin/ad/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/ad/update.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import type { AdsRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -46,6 +47,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- constructor( @Inject(DI.adsRepository) private adsRepository: AdsRepository, + + private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { const ad = await this.adsRepository.findOneBy({ id: ps.id }); @@ -63,6 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- startsAt: new Date(ps.startsAt), dayOfWeek: ps.dayOfWeek, }); + + const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id }); + + this.moderationLogService.log(me, 'updateAd', { + adId: ad.id, + before: ad, + after: updatedAd, + }); }); } } diff --git a/packages/backend/src/server/api/endpoints/admin/invite/create.ts b/packages/backend/src/server/api/endpoints/admin/invite/create.ts index 7112e06bdc..2cc5ab6e35 100644 --- a/packages/backend/src/server/api/endpoints/admin/invite/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/invite/create.ts @@ -10,6 +10,7 @@ import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService import { IdService } from '@/core/IdService.js'; import { DI } from '@/di-symbols.js'; import { generateInviteCode } from '@/misc/generate-invite-code.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -60,6 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private inviteCodeEntityService: InviteCodeEntityService, private idService: IdService, + private moderationLogService: ModerationLogService, ) { super(meta, paramDef, async (ps, me) => { if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) { @@ -78,6 +80,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } const tickets = await Promise.all(ticketsPromises); + + this.moderationLogService.log(me, 'createInvitation', { + invitations: tickets, + }); + return await this.inviteCodeEntityService.packMany(tickets, me); }); } diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index e065b99e93..3454597532 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- receiveAnnouncementEmail: profile.receiveAnnouncementEmail, mutedWords: profile.mutedWords, mutedInstances: profile.mutedInstances, - mutingNotificationTypes: profile.mutingNotificationTypes, + notificationRecieveConfig: profile.notificationRecieveConfig, isModerator: isModerator, isSilenced: isSilenced, isSuspended: user.isSuspended, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index b11e091957..431bb4c60a 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -165,9 +165,7 @@ export const paramDef = { mutedInstances: { type: 'array', items: { type: 'string', } }, - mutingNotificationTypes: { type: 'array', items: { - type: 'string', enum: notificationTypes, - } }, + notificationRecieveConfig: { type: 'object' }, emailNotificationTypes: { type: 'array', items: { type: 'string', } }, @@ -248,7 +246,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- profileUpdates.enableWordMute = ps.mutedWords.length > 0; } if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances; - if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; + if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus; diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index 6d34aaccf3..bfb024bcf2 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -34,10 +34,11 @@ describe('api:notes/create', () => { .toBe(VALID); }); - test('null post', () => { - expect(v({ text: null })) - .toBe(INVALID); - }); + // TODO + //test('null post', () => { + // expect(v({ text: null })) + // .toBe(INVALID); + //}); test('0 characters post', () => { expect(v({ text: '' })) diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 2e4d316c47..37a0525e25 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -118,7 +118,7 @@ export const paramDef = { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, - nullable: false, + nullable: true, }, fileIds: { type: 'array', diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index 0b3b5c902e..8784e86153 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -4,6 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import { Brackets } from 'typeorm'; import type { NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; @@ -40,6 +41,7 @@ export const paramDef = { properties: { withFiles: { type: 'boolean', default: false }, withReplies: { type: 'boolean', default: false }, + withRenotes: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -88,6 +90,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); } + + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } //#endregion const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts index e9ae5dc755..9bde5dee21 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -52,6 +52,7 @@ export const paramDef = { includeLocalRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, withReplies: { type: 'boolean', default: false }, + withRenotes: { type: 'boolean', default: true }, }, required: [], } as const; @@ -137,6 +138,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); } + + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } //#endregion const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index af1e0398dc..0fefddc51b 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -42,6 +42,7 @@ export const paramDef = { properties: { withFiles: { type: 'boolean', default: false }, withReplies: { type: 'boolean', default: false }, + withRenotes: { type: 'boolean', default: true }, fileType: { type: 'array', items: { type: 'string', } }, @@ -110,6 +111,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)'); } } + + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } //#endregion const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index 042115ab84..0d47cc1702 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -42,6 +42,7 @@ export const paramDef = { includeLocalRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false }, withReplies: { type: 'boolean', default: false }, + withRenotes: { type: 'boolean', default: true }, }, required: [], } as const; @@ -126,6 +127,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); } + + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } //#endregion const timeline = await query.limit(ps.limit).getMany(); diff --git a/packages/backend/src/server/api/endpoints/notes/update.ts b/packages/backend/src/server/api/endpoints/notes/update.ts new file mode 100644 index 0000000000..cdf7f085e0 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/update.ts @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository, NotesRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteDeleteService } from '@/core/NoteDeleteService.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: true, + requireRolePolicy: 'canEditNote', + + kind: 'write:notes', + + limit: { + duration: ms('1hour'), + max: 10, + minInterval: ms('1sec'), + }, + + errors: { + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'a6584e14-6e01-4ad3-b566-851e7bf0d474', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteId: { type: 'string', format: 'misskey:id' }, + text: { + type: 'string', + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: false, + }, + cw: { type: 'string', nullable: true, maxLength: 100 }, + }, + required: ['noteId', 'text', 'cw'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.notesRepository) + private notesRepository: NotesRepository, + + private getterService: GetterService, + private globalEventService: GlobalEventService, + ) { + super(meta, paramDef, async (ps, me) => { + const note = await this.getterService.getNote(ps.noteId).catch(err => { + if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw err; + }); + + if (note.userId !== me.id) { + throw new ApiError(meta.errors.noSuchNote); + } + + await this.notesRepository.update({ id: note.id }, { + updatedAt: new Date(), + cw: ps.cw, + text: ps.text, + }); + + this.globalEventService.publishNoteStream(note.id, 'updated', { + cw: ps.cw, + text: ps.text, + }); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 6932073791..c20274b2ba 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -49,6 +49,8 @@ export const paramDef = { includeMyRenotes: { type: 'boolean', default: true }, includeRenotedMyNotes: { type: 'boolean', default: true }, includeLocalRenotes: { type: 'boolean', default: true }, + withReplies: { type: 'boolean', default: false }, + withRenotes: { type: 'boolean', default: true }, withFiles: { type: 'boolean', default: false, @@ -130,6 +132,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- })); } + if (!ps.withReplies) { + query.andWhere('note.replyId IS NULL'); + } + + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } + if (ps.withFiles) { query.andWhere('note.fileIds != \'{}\''); } diff --git a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts index fd1bb48a4e..eae55905d3 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/create-from-public.ts @@ -144,7 +144,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } try { - await this.userListService.push(currentUser, userList, me); + await this.userListService.addMember(currentUser, userList, me); } catch (err) { if (err instanceof UserListService.TooManyUsersError) { throw new ApiError(meta.errors.tooManyUsers); diff --git a/packages/backend/src/server/api/endpoints/users/lists/pull.ts b/packages/backend/src/server/api/endpoints/users/lists/pull.ts index 0b01061740..e90122224c 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/pull.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/pull.ts @@ -4,12 +4,11 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { UserListsRepository, UserListJoiningsRepository } from '@/models/_.js'; +import type { UserListsRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { GetterService } from '@/server/api/GetterService.js'; -import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DI } from '@/di-symbols.js'; +import { UserListService } from '@/core/UserListService.js'; import { ApiError } from '../../../error.js'; export const meta = { @@ -53,12 +52,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- @Inject(DI.userListsRepository) private userListsRepository: UserListsRepository, - @Inject(DI.userListJoiningsRepository) - private userListJoiningsRepository: UserListJoiningsRepository, - - private userEntityService: UserEntityService, + private userListService: UserListService, private getterService: GetterService, - private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { // Fetch the list @@ -77,10 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- throw err; }); - // Pull the user - await this.userListJoiningsRepository.delete({ userListId: userList.id, userId: user.id }); - - this.globalEventService.publishUserListStream(userList.id, 'userRemoved', await this.userEntityService.pack(user)); + await this.userListService.removeMember(user, userList); }); } } diff --git a/packages/backend/src/server/api/endpoints/users/lists/push.ts b/packages/backend/src/server/api/endpoints/users/lists/push.ts index 9bb1a71f58..72a6a7380d 100644 --- a/packages/backend/src/server/api/endpoints/users/lists/push.ts +++ b/packages/backend/src/server/api/endpoints/users/lists/push.ts @@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } try { - await this.userListService.push(user, userList, me); + await this.userListService.addMember(user, userList, me); } catch (err) { if (err instanceof UserListService.TooManyUsersError) { throw new ApiError(meta.errors.tooManyUsers); diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts index 5934baef47..e660a0bb25 100644 --- a/packages/backend/src/server/api/endpoints/users/notes.ts +++ b/packages/backend/src/server/api/endpoints/users/notes.ts @@ -41,7 +41,8 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id' }, - includeReplies: { type: 'boolean', default: true }, + withReplies: { type: 'boolean', default: false }, + withRenotes: { type: 'boolean', default: true }, limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, @@ -114,10 +115,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- } } - if (!ps.includeReplies) { + if (!ps.withReplies) { query.andWhere('note.replyId IS NULL'); } + if (ps.withRenotes === false) { + query.andWhere(new Brackets(qb => { + qb.orWhere('note.renoteId IS NULL'); + qb.orWhere(new Brackets(qb => { + qb.orWhere('note.text IS NOT NULL'); + qb.orWhere('note.fileIds != \'{}\''); + })); + })); + } + if (ps.includeMyRenotes === false) { query.andWhere(new Brackets(qb => { qb.orWhere('note.userId != :userId', { userId: user.id }); diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index fd91681fc1..a73071ea99 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -12,10 +12,10 @@ import type { NotificationService } from '@/core/NotificationService.js'; import { bindThis } from '@/decorators.js'; import { CacheService } from '@/core/CacheService.js'; import { MiUserProfile } from '@/models/_.js'; +import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import type { ChannelsService } from './ChannelsService.js'; import type { EventEmitter } from 'events'; import type Channel from './channel.js'; -import type { StreamEventEmitter, StreamMessages } from './types.js'; /** * Main stream connection @@ -122,7 +122,7 @@ export default class Connection { } @bindThis - private onBroadcastMessage(data: StreamMessages['broadcast']['payload']) { + private onBroadcastMessage(data: GlobalEvents['broadcast']['payload']) { this.sendMessageToWs(data.type, data.body); } @@ -196,7 +196,7 @@ export default class Connection { } @bindThis - private async onNoteStreamMessage(data: StreamMessages['note']['payload']) { + private async onNoteStreamMessage(data: GlobalEvents['note']['payload']) { this.sendMessageToWs('noteUpdated', { id: data.body.id, type: data.type, diff --git a/packages/backend/src/server/api/stream/channels/antenna.ts b/packages/backend/src/server/api/stream/channels/antenna.ts index 87648a3a77..a48e6ba5c6 100644 --- a/packages/backend/src/server/api/stream/channels/antenna.ts +++ b/packages/backend/src/server/api/stream/channels/antenna.ts @@ -7,8 +7,8 @@ import { Injectable } from '@nestjs/common'; import { isUserRelated } from '@/misc/is-user-related.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; import Channel from '../channel.js'; -import type { StreamMessages } from '../types.js'; class AntennaChannel extends Channel { public readonly chName = 'antenna'; @@ -35,7 +35,7 @@ class AntennaChannel extends Channel { } @bindThis - private async onEvent(data: StreamMessages['antenna']['payload']) { + private async onEvent(data: GlobalEvents['antenna']['payload']) { if (data.type === 'note') { const note = await this.noteEntityService.pack(data.body.id, this.user, { detail: true }); diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts index a33f1a956a..fef52b6856 100644 --- a/packages/backend/src/server/api/stream/channels/global-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts @@ -19,6 +19,7 @@ class GlobalTimelineChannel extends Channel { public static shouldShare = true; public static requireCredential = false; private withReplies: boolean; + private withRenotes: boolean; constructor( private metaService: MetaService, @@ -37,7 +38,8 @@ class GlobalTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.gtlAvailable) return; - this.withReplies = params.withReplies as boolean; + this.withReplies = params.withReplies ?? false; + this.withRenotes = params.withRenotes ?? true; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -68,6 +70,8 @@ class GlobalTimelineChannel extends Channel { if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; } + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // Ignore notes from instances the user has muted if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return; diff --git a/packages/backend/src/server/api/stream/channels/home-timeline.ts b/packages/backend/src/server/api/stream/channels/home-timeline.ts index bd8888f679..198c68e1c2 100644 --- a/packages/backend/src/server/api/stream/channels/home-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/home-timeline.ts @@ -17,6 +17,7 @@ class HomeTimelineChannel extends Channel { public static shouldShare = true; public static requireCredential = true; private withReplies: boolean; + private withRenotes: boolean; constructor( private noteEntityService: NoteEntityService, @@ -30,7 +31,8 @@ class HomeTimelineChannel extends Channel { @bindThis public async init(params: any) { - this.withReplies = params.withReplies as boolean; + this.withReplies = params.withReplies ?? false; + this.withRenotes = params.withRenotes ?? true; this.subscriber.on('notesStream', this.onNote); } @@ -77,6 +79,8 @@ class HomeTimelineChannel extends Channel { if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; } + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts index 760fb8d19f..cde4297478 100644 --- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts @@ -19,6 +19,7 @@ class HybridTimelineChannel extends Channel { public static shouldShare = true; public static requireCredential = true; private withReplies: boolean; + private withRenotes: boolean; constructor( private metaService: MetaService, @@ -37,7 +38,8 @@ class HybridTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withReplies = params.withReplies as boolean; + this.withReplies = params.withReplies ?? false; + this.withRenotes = params.withRenotes ?? true; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -89,6 +91,8 @@ class HybridTimelineChannel extends Channel { if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return; } + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts index f32f8c5cec..ef708c4fee 100644 --- a/packages/backend/src/server/api/stream/channels/local-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts @@ -18,6 +18,7 @@ class LocalTimelineChannel extends Channel { public static shouldShare = true; public static requireCredential = false; private withReplies: boolean; + private withRenotes: boolean; constructor( private metaService: MetaService, @@ -36,7 +37,8 @@ class LocalTimelineChannel extends Channel { const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null); if (!policies.ltlAvailable) return; - this.withReplies = params.withReplies as boolean; + this.withReplies = params.withReplies ?? false; + this.withRenotes = params.withRenotes ?? true; // Subscribe events this.subscriber.on('notesStream', this.onNote); @@ -68,6 +70,8 @@ class LocalTimelineChannel extends Channel { if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return; } + if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return; + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (isUserRelated(note, this.userIdsWhoMeMuting)) return; // 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する diff --git a/packages/backend/src/server/api/stream/channels/role-timeline.ts b/packages/backend/src/server/api/stream/channels/role-timeline.ts index 76b5875343..38d3604cc5 100644 --- a/packages/backend/src/server/api/stream/channels/role-timeline.ts +++ b/packages/backend/src/server/api/stream/channels/role-timeline.ts @@ -9,8 +9,8 @@ import type { Packed } from '@/misc/json-schema.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; +import type { GlobalEvents } from '@/core/GlobalEventService.js'; import Channel from '../channel.js'; -import { StreamMessages } from '../types.js'; class RoleTimelineChannel extends Channel { public readonly chName = 'roleTimeline'; @@ -37,7 +37,7 @@ class RoleTimelineChannel extends Channel { } @bindThis - private async onEvent(data: StreamMessages['roleTimeline']['payload']) { + private async onEvent(data: GlobalEvents['roleTimeline']['payload']) { if (data.type === 'note') { const note = data.body; diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts deleted file mode 100644 index 90e0a61f26..0000000000 --- a/packages/backend/src/server/api/stream/types.ts +++ /dev/null @@ -1,255 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and other misskey contributors - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import type { MiChannel } from '@/models/Channel.js'; -import type { MiUser } from '@/models/User.js'; -import type { MiUserProfile } from '@/models/UserProfile.js'; -import type { MiNote } from '@/models/Note.js'; -import type { MiAntenna } from '@/models/Antenna.js'; -import type { MiDriveFile } from '@/models/DriveFile.js'; -import type { MiDriveFolder } from '@/models/DriveFolder.js'; -import type { MiUserList } from '@/models/UserList.js'; -import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; -import type { MiSignin } from '@/models/Signin.js'; -import type { MiPage } from '@/models/Page.js'; -import type { Packed } from '@/misc/json-schema.js'; -import type { MiWebhook } from '@/models/Webhook.js'; -import type { MiMeta } from '@/models/Meta.js'; -import { MiRole, MiRoleAssignment } from '@/models/_.js'; -import type Emitter from 'strict-event-emitter-types'; -import type { EventEmitter } from 'events'; - -//#region Stream type-body definitions -export interface InternalStreamTypes { - userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; - userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; }; - remoteUserUpdated: { id: MiUser['id']; }; - follow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; - unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; - blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; - blockingDeleted: { blockerId: MiUser['id']; blockeeId: MiUser['id']; }; - policiesUpdated: MiRole['policies']; - roleCreated: MiRole; - roleDeleted: MiRole; - roleUpdated: MiRole; - userRoleAssigned: MiRoleAssignment; - userRoleUnassigned: MiRoleAssignment; - webhookCreated: MiWebhook; - webhookDeleted: MiWebhook; - webhookUpdated: MiWebhook; - antennaCreated: MiAntenna; - antennaDeleted: MiAntenna; - antennaUpdated: MiAntenna; - metaUpdated: MiMeta; - followChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; - unfollowChannel: { userId: MiUser['id']; channelId: MiChannel['id']; }; - updateUserProfile: MiUserProfile; - mute: { muterId: MiUser['id']; muteeId: MiUser['id']; }; - unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; }; -} - -export interface BroadcastTypes { - emojiAdded: { - emoji: Packed<'EmojiDetailed'>; - }; - emojiUpdated: { - emojis: Packed<'EmojiDetailed'>[]; - }; - emojiDeleted: { - emojis: { - id?: string; - name: string; - [other: string]: any; - }[]; - }; - announcementCreated: { - announcement: Packed<'Announcement'>; - }; -} - -export interface MainStreamTypes { - notification: Packed<'Notification'>; - mention: Packed<'Note'>; - reply: Packed<'Note'>; - renote: Packed<'Note'>; - follow: Packed<'UserDetailedNotMe'>; - followed: Packed<'User'>; - unfollow: Packed<'User'>; - meUpdated: Packed<'User'>; - pageEvent: { - pageId: MiPage['id']; - event: string; - var: any; - userId: MiUser['id']; - user: Packed<'User'>; - }; - urlUploadFinished: { - marker?: string | null; - file: Packed<'DriveFile'>; - }; - readAllNotifications: undefined; - unreadNotification: Packed<'Notification'>; - unreadMention: MiNote['id']; - readAllUnreadMentions: undefined; - unreadSpecifiedNote: MiNote['id']; - readAllUnreadSpecifiedNotes: undefined; - readAllAntennas: undefined; - unreadAntenna: MiAntenna; - readAllAnnouncements: undefined; - myTokenRegenerated: undefined; - signin: MiSignin; - registryUpdated: { - scope?: string[]; - key: string; - value: any | null; - }; - driveFileCreated: Packed<'DriveFile'>; - readAntenna: MiAntenna; - receiveFollowRequest: Packed<'User'>; - announcementCreated: { - announcement: Packed<'Announcement'>; - }; -} - -export interface DriveStreamTypes { - fileCreated: Packed<'DriveFile'>; - fileDeleted: MiDriveFile['id']; - fileUpdated: Packed<'DriveFile'>; - folderCreated: Packed<'DriveFolder'>; - folderDeleted: MiDriveFolder['id']; - folderUpdated: Packed<'DriveFolder'>; -} - -export interface NoteStreamTypes { - pollVoted: { - choice: number; - userId: MiUser['id']; - }; - deleted: { - deletedAt: Date; - }; - reacted: { - reaction: string; - emoji?: { - name: string; - url: string; - } | null; - userId: MiUser['id']; - }; - unreacted: { - reaction: string; - userId: MiUser['id']; - }; -} -type NoteStreamEventTypes = { - [key in keyof NoteStreamTypes]: { - id: MiNote['id']; - body: NoteStreamTypes[key]; - }; -}; - -export interface UserListStreamTypes { - userAdded: Packed<'User'>; - userRemoved: Packed<'User'>; -} - -export interface AntennaStreamTypes { - note: MiNote; -} - -export interface RoleTimelineStreamTypes { - note: Packed<'Note'>; -} - -export interface AdminStreamTypes { - newAbuseUserReport: { - id: MiAbuseUserReport['id']; - targetUserId: MiUser['id'], - reporterId: MiUser['id'], - comment: string; - }; -} -//#endregion - -// 辞書(interface or type)から{ type, body }ユニオンを定義 -// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type -// VS Codeの展開を防止するためにEvents型を定義 -type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } }; -type EventUnionFromDictionary< - T extends object, - U = Events<T> -> = U[keyof U]; - -// redis通すとDateのインスタンスはstringに変換されるので -export type Serialized<T> = { - [K in keyof T]: - T[K] extends Date - ? string - : T[K] extends (Date | null) - ? (string | null) - : T[K] extends Record<string, any> - ? Serialized<T[K]> - : T[K]; -}; - -type SerializedAll<T> = { - [K in keyof T]: Serialized<T[K]>; -}; - -// name/messages(spec) pairs dictionary -export type StreamMessages = { - internal: { - name: 'internal'; - payload: EventUnionFromDictionary<SerializedAll<InternalStreamTypes>>; - }; - broadcast: { - name: 'broadcast'; - payload: EventUnionFromDictionary<SerializedAll<BroadcastTypes>>; - }; - main: { - name: `mainStream:${MiUser['id']}`; - payload: EventUnionFromDictionary<SerializedAll<MainStreamTypes>>; - }; - drive: { - name: `driveStream:${MiUser['id']}`; - payload: EventUnionFromDictionary<SerializedAll<DriveStreamTypes>>; - }; - note: { - name: `noteStream:${MiNote['id']}`; - payload: EventUnionFromDictionary<SerializedAll<NoteStreamEventTypes>>; - }; - userList: { - name: `userListStream:${MiUserList['id']}`; - payload: EventUnionFromDictionary<SerializedAll<UserListStreamTypes>>; - }; - roleTimeline: { - name: `roleTimelineStream:${MiRole['id']}`; - payload: EventUnionFromDictionary<SerializedAll<RoleTimelineStreamTypes>>; - }; - antenna: { - name: `antennaStream:${MiAntenna['id']}`; - payload: EventUnionFromDictionary<SerializedAll<AntennaStreamTypes>>; - }; - admin: { - name: `adminStream:${MiUser['id']}`; - payload: EventUnionFromDictionary<SerializedAll<AdminStreamTypes>>; - }; - notes: { - name: 'notesStream'; - payload: Serialized<Packed<'Note'>>; - }; -}; - -// API event definitions -// ストリームごとのEmitterの辞書を用意 -type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter.default<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['payload']) => void }> }; -// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection -type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; -// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする -export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>; -// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる - -// provide stream channels union -export type StreamChannels = StreamMessages[keyof StreamMessages]['name']; |