From 6b676a928d3e167c98f2a1854a432adf5c125d65 Mon Sep 17 00:00:00 2001 From: yupix Date: Sun, 10 Mar 2024 17:31:39 +0900 Subject: enhance(backend): antennas/updateの必須項目をantennaIdのみに (#13542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: antennas/updateの必須項目を最小限に * fix: userListIdがnullにできない --- packages/backend/src/server/api/endpoints/antennas/update.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 459729f61f..76a34924a0 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -67,7 +67,7 @@ export const paramDef = { withFile: { type: 'boolean' }, notify: { type: 'boolean' }, }, - required: ['antennaId', 'name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], + required: ['antennaId'], } as const; @Injectable() @@ -83,8 +83,10 @@ export default class extends Endpoint { // eslint- private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, me) => { - if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { - throw new Error('either keywords or excludeKeywords is required.'); + if (ps.keywords && ps.excludeKeywords) { + if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { + throw new Error('either keywords or excludeKeywords is required.'); + } } // Fetch the antenna const antenna = await this.antennasRepository.findOneBy({ @@ -98,7 +100,7 @@ export default class extends Endpoint { // eslint- let userList; - if (ps.src === 'list' && ps.userListId) { + if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) { userList = await this.userListsRepository.findOneBy({ id: ps.userListId, userId: me.id, @@ -112,7 +114,7 @@ export default class extends Endpoint { // eslint- await this.antennasRepository.update(antenna.id, { name: ps.name, src: ps.src, - userListId: userList ? userList.id : null, + userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined, keywords: ps.keywords, excludeKeywords: ps.excludeKeywords, users: ps.users, -- cgit v1.2.3-freya From 5c1d86b796d6ab878bc4f9bd2faf4207998e71cf Mon Sep 17 00:00:00 2001 From: おさむのひと <46447427+samunohito@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:31:34 +0900 Subject: refactor(backend): UserEntityService.packMany()の高速化 (#13550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(backend): UserEntityService.packMany()の高速化 * 修正 --- .../backend/src/core/entities/UserEntityService.ts | 229 +++++++-- .../src/server/api/endpoints/users/relation.ts | 8 +- .../test/unit/entities/UserEntityService.ts | 528 +++++++++++++++++++++ 3 files changed, 729 insertions(+), 36 deletions(-) create mode 100644 packages/backend/test/unit/entities/UserEntityService.ts (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 14761357a5..df2b27d709 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; import _Ajv from 'ajv'; import { ModuleRef } from '@nestjs/core'; +import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema.js'; @@ -14,9 +15,30 @@ import type { Promiseable } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js'; -import { MiNotification } from '@/models/Notification.js'; -import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js'; +import { + birthdaySchema, + descriptionSchema, + localUsernameSchema, + locationSchema, + nameSchema, + passwordSchema, +} from '@/models/User.js'; +import type { + BlockingsRepository, + FollowingsRepository, + FollowRequestsRepository, + MiFollowing, + MiUserNotePining, + MiUserProfile, + MutingsRepository, + NoteUnreadsRepository, + RenoteMutingsRepository, + UserMemoRepository, + UserNotePiningsRepository, + UserProfilesRepository, + UserSecurityKeysRepository, + UsersRepository, +} from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; @@ -46,11 +68,23 @@ function isRemoteUser(user: MiUser | { host: MiUser['host'] }): boolean { return !isLocalUser(user); } +export type UserRelation = { + id: MiUser['id'] + following: MiFollowing | null, + isFollowing: boolean + isFollowed: boolean + hasPendingFollowRequestFromYou: boolean + hasPendingFollowRequestToYou: boolean + isBlocking: boolean + isBlocked: boolean + isMuted: boolean + isRenoteMuted: boolean +} + @Injectable() export class UserEntityService implements OnModuleInit { private apPersonService: ApPersonService; private noteEntityService: NoteEntityService; - private driveFileEntityService: DriveFileEntityService; private pageEntityService: PageEntityService; private customEmojiService: CustomEmojiService; private announcementService: AnnouncementService; @@ -89,9 +123,6 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.renoteMutingsRepository) private renoteMutingsRepository: RenoteMutingsRepository, - @Inject(DI.driveFilesRepository) - private driveFilesRepository: DriveFilesRepository, - @Inject(DI.noteUnreadsRepository) private noteUnreadsRepository: NoteUnreadsRepository, @@ -101,12 +132,6 @@ export class UserEntityService implements OnModuleInit { @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - @Inject(DI.announcementReadsRepository) - private announcementReadsRepository: AnnouncementReadsRepository, - - @Inject(DI.announcementsRepository) - private announcementsRepository: AnnouncementsRepository, - @Inject(DI.userMemosRepository) private userMemosRepository: UserMemoRepository, ) { @@ -115,7 +140,6 @@ export class UserEntityService implements OnModuleInit { onModuleInit() { this.apPersonService = this.moduleRef.get('ApPersonService'); this.noteEntityService = this.moduleRef.get('NoteEntityService'); - this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.pageEntityService = this.moduleRef.get('PageEntityService'); this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.announcementService = this.moduleRef.get('AnnouncementService'); @@ -138,7 +162,7 @@ export class UserEntityService implements OnModuleInit { public isRemoteUser = isRemoteUser; @bindThis - public async getRelation(me: MiUser['id'], target: MiUser['id']) { + public async getRelation(me: MiUser['id'], target: MiUser['id']): Promise { const [ following, isFollowed, @@ -211,6 +235,59 @@ export class UserEntityService implements OnModuleInit { }; } + @bindThis + public async getRelations(me: MiUser['id'], targets: MiUser['id'][]): Promise> { + const [ + followers, + followees, + followersRequests, + followeesRequests, + blockers, + blockees, + muters, + renoteMuters, + ] = await Promise.all([ + this.followingsRepository.findBy({ followerId: me }) + .then(f => new Map(f.map(it => [it.followeeId, it]))), + this.followingsRepository.findBy({ followeeId: me }) + .then(it => it.map(it => it.followerId)), + this.followRequestsRepository.findBy({ followerId: me }) + .then(it => it.map(it => it.followeeId)), + this.followRequestsRepository.findBy({ followeeId: me }) + .then(it => it.map(it => it.followerId)), + this.blockingsRepository.findBy({ blockerId: me }) + .then(it => it.map(it => it.blockeeId)), + this.blockingsRepository.findBy({ blockeeId: me }) + .then(it => it.map(it => it.blockerId)), + this.mutingsRepository.findBy({ muterId: me }) + .then(it => it.map(it => it.muteeId)), + this.renoteMutingsRepository.findBy({ muterId: me }) + .then(it => it.map(it => it.muteeId)), + ]); + + return new Map( + targets.map(target => { + const following = followers.get(target) ?? null; + + return [ + target, + { + id: target, + following: following, + isFollowing: following != null, + isFollowed: followees.includes(target), + hasPendingFollowRequestFromYou: followersRequests.includes(target), + hasPendingFollowRequestToYou: followeesRequests.includes(target), + isBlocking: blockers.includes(target), + isBlocked: blockees.includes(target), + isMuted: muters.includes(target), + isRenoteMuted: renoteMuters.includes(target), + }, + ]; + }), + ); + } + @bindThis public async getHasUnreadAntenna(userId: MiUser['id']): Promise { /* @@ -303,6 +380,9 @@ export class UserEntityService implements OnModuleInit { schema?: S, includeSecrets?: boolean, userProfile?: MiUserProfile, + userRelations?: Map, + userMemos?: Map, + pinNotes?: Map, }, ): Promise> { const opts = Object.assign({ @@ -317,13 +397,41 @@ export class UserEntityService implements OnModuleInit { const isMe = meId === user.id; const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; - const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null; - const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin') - .where('pin.userId = :userId', { userId: user.id }) - .innerJoinAndSelect('pin.note', 'note') - .orderBy('pin.id', 'DESC') - .getMany() : []; - const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null; + const profile = isDetailed + ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) + : null; + + let relation: UserRelation | null = null; + if (meId && !isMe && isDetailed) { + if (opts.userRelations) { + relation = opts.userRelations.get(user.id) ?? null; + } else { + relation = await this.getRelation(meId, user.id); + } + } + + let memo: string | null = null; + if (isDetailed && meId) { + if (opts.userMemos) { + memo = opts.userMemos.get(user.id) ?? null; + } else { + memo = await this.userMemosRepository.findOneBy({ userId: meId, targetUserId: user.id }) + .then(row => row?.memo ?? null); + } + } + + let pins: MiUserNotePining[] = []; + if (isDetailed) { + if (opts.pinNotes) { + pins = opts.pinNotes.get(user.id) ?? []; + } else { + pins = await this.userNotePiningsRepository.createQueryBuilder('pin') + .where('pin.userId = :userId', { userId: user.id }) + .innerJoinAndSelect('pin.note', 'note') + .orderBy('pin.id', 'DESC') + .getMany(); + } + } const followingCount = profile == null ? null : (profile.followingVisibility === 'public') || isMe ? user.followingCount : @@ -416,9 +524,7 @@ export class UserEntityService implements OnModuleInit { twoFactorEnabled: profile!.twoFactorEnabled, usePasswordLessLogin: profile!.usePasswordLessLogin, securityKeys: profile!.twoFactorEnabled - ? this.userSecurityKeysRepository.countBy({ - userId: user.id, - }).then(result => result >= 1) + ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1) : false, roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({ id: role.id, @@ -430,10 +536,7 @@ export class UserEntityService implements OnModuleInit { isAdministrator: role.isAdministrator, displayOrder: role.displayOrder, }))), - memo: meId == null ? null : await this.userMemosRepository.findOneBy({ - userId: meId, - targetUserId: user.id, - }).then(row => row?.memo ?? null), + memo: memo, moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined, } : {}), @@ -514,7 +617,7 @@ export class UserEntityService implements OnModuleInit { return await awaitAll(packed); } - public packMany( + public async packMany( users: (MiUser['id'] | MiUser)[], me?: { id: MiUser['id'] } | null | undefined, options?: { @@ -522,6 +625,70 @@ export class UserEntityService implements OnModuleInit { includeSecrets?: boolean, }, ): Promise[]> { - return Promise.all(users.map(u => this.pack(u, me, options))); + // -- IDのみの要素を補完して完全なエンティティ一覧を作る + + const _users = users.filter((user): user is MiUser => typeof user !== 'string'); + if (_users.length !== users.length) { + _users.push( + ...await this.usersRepository.findBy({ + id: In(users.filter((user): user is string => typeof user === 'string')), + }), + ); + } + const _userIds = _users.map(u => u.id); + + // -- 特に前提条件のない値群を取得 + + const profilesMap = await this.userProfilesRepository.findBy({ userId: In(_userIds) }) + .then(profiles => new Map(profiles.map(p => [p.userId, p]))); + + // -- 実行者の有無や指定スキーマの種別によって要否が異なる値群を取得 + + let userRelations: Map = new Map(); + let userMemos: Map = new Map(); + let pinNotes: Map = new Map(); + + if (options?.schema !== 'UserLite') { + const meId = me ? me.id : null; + if (meId) { + userMemos = await this.userMemosRepository.findBy({ userId: meId }) + .then(memos => new Map(memos.map(memo => [memo.targetUserId, memo.memo]))); + + if (_userIds.length > 0) { + userRelations = await this.getRelations(meId, _userIds); + pinNotes = await this.userNotePiningsRepository.createQueryBuilder('pin') + .where('pin.userId IN (:...userIds)', { userIds: _userIds }) + .innerJoinAndSelect('pin.note', 'note') + .getMany() + .then(pinsNotes => { + const map = new Map(); + for (const note of pinsNotes) { + const notes = map.get(note.userId) ?? []; + notes.push(note); + map.set(note.userId, notes); + } + for (const [, notes] of map.entries()) { + // pack側ではDESCで取得しているので、それに合わせて降順に並び替えておく + notes.sort((a, b) => b.id.localeCompare(a.id)); + } + return map; + }); + } + } + } + + return Promise.all( + _users.map(u => this.pack( + u, + me, + { + ...options, + userProfile: profilesMap.get(u.id), + userRelations: userRelations, + userMemos: userMemos, + pinNotes: pinNotes, + }, + )), + ); } } diff --git a/packages/backend/src/server/api/endpoints/users/relation.ts b/packages/backend/src/server/api/endpoints/users/relation.ts index 6a5b2262fa..1d75437b81 100644 --- a/packages/backend/src/server/api/endpoints/users/relation.ts +++ b/packages/backend/src/server/api/endpoints/users/relation.ts @@ -132,11 +132,9 @@ export default class extends Endpoint { // eslint- private userEntityService: UserEntityService, ) { super(meta, paramDef, async (ps, me) => { - const ids = Array.isArray(ps.userId) ? ps.userId : [ps.userId]; - - const relations = await Promise.all(ids.map(id => this.userEntityService.getRelation(me.id, id))); - - return Array.isArray(ps.userId) ? relations : relations[0]; + return Array.isArray(ps.userId) + ? await this.userEntityService.getRelations(me.id, ps.userId).then(it => [...it.values()]) + : await this.userEntityService.getRelation(me.id, ps.userId).then(it => [it]); }); } } diff --git a/packages/backend/test/unit/entities/UserEntityService.ts b/packages/backend/test/unit/entities/UserEntityService.ts new file mode 100644 index 0000000000..ee16d421c4 --- /dev/null +++ b/packages/backend/test/unit/entities/UserEntityService.ts @@ -0,0 +1,528 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Test, TestingModule } from '@nestjs/testing'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { GlobalModule } from '@/GlobalModule.js'; +import { CoreModule } from '@/core/CoreModule.js'; +import type { MiUser } from '@/models/User.js'; +import { secureRndstr } from '@/misc/secure-rndstr.js'; +import { genAidx } from '@/misc/id/aidx.js'; +import { + BlockingsRepository, + FollowingsRepository, FollowRequestsRepository, + MiUserProfile, MutingsRepository, RenoteMutingsRepository, + UserMemoRepository, + UserProfilesRepository, + UsersRepository, +} from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; +import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; +import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { PageEntityService } from '@/core/entities/PageEntityService.js'; +import { CustomEmojiService } from '@/core/CustomEmojiService.js'; +import { AnnouncementService } from '@/core/AnnouncementService.js'; +import { RoleService } from '@/core/RoleService.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { IdService } from '@/core/IdService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; +import { MetaService } from '@/core/MetaService.js'; +import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; +import { CacheService } from '@/core/CacheService.js'; +import { ApResolverService } from '@/core/activitypub/ApResolverService.js'; +import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; +import { ApImageService } from '@/core/activitypub/models/ApImageService.js'; +import { ApMfmService } from '@/core/activitypub/ApMfmService.js'; +import { MfmService } from '@/core/MfmService.js'; +import { HashtagService } from '@/core/HashtagService.js'; +import UsersChart from '@/core/chart/charts/users.js'; +import { ChartLoggerService } from '@/core/chart/ChartLoggerService.js'; +import InstanceChart from '@/core/chart/charts/instance.js'; +import { ApLoggerService } from '@/core/activitypub/ApLoggerService.js'; +import { AccountMoveService } from '@/core/AccountMoveService.js'; +import { ReactionService } from '@/core/ReactionService.js'; +import { NotificationService } from '@/core/NotificationService.js'; + +process.env.NODE_ENV = 'test'; + +describe('UserEntityService', () => { + describe('pack/packMany', () => { + let app: TestingModule; + let service: UserEntityService; + let usersRepository: UsersRepository; + let userProfileRepository: UserProfilesRepository; + let userMemosRepository: UserMemoRepository; + let followingRepository: FollowingsRepository; + let followingRequestRepository: FollowRequestsRepository; + let blockingRepository: BlockingsRepository; + let mutingRepository: MutingsRepository; + let renoteMutingsRepository: RenoteMutingsRepository; + + async function createUser(userData: Partial = {}, profileData: Partial = {}) { + const un = secureRndstr(16); + const user = await usersRepository + .insert({ + ...userData, + id: genAidx(Date.now()), + username: un, + usernameLower: un, + }) + .then(x => usersRepository.findOneByOrFail(x.identifiers[0])); + + await userProfileRepository.insert({ + ...profileData, + userId: user.id, + }); + + return user; + } + + async function memo(writer: MiUser, target: MiUser, memo: string) { + await userMemosRepository.insert({ + id: genAidx(Date.now()), + userId: writer.id, + targetUserId: target.id, + memo, + }); + } + + async function follow(follower: MiUser, followee: MiUser) { + await followingRepository.insert({ + id: genAidx(Date.now()), + followerId: follower.id, + followeeId: followee.id, + }); + } + + async function requestFollow(requester: MiUser, requestee: MiUser) { + await followingRequestRepository.insert({ + id: genAidx(Date.now()), + followerId: requester.id, + followeeId: requestee.id, + }); + } + + async function block(blocker: MiUser, blockee: MiUser) { + await blockingRepository.insert({ + id: genAidx(Date.now()), + blockerId: blocker.id, + blockeeId: blockee.id, + }); + } + + async function mute(mutant: MiUser, mutee: MiUser) { + await mutingRepository.insert({ + id: genAidx(Date.now()), + muterId: mutant.id, + muteeId: mutee.id, + }); + } + + async function muteRenote(mutant: MiUser, mutee: MiUser) { + await renoteMutingsRepository.insert({ + id: genAidx(Date.now()), + muterId: mutant.id, + muteeId: mutee.id, + }); + } + + function randomIntRange(weight = 10) { + return [...Array(Math.floor(Math.random() * weight))].map((it, idx) => idx); + } + + beforeAll(async () => { + const services = [ + UserEntityService, + ApPersonService, + NoteEntityService, + PageEntityService, + CustomEmojiService, + AnnouncementService, + RoleService, + FederatedInstanceService, + IdService, + AvatarDecorationService, + UtilityService, + EmojiEntityService, + ModerationLogService, + GlobalEventService, + DriveFileEntityService, + MetaService, + FetchInstanceMetadataService, + CacheService, + ApResolverService, + ApNoteService, + ApImageService, + ApMfmService, + MfmService, + HashtagService, + UsersChart, + ChartLoggerService, + InstanceChart, + ApLoggerService, + AccountMoveService, + ReactionService, + NotificationService, + ]; + + app = await Test.createTestingModule({ + imports: [GlobalModule, CoreModule], + providers: [ + ...services, + ...services.map(x => ({ provide: x.name, useExisting: x })), + ], + }).compile(); + await app.init(); + app.enableShutdownHooks(); + + service = app.get(UserEntityService); + usersRepository = app.get(DI.usersRepository); + userProfileRepository = app.get(DI.userProfilesRepository); + userMemosRepository = app.get(DI.userMemosRepository); + followingRepository = app.get(DI.followingsRepository); + followingRequestRepository = app.get(DI.followRequestsRepository); + blockingRepository = app.get(DI.blockingsRepository); + mutingRepository = app.get(DI.mutingsRepository); + renoteMutingsRepository = app.get(DI.renoteMutingsRepository); + }); + + afterAll(async () => { + await app.close(); + }); + + test('UserLite', async() => { + const me = await createUser(); + const who = await createUser(); + + await memo(me, who, 'memo'); + + const actual = await service.pack(who, me, { schema: 'UserLite' }) as any; + // no detail + expect(actual.memo).toBeUndefined(); + // no detail and me + expect(actual.birthday).toBeUndefined(); + // no detail and me + expect(actual.achievements).toBeUndefined(); + }); + + test('UserDetailedNotMe', async() => { + const me = await createUser(); + const who = await createUser({}, { birthday: '2000-01-01' }); + + await memo(me, who, 'memo'); + + const actual = await service.pack(who, me, { schema: 'UserDetailedNotMe' }) as any; + // is detail + expect(actual.memo).toBe('memo'); + // is detail + expect(actual.birthday).toBe('2000-01-01'); + // no detail and me + expect(actual.achievements).toBeUndefined(); + }); + + test('MeDetailed', async() => { + const achievements = [{ name: 'achievement', unlockedAt: new Date().getTime() }]; + const me = await createUser({}, { + birthday: '2000-01-01', + achievements: achievements, + }); + await memo(me, me, 'memo'); + + const actual = await service.pack(me, me, { schema: 'MeDetailed' }) as any; + // is detail + expect(actual.memo).toBe('memo'); + // is detail + expect(actual.birthday).toBe('2000-01-01'); + // is detail and me + expect(actual.achievements).toEqual(achievements); + }); + + describe('packManyによるpreloadがある時、preloadが無い時とpackの結果が同じになるか見たい', () => { + test('no-preload', async() => { + const me = await createUser(); + // meがフォローしてる人たち + const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of followeeMe) { + await follow(me, who); + const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; + expect(actual.isFollowing).toBe(true); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + + // meをフォローしてる人たち + const followerMe = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of followerMe) { + await follow(who, me); + const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(true); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + + // meがフォローリクエストを送った人たち + const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of requestsFromYou) { + await requestFollow(me, who); + const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(true); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + + // meにフォローリクエストを送った人たち + const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of requestsToYou) { + await requestFollow(who, me); + const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(true); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + + // meがブロックしてる人たち + const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of blockingYou) { + await block(me, who); + const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(true); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + + // meをブロックしてる人たち + const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of blockingMe) { + await block(who, me); + const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(true); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + + // meがミュートしてる人たち + const muters = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of muters) { + await mute(me, who); + const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(true); + expect(actual.isRenoteMuted).toBe(false); + } + + // meがリノートミュートしてる人たち + const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of renoteMuters) { + await muteRenote(me, who); + const actual = await service.pack(who, me, { schema: 'UserDetailed' }) as any; + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(true); + } + }); + + test('preload', async() => { + const me = await createUser(); + + { + // meがフォローしてる人たち + const followeeMe = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of followeeMe) { + await follow(me, who); + } + const actualList = await service.packMany(followeeMe, me, { schema: 'UserDetailed' }) as any; + for (const actual of actualList) { + expect(actual.isFollowing).toBe(true); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + } + + { + // meをフォローしてる人たち + const followerMe = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of followerMe) { + await follow(who, me); + } + const actualList = await service.packMany(followerMe, me, { schema: 'UserDetailed' }) as any; + for (const actual of actualList) { + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(true); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + } + + { + // meがフォローリクエストを送った人たち + const requestsFromYou = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of requestsFromYou) { + await requestFollow(me, who); + } + const actualList = await service.packMany(requestsFromYou, me, { schema: 'UserDetailed' }) as any; + for (const actual of actualList) { + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(true); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + } + + { + // meにフォローリクエストを送った人たち + const requestsToYou = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of requestsToYou) { + await requestFollow(who, me); + } + const actualList = await service.packMany(requestsToYou, me, { schema: 'UserDetailed' }) as any; + for (const actual of actualList) { + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(true); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + } + + { + // meがブロックしてる人たち + const blockingYou = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of blockingYou) { + await block(me, who); + } + const actualList = await service.packMany(blockingYou, me, { schema: 'UserDetailed' }) as any; + for (const actual of actualList) { + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(true); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + } + + { + // meをブロックしてる人たち + const blockingMe = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of blockingMe) { + await block(who, me); + } + const actualList = await service.packMany(blockingMe, me, { schema: 'UserDetailed' }) as any; + for (const actual of actualList) { + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(true); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(false); + } + } + + { + // meがミュートしてる人たち + const muters = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of muters) { + await mute(me, who); + } + const actualList = await service.packMany(muters, me, { schema: 'UserDetailed' }) as any; + for (const actual of actualList) { + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(true); + expect(actual.isRenoteMuted).toBe(false); + } + } + + { + // meがリノートミュートしてる人たち + const renoteMuters = await Promise.all(randomIntRange().map(() => createUser())); + for (const who of renoteMuters) { + await muteRenote(me, who); + } + const actualList = await service.packMany(renoteMuters, me, { schema: 'UserDetailed' }) as any; + for (const actual of actualList) { + expect(actual.isFollowing).toBe(false); + expect(actual.isFollowed).toBe(false); + expect(actual.hasPendingFollowRequestFromYou).toBe(false); + expect(actual.hasPendingFollowRequestToYou).toBe(false); + expect(actual.isBlocking).toBe(false); + expect(actual.isBlocked).toBe(false); + expect(actual.isMuted).toBe(false); + expect(actual.isRenoteMuted).toBe(true); + } + } + }); + }); + }); +}); -- cgit v1.2.3-freya From 4b1ca9ef619903e9ff1de10101c18569efbe099c Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Fri, 15 Mar 2024 22:02:57 +0900 Subject: fix(general): `flash/create`でPlayの公開範囲を指定できない問題の修正と編集画面の調整 (#13574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): param `visibility` wasn't included in `flash/create` * fix(frontend): tweak flash editor ui * Update CHANGELOG.md --- CHANGELOG.md | 2 +- locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + packages/backend/src/server/api/endpoints/flash/create.ts | 2 ++ packages/frontend/src/pages/flash/flash-edit.vue | 14 ++++++++------ packages/misskey-js/src/autogen/types.ts | 5 +++++ 6 files changed, 21 insertions(+), 7 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index f1e863a8f2..fa56f1a268 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## Unreleased ### General -- +- Fix: Play作成時に設定した公開範囲が機能していない問題を修正 ### Client - Enhance: 自分のノートの添付ファイルから直接ファイルの詳細ページに飛べるように diff --git a/locales/index.d.ts b/locales/index.d.ts index 87065e1d37..7f4ec7ecb0 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -8635,6 +8635,10 @@ export interface Locale extends ILocale { * 説明 */ "summary": string; + /** + * 非公開に設定するとプロフィールに表示されなくなりますが、URLを知っている人は引き続きアクセスできます。 + */ + "visibilityDescription": string; }; "_pages": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f42fd6587a..8b44ac2121 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2280,6 +2280,7 @@ _play: title: "タイトル" script: "スクリプト" summary: "説明" + visibilityDescription: "非公開に設定するとプロフィールに表示されなくなりますが、URLを知っている人は引き続きアクセスできます。" _pages: newPage: "ページの作成" diff --git a/packages/backend/src/server/api/endpoints/flash/create.ts b/packages/backend/src/server/api/endpoints/flash/create.ts index 584d167a29..361496e17e 100644 --- a/packages/backend/src/server/api/endpoints/flash/create.ts +++ b/packages/backend/src/server/api/endpoints/flash/create.ts @@ -44,6 +44,7 @@ export const paramDef = { permissions: { type: 'array', items: { type: 'string', } }, + visibility: { type: 'string', enum: ['public', 'private'], default: 'public' }, }, required: ['title', 'summary', 'script', 'permissions'], } as const; @@ -66,6 +67,7 @@ export default class extends Endpoint { // eslint- summary: ps.summary, script: ps.script, permissions: ps.permissions, + visibility: ps.visibility, }).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0])); return await this.flashEntityService.pack(flash); diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue index 4418172e62..3625bc1164 100644 --- a/packages/frontend/src/pages/flash/flash-edit.vue +++ b/packages/frontend/src/pages/flash/flash-edit.vue @@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only -
- {{ i18n.ts.save }} - {{ i18n.ts.show }} - {{ i18n.ts.delete }} -
+ +
+ {{ i18n.ts.save }} + {{ i18n.ts.show }} + {{ i18n.ts.delete }} +
@@ -367,7 +368,7 @@ const props = defineProps<{ }>(); const flash = ref(null); -const visibility = ref('public'); +const visibility = ref<'private' | 'public'>('public'); if (props.id) { flash.value = await misskeyApi('flash/show', { @@ -420,6 +421,7 @@ async function save() { summary: summary.value, permissions: permissions.value, script: script.value, + visibility: visibility.value, }); router.push('/play/' + created.id + '/edit'); } diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index be3e86bd78..3c862f690e 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -22686,6 +22686,11 @@ export type operations = { summary: string; script: string; permissions: string[]; + /** + * @default public + * @enum {string} + */ + visibility?: 'public' | 'private'; }; }; }; -- cgit v1.2.3-freya From f4838e50b4043f917020dd1cfa7b75da087ff8f2 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 21 Mar 2024 07:51:01 +0900 Subject: enhance(antenna): Botの投稿を除外できるように (#13603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(antenna): Botの投稿を除外できるように (MisskeyIO#545) (cherry picked from commit a95ce067c6cf0a93647e358aabc984bdbe99e952) * Update Changelog * remove translations * spdx --------- Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com> --- CHANGELOG.md | 2 ++ locales/index.d.ts | 4 ++++ locales/ja-JP.yml | 1 + .../migration/1710919614510-antenna-exclude-bots.js | 16 ++++++++++++++++ packages/backend/src/core/AntennaService.ts | 6 ++++-- .../backend/src/core/entities/AntennaEntityService.ts | 1 + packages/backend/src/models/Antenna.ts | 5 +++++ packages/backend/src/models/json-schema/antenna.ts | 5 +++++ .../queue/processors/ExportAntennasProcessorService.ts | 1 + .../queue/processors/ImportAntennasProcessorService.ts | 2 ++ .../backend/src/server/api/endpoints/antennas/create.ts | 2 ++ .../backend/src/server/api/endpoints/antennas/update.ts | 2 ++ packages/backend/test/e2e/antennas.ts | 2 ++ packages/frontend/src/pages/my-antennas/create.vue | 1 + packages/frontend/src/pages/my-antennas/editor.vue | 3 +++ packages/misskey-js/src/autogen/types.ts | 4 ++++ 16 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 packages/backend/migration/1710919614510-antenna-exclude-bots.js (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index 18dd07f1c3..0dce1a0496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## Unreleased ### General +- Enhance: アンテナでBotによるノートを除外できるように + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545) - Fix: Play作成時に設定した公開範囲が機能していない問題を修正 ### Client diff --git a/locales/index.d.ts b/locales/index.d.ts index 7f4ec7ecb0..afb4adac6c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1616,6 +1616,10 @@ export interface Locale extends ILocale { * 除外キーワード */ "antennaExcludeKeywords": string; + /** + * Botアカウントを除外 + */ + "antennaExcludeBots": string; /** * スペースで区切るとAND指定になり、改行で区切るとOR指定になります */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8b44ac2121..a64c83b10f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -400,6 +400,7 @@ name: "名前" antennaSource: "受信ソース" antennaKeywords: "受信キーワード" antennaExcludeKeywords: "除外キーワード" +antennaExcludeBots: "Botアカウントを除外" antennaKeywordsDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります" notifyAntenna: "新しいノートを通知する" withFileAntenna: "ファイルが添付されたノートのみ" diff --git a/packages/backend/migration/1710919614510-antenna-exclude-bots.js b/packages/backend/migration/1710919614510-antenna-exclude-bots.js new file mode 100644 index 0000000000..fac84317cc --- /dev/null +++ b/packages/backend/migration/1710919614510-antenna-exclude-bots.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class AntennaExcludeBots1710919614510 { + name = 'AntennaExcludeBots1710919614510' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "antenna" ADD "excludeBots" boolean NOT NULL DEFAULT false`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "excludeBots"`); + } +} diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 4f956a43ed..793d8974b3 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -92,7 +92,7 @@ export class AntennaService implements OnApplicationShutdown { } @bindThis - public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise { + public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise { const antennas = await this.getAntennas(); const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const))); const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna); @@ -110,10 +110,12 @@ export class AntennaService implements OnApplicationShutdown { // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている @bindThis - public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; }): Promise { + public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise { if (note.visibility === 'specified') return false; if (note.visibility === 'followers') return false; + if (antenna.excludeBots && noteUser.isBot) return false; + if (antenna.localOnly && noteUser.host != null) return false; if (!antenna.withReplies && note.replyId != null) return false; diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index 64d6a3c978..3ec8efa6bf 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -39,6 +39,7 @@ export class AntennaEntityService { caseSensitive: antenna.caseSensitive, localOnly: antenna.localOnly, notify: antenna.notify, + excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, isActive: antenna.isActive, diff --git a/packages/backend/src/models/Antenna.ts b/packages/backend/src/models/Antenna.ts index 332a899768..f5e819059e 100644 --- a/packages/backend/src/models/Antenna.ts +++ b/packages/backend/src/models/Antenna.ts @@ -72,6 +72,11 @@ export class MiAntenna { }) public caseSensitive: boolean; + @Column('boolean', { + default: false, + }) + public excludeBots: boolean; + @Column('boolean', { default: false, }) diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts index 74622b6193..78cf6d3ba2 100644 --- a/packages/backend/src/models/json-schema/antenna.ts +++ b/packages/backend/src/models/json-schema/antenna.ts @@ -76,6 +76,11 @@ export const packedAntennaSchema = { type: 'boolean', optional: false, nullable: false, }, + excludeBots: { + type: 'boolean', + optional: false, nullable: false, + default: false, + }, withReplies: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index af48bad417..1d8e90f367 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -81,6 +81,7 @@ export class ExportAntennasProcessorService { }) : null, caseSensitive: antenna.caseSensitive, localOnly: antenna.localOnly, + excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, notify: antenna.notify, diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts index 951b560597..ff1c04de06 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -44,6 +44,7 @@ const validate = new Ajv().compile({ } }, caseSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, + excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, notify: { type: 'boolean' }, @@ -88,6 +89,7 @@ export class ImportAntennasProcessorService { users: (antenna.src === 'list' && antenna.userListAccts !== null ? antenna.userListAccts : antenna.users).filter(Boolean), caseSensitive: antenna.caseSensitive, localOnly: antenna.localOnly, + excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, notify: antenna.notify, diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 191de8f833..57c8eb4958 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -64,6 +64,7 @@ export const paramDef = { } }, caseSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, + excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, notify: { type: 'boolean' }, @@ -124,6 +125,7 @@ export default class extends Endpoint { // eslint- users: ps.users, caseSensitive: ps.caseSensitive, localOnly: ps.localOnly, + excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, notify: ps.notify, diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index 76a34924a0..e6720aacf8 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -63,6 +63,7 @@ export const paramDef = { } }, caseSensitive: { type: 'boolean' }, localOnly: { type: 'boolean' }, + excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, notify: { type: 'boolean' }, @@ -120,6 +121,7 @@ export default class extends Endpoint { // eslint- users: ps.users, caseSensitive: ps.caseSensitive, localOnly: ps.localOnly, + excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, notify: ps.notify, diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 7370b1963c..cf5c7dd130 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -44,6 +44,7 @@ describe('アンテナ', () => { users: [''], withFile: false, withReplies: false, + excludeBots: false, }; let root: User; @@ -156,6 +157,7 @@ describe('アンテナ', () => { users: [''], withFile: false, withReplies: false, + excludeBots: false, localOnly: false, }; assert.deepStrictEqual(response, expected); diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index 8b3b3cfbfd..2d026d2fa9 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -26,6 +26,7 @@ const draft = ref({ users: [], keywords: [], excludeKeywords: [], + excludeBots: false, withReplies: false, caseSensitive: false, localOnly: false, diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue index c6dcbadd9b..97edbc44ce 100644 --- a/packages/frontend/src/pages/my-antennas/editor.vue +++ b/packages/frontend/src/pages/my-antennas/editor.vue @@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only + {{ i18n.ts.antennaExcludeBots }} {{ i18n.ts.withReplies }} @@ -78,6 +79,7 @@ const keywords = ref(props.antenna.keywords.map(x => x.join(' ')).join(' const excludeKeywords = ref(props.antenna.excludeKeywords.map(x => x.join(' ')).join('\n')); const caseSensitive = ref(props.antenna.caseSensitive); const localOnly = ref(props.antenna.localOnly); +const excludeBots = ref(props.antenna.excludeBots); const withReplies = ref(props.antenna.withReplies); const withFile = ref(props.antenna.withFile); const notify = ref(props.antenna.notify); @@ -94,6 +96,7 @@ async function saveAntenna() { name: name.value, src: src.value, userListId: userListId.value, + excludeBots: excludeBots.value, withReplies: withReplies.value, withFile: withFile.value, notify: notify.value, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 3c862f690e..636bc62aaa 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4434,6 +4434,8 @@ export type components = { localOnly: boolean; notify: boolean; /** @default false */ + excludeBots: boolean; + /** @default false */ withReplies: boolean; withFile: boolean; isActive: boolean; @@ -9654,6 +9656,7 @@ export type operations = { users: string[]; caseSensitive: boolean; localOnly?: boolean; + excludeBots?: boolean; withReplies: boolean; withFile: boolean; notify: boolean; @@ -9935,6 +9938,7 @@ export type operations = { users?: string[]; caseSensitive?: boolean; localOnly?: boolean; + excludeBots?: boolean; withReplies?: boolean; withFile?: boolean; notify?: boolean; -- cgit v1.2.3-freya From 831c74a25b2db0ba3f6d43a9a1a9072d342b2822 Mon Sep 17 00:00:00 2001 From: おさむのひと <46447427+samunohito@users.noreply.github.com> Date: Thu, 21 Mar 2024 18:46:42 +0900 Subject: fix: URLプレビューの動作改善+動作設定を可能にする (#13579) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * support new version * URLプレビュー無効化時、フロント側も非表示にしてリクエストをしないようにする * fix lint * fix lint * tweak preview request error handles * fix: CHANGELOG.md * fix * fix --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 5 ++ locales/index.d.ts | 58 +++++++++++++++++ locales/ja-JP.yml | 15 +++++ .../migration/1710512074000-url-preview-meta.js | 42 ++++++++++++ packages/backend/package.json | 2 +- .../backend/src/core/entities/MetaEntityService.ts | 1 + packages/backend/src/models/Meta.ts | 38 +++++++++-- packages/backend/src/models/json-schema/meta.ts | 4 ++ .../backend/src/server/api/endpoints/admin/meta.ts | 34 +++++++++- .../src/server/api/endpoints/admin/update-meta.ts | 41 ++++++++++-- .../backend/src/server/web/UrlPreviewService.ts | 67 +++++++++++++++----- packages/frontend/src/components/MkLink.vue | 17 +++-- packages/frontend/src/components/MkNote.vue | 7 +- .../frontend/src/components/MkNoteDetailed.vue | 5 +- packages/frontend/src/components/MkUrlPreview.vue | 12 ++-- packages/frontend/src/components/global/MkUrl.vue | 3 +- .../frontend/src/components/page/page.text.vue | 5 +- packages/frontend/src/instance.ts | 2 + packages/frontend/src/pages/admin/security.vue | 16 ----- packages/frontend/src/pages/admin/settings.vue | 74 +++++++++++++++++++++- packages/misskey-js/src/autogen/types.ts | 20 +++++- pnpm-lock.yaml | 18 +++++- 22 files changed, 420 insertions(+), 66 deletions(-) create mode 100644 packages/backend/migration/1710512074000-url-preview-meta.js (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dce1a0496..188e146cdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## Unreleased +### Note +- コントロールパネル内にあるサマリープロキシの設定個所がセキュリティから全般へ変更となります。 + ### General +- Enhance: URLプレビューの有効化・無効化を設定できるように #13569 - Enhance: アンテナでBotによるノートを除外できるように (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/545) - Fix: Play作成時に設定した公開範囲が機能していない問題を修正 @@ -25,6 +29,7 @@ ### Server - Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに +- Enhance: misskey-dev/summaly@5.1.0の取り込み(プレビュー生成処理の効率化) - Fix: フォローリクエストを作成する際に既存のものは削除するように (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440) diff --git a/locales/index.d.ts b/locales/index.d.ts index afb4adac6c..70586d7a87 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4916,6 +4916,10 @@ export interface Locale extends ILocale { * リトライ */ "gameRetry": string; + /** + * 使用しない場合は空欄にしてください + */ + "notUsePleaseLeaveBlank": string; "_bubbleGame": { /** * 遊び方 @@ -9768,6 +9772,60 @@ export interface Locale extends ILocale { */ "header": string; }; + "_urlPreviewSetting": { + /** + * URLプレビューの設定 + */ + "title": string; + /** + * URLプレビューを有効にする + */ + "enable": string; + /** + * プレビュー取得時のタイムアウト(ms) + */ + "timeout": string; + /** + * プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。 + */ + "timeoutDescription": string; + /** + * Content-Lengthの最大値(byte) + */ + "maximumContentLength": string; + /** + * Content-Lengthがこの値を超えた場合、プレビューは生成されません。 + */ + "maximumContentLengthDescription": string; + /** + * Content-Lengthが取得できた場合のみプレビューを生成 + */ + "requireContentLength": string; + /** + * 相手サーバがContent-Lengthを返さない場合、プレビューは生成されません。 + */ + "requireContentLengthDescription": string; + /** + * User-Agent + */ + "userAgent": string; + /** + * プレビュー取得時に使用されるUser-Agentを設定します。空欄の場合、デフォルトのUser-Agentが使用されます。 + */ + "userAgentDescription": string; + /** + * プレビューを生成するプロキシのエンドポイント + */ + "summaryProxy": string; + /** + * Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。 + */ + "summaryProxyDescription": string; + /** + * プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。 + */ + "summaryProxyDescription2": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index a64c83b10f..cada6d855f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1225,6 +1225,7 @@ enableHorizontalSwipe: "スワイプしてタブを切り替える" loading: "読み込み中" surrender: "やめる" gameRetry: "リトライ" +notUsePleaseLeaveBlank: "使用しない場合は空欄にしてください" _bubbleGame: howToPlay: "遊び方" @@ -2602,3 +2603,17 @@ _offlineScreen: title: "オフライン - サーバーに接続できません" header: "サーバーに接続できません" +_urlPreviewSetting: + title: "URLプレビューの設定" + enable: "URLプレビューを有効にする" + timeout: "プレビュー取得時のタイムアウト(ms)" + timeoutDescription: "プレビュー取得の所要時間がこの値を超えた場合、プレビューは生成されません。" + maximumContentLength: "Content-Lengthの最大値(byte)" + maximumContentLengthDescription: "Content-Lengthがこの値を超えた場合、プレビューは生成されません。" + requireContentLength: "Content-Lengthが取得できた場合のみプレビューを生成" + requireContentLengthDescription: "相手サーバがContent-Lengthを返さない場合、プレビューは生成されません。" + userAgent: "User-Agent" + userAgentDescription: "プレビュー取得時に使用されるUser-Agentを設定します。空欄の場合、デフォルトのUser-Agentが使用されます。" + summaryProxy: "プレビューを生成するプロキシのエンドポイント" + summaryProxyDescription: "Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。" + summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。" diff --git a/packages/backend/migration/1710512074000-url-preview-meta.js b/packages/backend/migration/1710512074000-url-preview-meta.js new file mode 100644 index 0000000000..8af521bbf4 --- /dev/null +++ b/packages/backend/migration/1710512074000-url-preview-meta.js @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UrlPreviewMeta1710512074000 { + name = 'UrlPreviewMeta1710512074000' + + async up(queryRunner) { + await queryRunner.query(` + alter table meta + rename column "summalyProxy" to "urlPreviewSummaryProxyUrl"; + alter table meta + add "urlPreviewEnabled" boolean default true not null; + alter table meta + add "urlPreviewTimeout" integer default 10000 not null; + alter table meta + add "urlPreviewMaximumContentLength" bigint default 10485760 not null; + alter table meta + add "urlPreviewRequireContentLength" boolean default false not null; + alter table meta + add "urlPreviewUserAgent" varchar(1024) default null; + `); + } + + async down(queryRunner) { + await queryRunner.query(` + alter table meta + rename column "urlPreviewSummaryProxyUrl" to "summalyProxy"; + alter table meta + drop column "urlPreviewEnabled"; + alter table meta + drop column "urlPreviewTimeout"; + alter table meta + drop column "urlPreviewMaximumContentLength"; + alter table meta + drop column "urlPreviewRequireContentLength"; + alter table meta + drop column "urlPreviewUserAgent"; + `); + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index eaad96d5f6..d64fcc3d2a 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -80,7 +80,7 @@ "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", "@misskey-dev/sharp-read-bmp": "1.2.0", - "@misskey-dev/summaly": "5.0.3", + "@misskey-dev/summaly": "5.1.0", "@nestjs/common": "10.3.3", "@nestjs/core": "10.3.3", "@nestjs/testing": "10.3.3", diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index b50d76288f..9d054ab6a1 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -111,6 +111,7 @@ export class MetaEntityService { policies: { ...DEFAULT_POLICIES, ...instance.policies }, mediaProxy: this.config.mediaProxy, + enableUrlPreview: instance.urlPreviewEnabled, }; return packed; diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 66f19ce197..04a34bbbb4 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -277,12 +277,6 @@ export class MiMeta { }) public enableSensitiveMediaDetectionForVideos: boolean; - @Column('varchar', { - length: 1024, - nullable: true, - }) - public summalyProxy: string | null; - @Column('boolean', { default: false, }) @@ -588,4 +582,36 @@ export class MiMeta { default: 0, }) public notesPerOneAd: number; + + @Column('boolean', { + default: true, + }) + public urlPreviewEnabled: boolean; + + @Column('integer', { + default: 10000, + }) + public urlPreviewTimeout: number; + + @Column('bigint', { + default: 1024 * 1024 * 10, + }) + public urlPreviewMaximumContentLength: number; + + @Column('boolean', { + default: true, + }) + public urlPreviewRequireContentLength: boolean; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public urlPreviewSummaryProxyUrl: string | null; + + @Column('varchar', { + length: 1024, + nullable: true, + }) + public urlPreviewUserAgent: string | null; } diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 17789f3b46..473339a1ad 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -207,6 +207,10 @@ export const packedMetaLiteSchema = { type: 'string', optional: false, nullable: false, }, + enableUrlPreview: { + type: 'boolean', + optional: false, nullable: false, + }, backgroundImageUrl: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 88c5907bcc..f4ff573271 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -434,6 +434,8 @@ export const meta = { summalyProxy: { type: 'string', optional: false, nullable: true, + deprecated: true, + description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', }, themeColor: { type: 'string', @@ -451,6 +453,30 @@ export const meta = { type: 'string', optional: false, nullable: false, }, + urlPreviewEnabled: { + type: 'boolean', + optional: false, nullable: false, + }, + urlPreviewTimeout: { + type: 'number', + optional: false, nullable: false, + }, + urlPreviewMaximumContentLength: { + type: 'number', + optional: false, nullable: false, + }, + urlPreviewRequireContentLength: { + type: 'boolean', + optional: false, nullable: false, + }, + urlPreviewUserAgent: { + type: 'string', + optional: false, nullable: true, + }, + urlPreviewSummaryProxyUrl: { + type: 'string', + optional: false, nullable: true, + }, }, }, } as const; @@ -533,7 +559,6 @@ export default class extends Endpoint { // eslint- setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, proxyAccountId: instance.proxyAccountId, - summalyProxy: instance.summalyProxy, email: instance.email, smtpSecure: instance.smtpSecure, smtpHost: instance.smtpHost, @@ -577,6 +602,13 @@ export default class extends Endpoint { // eslint- perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, notesPerOneAd: instance.notesPerOneAd, + summalyProxy: instance.urlPreviewSummaryProxyUrl, + urlPreviewEnabled: instance.urlPreviewEnabled, + urlPreviewTimeout: instance.urlPreviewTimeout, + urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength, + urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength, + urlPreviewUserAgent: instance.urlPreviewUserAgent, + urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl, }; }); } diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index bffceef815..2f62d30ada 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -90,7 +90,6 @@ export const paramDef = { type: 'string', }, }, - summalyProxy: { type: 'string', nullable: true }, deeplAuthKey: { type: 'string', nullable: true }, deeplIsPro: { type: 'boolean' }, enableEmail: { type: 'boolean' }, @@ -150,6 +149,16 @@ export const paramDef = { type: 'string', }, }, + summalyProxy: { + type: 'string', nullable: true, + description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.', + }, + urlPreviewEnabled: { type: 'boolean' }, + urlPreviewTimeout: { type: 'integer' }, + urlPreviewMaximumContentLength: { type: 'integer' }, + urlPreviewRequireContentLength: { type: 'boolean' }, + urlPreviewUserAgent: { type: 'string', nullable: true }, + urlPreviewSummaryProxyUrl: { type: 'string', nullable: true }, }, required: [], } as const; @@ -353,10 +362,6 @@ export default class extends Endpoint { // eslint- set.langs = ps.langs.filter(Boolean); } - if (ps.summalyProxy !== undefined) { - set.summalyProxy = ps.summalyProxy; - } - if (ps.enableEmail !== undefined) { set.enableEmail = ps.enableEmail; } @@ -581,6 +586,32 @@ export default class extends Endpoint { // eslint- set.bannedEmailDomains = ps.bannedEmailDomains; } + if (ps.urlPreviewEnabled !== undefined) { + set.urlPreviewEnabled = ps.urlPreviewEnabled; + } + + if (ps.urlPreviewTimeout !== undefined) { + set.urlPreviewTimeout = ps.urlPreviewTimeout; + } + + if (ps.urlPreviewMaximumContentLength !== undefined) { + set.urlPreviewMaximumContentLength = ps.urlPreviewMaximumContentLength; + } + + if (ps.urlPreviewRequireContentLength !== undefined) { + set.urlPreviewRequireContentLength = ps.urlPreviewRequireContentLength; + } + + if (ps.urlPreviewUserAgent !== undefined) { + const value = (ps.urlPreviewUserAgent ?? '').trim(); + set.urlPreviewUserAgent = value === '' ? null : ps.urlPreviewUserAgent; + } + + if (ps.summalyProxy !== undefined || ps.urlPreviewSummaryProxyUrl !== undefined) { + const value = ((ps.urlPreviewSummaryProxyUrl ?? ps.summalyProxy) ?? '').trim(); + set.urlPreviewSummaryProxyUrl = value === '' ? null : value; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index c6a96e94cb..8f8f08a305 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { summaly } from '@misskey-dev/summaly'; +import { SummalyResult } from '@misskey-dev/summaly/built/summary.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; @@ -14,6 +15,7 @@ import { query } from '@/misc/prelude/url.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '@/server/api/error.js'; +import { MiMeta } from '@/models/Meta.js'; import type { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() @@ -62,24 +64,25 @@ export class UrlPreviewService { const meta = await this.metaService.fetch(); - this.logger.info(meta.summalyProxy + if (!meta.urlPreviewEnabled) { + reply.code(403); + return { + error: new ApiError({ + message: 'URL preview is disabled', + code: 'URL_PREVIEW_DISABLED', + id: '58b36e13-d2f5-0323-b0c6-76aa9dabefb8', + }), + }; + } + + this.logger.info(meta.urlPreviewSummaryProxyUrl ? `(Proxy) Getting preview of ${url}@${lang} ...` : `Getting preview of ${url}@${lang} ...`); + try { - const summary = meta.summalyProxy ? - await this.httpRequestService.getJson>(`${meta.summalyProxy}?${query({ - url: url, - lang: lang ?? 'ja-JP', - })}`) - : - await summaly(url, { - followRedirects: false, - lang: lang ?? 'ja-JP', - agent: this.config.proxy ? { - http: this.httpRequestService.httpAgent, - https: this.httpRequestService.httpsAgent, - } : undefined, - }); + const summary = meta.urlPreviewSummaryProxyUrl + ? await this.fetchSummaryFromProxy(url, meta, lang) + : await this.fetchSummary(url, meta, lang); this.logger.succ(`Got preview of ${url}: ${summary.title}`); @@ -100,6 +103,7 @@ export class UrlPreviewService { return summary; } catch (err) { this.logger.warn(`Failed to get preview of ${url}: ${err}`); + reply.code(422); reply.header('Cache-Control', 'max-age=86400, immutable'); return { @@ -111,4 +115,37 @@ export class UrlPreviewService { }; } } + + private fetchSummary(url: string, meta: MiMeta, lang?: string): Promise { + const agent = this.config.proxy + ? { + http: this.httpRequestService.httpAgent, + https: this.httpRequestService.httpsAgent, + } + : undefined; + + return summaly(url, { + followRedirects: false, + lang: lang ?? 'ja-JP', + agent: agent, + userAgent: meta.urlPreviewUserAgent ?? undefined, + operationTimeout: meta.urlPreviewTimeout, + contentLengthLimit: meta.urlPreviewMaximumContentLength, + contentLengthRequired: meta.urlPreviewRequireContentLength, + }); + } + + private fetchSummaryFromProxy(url: string, meta: MiMeta, lang?: string): Promise { + const proxy = meta.urlPreviewSummaryProxyUrl!; + const queryStr = query({ + url: url, + lang: lang ?? 'ja-JP', + userAgent: meta.urlPreviewUserAgent ?? undefined, + operationTimeout: meta.urlPreviewTimeout, + contentLengthLimit: meta.urlPreviewMaximumContentLength, + contentLengthRequired: meta.urlPreviewRequireContentLength, + }); + + return this.httpRequestService.getJson(`${proxy}?${queryStr}`); + } } diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue index 3f7aba2fe4..ca875242b4 100644 --- a/packages/frontend/src/components/MkLink.vue +++ b/packages/frontend/src/components/MkLink.vue @@ -18,6 +18,7 @@ import { defineAsyncComponent, ref } from 'vue'; import { url as local } from '@/config.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import * as os from '@/os.js'; +import { isEnabledUrlPreview } from '@/instance.js'; const props = withDefaults(defineProps<{ url: string; @@ -31,13 +32,15 @@ const target = self ? null : '_blank'; const el = ref(); -useTooltip(el, (showing) => { - os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { - showing, - url: props.url, - source: el.value instanceof HTMLElement ? el.value : el.value?.$el, - }, {}, 'closed'); -}); +if (isEnabledUrlPreview.value) { + useTooltip(el, (showing) => { + os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { + showing, + url: props.url, + source: el.value instanceof HTMLElement ? el.value : el.value?.$el, + }, {}, 'closed'); + }); +} diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 636bc62aaa..c6e36d80aa 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4810,6 +4810,7 @@ export type components = { enableServiceWorker: boolean; translatorAvailable: boolean; mediaProxy: string; + enableUrlPreview: boolean; backgroundImageUrl: string | null; impressumUrl: string | null; logoImageUrl: string | null; @@ -4962,11 +4963,21 @@ export type operations = { objectStorageS3ForcePathStyle: boolean; privacyPolicyUrl: string | null; repositoryUrl: string | null; + /** + * @deprecated + * @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. + */ summalyProxy: string | null; themeColor: string | null; tosUrl: string | null; uri: string; version: string; + urlPreviewEnabled: boolean; + urlPreviewTimeout: number; + urlPreviewMaximumContentLength: number; + urlPreviewRequireContentLength: boolean; + urlPreviewUserAgent: string | null; + urlPreviewSummaryProxyUrl: string | null; }; }; }; @@ -8862,7 +8873,6 @@ export type operations = { maintainerName?: string | null; maintainerEmail?: string | null; langs?: string[]; - summalyProxy?: string | null; deeplAuthKey?: string | null; deeplIsPro?: boolean; enableEmail?: boolean; @@ -8916,6 +8926,14 @@ export type operations = { perUserListTimelineCacheMax?: number; notesPerOneAd?: number; silencedHosts?: string[] | null; + /** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */ + summalyProxy?: string | null; + urlPreviewEnabled?: boolean; + urlPreviewTimeout?: number; + urlPreviewMaximumContentLength?: number; + urlPreviewRequireContentLength?: boolean; + urlPreviewUserAgent?: string | null; + urlPreviewSummaryProxyUrl?: string | null; }; }; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c1e228a95..383b31b1f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,8 +117,8 @@ importers: specifier: 1.2.0 version: 1.2.0 '@misskey-dev/summaly': - specifier: 5.0.3 - version: 5.0.3 + specifier: 5.1.0 + version: 5.1.0 '@nestjs/common': specifier: 10.3.3 version: 10.3.3(reflect-metadata@0.2.1)(rxjs@7.8.1) @@ -4772,6 +4772,20 @@ packages: jschardet: 3.0.0 private-ip: 2.3.3 trace-redirect: 1.0.6 + dev: true + + /@misskey-dev/summaly@5.1.0: + resolution: {integrity: sha512-WAUrgX3/z4h4aI8Y/WVwmJcJ6Fa1Zf2LJCSS651t9MHoWVGABLsQ2KCXRGmlpk4i+cMDNIwweObUroosE7j8rg==} + dependencies: + cheerio: 1.0.0-rc.12 + escape-regexp: 0.0.1 + got: 12.6.1 + html-entities: 2.3.2 + iconv-lite: 0.6.3 + jschardet: 3.0.0 + private-ip: 2.3.3 + trace-redirect: 1.0.6 + dev: false /@mole-inc/bin-wrapper@8.0.1: resolution: {integrity: sha512-sTGoeZnjI8N4KS+sW2AN95gDBErhAguvkw/tWdCjeM8bvxpz5lqrnd0vOJABA1A+Ic3zED7PYoLP/RANLgVotA==} -- cgit v1.2.3-freya From f90be427f51392ef3ed5a7eb7f35059274bb47fc Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Mon, 25 Mar 2024 18:31:30 +0900 Subject: fix(frontend): 「今日誕生日のフォロー中ユーザー」ウィジェットが正しく動作しない問題を修正 (#12835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * (fix) タイムゾーンによっては誕生日のフォロー中ユーザーが正しく読み込まれない * 文言をわかりやすく * Update Changelog * (add) reload button * Update CHANGELOG.md * run misskey-js * fix * Revert "文言をわかりやすく" This reverts commit c5ab6419563cc70ec8ba758e800c74d3469131e3. * Update packages/frontend/src/widgets/WidgetBirthdayFollowings.vue * Update packages/frontend/src/widgets/WidgetBirthdayFollowings.vue --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 2 ++ .../src/server/api/endpoints/users/following.ts | 7 ++--- .../src/widgets/WidgetBirthdayFollowings.vue | 35 ++++++++++++++++------ 3 files changed, 31 insertions(+), 13 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index d90b1425c1..f41ff2171f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - 実装の都合により、プラグインは1つエラーを起こした時に即時停止するようになりました - Enhance: ページのデザインを変更 - Enhance: 2要素認証(ワンタイムパスワード)の入力欄を改善 +- Enhance: 「今日誕生日のフォロー中ユーザー」ウィジェットを手動でリロードできるように - Fix: 一部のページ内リンクが正しく動作しない問題を修正 - Fix: 周年の実績が閏年を考慮しない問題を修正 - Fix: ローカルURLのプレビューポップアップが左上に表示される @@ -27,6 +28,7 @@ (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528) - Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177 - CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。 +- Fix: タイムゾーンによっては、「今日誕生日のフォロー中ユーザー」ウィジェットが正しく動作しない問題を修正 ### Server - Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 5d52ebba76..6b3389f0b2 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -6,6 +6,7 @@ import { IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository, FollowingsRepository, UserProfilesRepository } from '@/models/_.js'; +import { birthdaySchema } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; @@ -66,7 +67,7 @@ export const paramDef = { description: 'The local host is represented with `null`.', }, - birthday: { type: 'string', nullable: true }, + birthday: { ...birthdaySchema, nullable: true }, }, anyOf: [ { required: ['userId'] }, @@ -127,9 +128,7 @@ export default class extends Endpoint { // eslint- if (ps.birthday) { try { - const d = new Date(ps.birthday); - d.setHours(0, 0, 0, 0); - const birthday = `${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; + const birthday = ps.birthday.substring(5, 10); const birthdayUserQuery = this.userProfilesRepository.createQueryBuilder('user_profile'); birthdayUserQuery.select('user_profile.userId') .where(`SUBSTR(user_profile.birthday, 6, 5) = '${birthday}'`); diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index 5b448e2c3b..49fd103d37 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only +
@@ -53,7 +54,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -const users = ref([]); +const users = ref([]); const fetching = ref(true); let lastFetchedAt = '1970-01-01'; @@ -70,19 +71,35 @@ const fetch = () => { now.setHours(0, 0, 0, 0); if (now > lfAtD) { - misskeyApi('users/following', { - limit: 18, - birthday: now.toISOString(), - userId: $i.id, - }).then(res => { - users.value = res; - fetching.value = false; - }); + actualFetch(); lastFetchedAt = now.toISOString(); } }; +function actualFetch() { + if ($i == null) { + users.value = []; + fetching.value = false; + return; + } + + const now = new Date(); + now.setHours(0, 0, 0, 0); + fetching.value = true; + misskeyApi('users/following', { + limit: 18, + birthday: `${now.getFullYear().toString().padStart(4, '0')}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}`, + userId: $i.id, + }).then(res => { + users.value = res; + window.setTimeout(() => { + // 早すぎるとチカチカする + fetching.value = false; + }, 100); + }); +} + useInterval(fetch, 1000 * 60, { immediate: true, afterMounted: true, -- cgit v1.2.3-freya From b35ae97ba7b57ae2b04eb0cc25dd3360e321e537 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Sat, 30 Mar 2024 13:51:53 +0900 Subject: fix(backend): better `notes/translate` error response (#13631) * fix(backend): better `notes/translate` error response * Update CHANGELOG.md * test(backend): perform administrative operations as `root` --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 1 + .../src/server/api/endpoints/notes/translate.ts | 13 ++- packages/backend/test/e2e/note.ts | 107 ++++++++++++++++----- packages/misskey-js/src/autogen/types.ts | 4 + 4 files changed, 97 insertions(+), 28 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cfbb5f9c8..5963549cc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Enhance: misskey-dev/summaly@5.1.0の取り込み(プレビュー生成処理の効率化) - Fix: フォローリクエストを作成する際に既存のものは削除するように (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/440) +- Fix: エンドポイント`notes/translate`のエラーを改善 - Fix: CleanRemoteFilesProcessorService report progress from 100% (#13632) ## 2024.3.1 diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 78812351f4..38a9660aa2 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -21,7 +21,7 @@ export const meta = { res: { type: 'object', - optional: false, nullable: false, + optional: true, nullable: false, properties: { sourceLang: { type: 'string' }, text: { type: 'string' }, @@ -39,6 +39,11 @@ export const meta = { code: 'NO_SUCH_NOTE', id: 'bea9b03f-36e0-49c5-a4db-627a029f8971', }, + cannotTranslateInvisibleNote: { + message: 'Cannot translate invisible note.', + code: 'CANNOT_TRANSLATE_INVISIBLE_NOTE', + id: 'ea29f2ca-c368-43b3-aaf1-5ac3e74bbe5d', + }, }, } as const; @@ -72,17 +77,17 @@ export default class extends Endpoint { // eslint- }); if (!(await this.noteEntityService.isVisibleForMe(note, me.id))) { - return 204; // TODO: 良い感じのエラー返す + throw new ApiError(meta.errors.cannotTranslateInvisibleNote); } if (note.text == null) { - return 204; + return; } const instance = await this.metaService.fetch(); if (instance.deeplAuthKey == null) { - return 204; // TODO: 良い感じのエラー返す + throw new ApiError(meta.errors.unavailable); } let targetLang = ps.targetLang; diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 11016f58ae..bda31d9640 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -8,12 +8,13 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { MiNote } from '@/models/Note.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { api, initTestDb, post, signup, uploadFile, uploadUrl } from '../utils.js'; +import { api, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js'; import type * as misskey from 'misskey-js'; describe('Note', () => { let Notes: any; + let root: misskey.entities.SignupResponse; let alice: misskey.entities.SignupResponse; let bob: misskey.entities.SignupResponse; let tom: misskey.entities.SignupResponse; @@ -21,6 +22,7 @@ describe('Note', () => { beforeAll(async () => { const connection = await initTestDb(true); Notes = connection.getRepository(MiNote); + root = await signup({ username: 'root' }); alice = await signup({ username: 'alice' }); bob = await signup({ username: 'bob' }); tom = await signup({ username: 'tom', host: 'example.com' }); @@ -473,14 +475,14 @@ describe('Note', () => { value: true, }, } as any, - }, alice); + }, root); assert.strictEqual(res.status, 200); const assign = await api('admin/roles/assign', { userId: alice.id, roleId: res.body.id, - }, alice); + }, root); assert.strictEqual(assign.status, 204); assert.strictEqual(file.body!.isSensitive, false); @@ -508,11 +510,11 @@ describe('Note', () => { await api('admin/roles/unassign', { userId: alice.id, roleId: res.body.id, - }); + }, root); await api('admin/roles/delete', { roleId: res.body.id, - }, alice); + }, root); }); }); @@ -644,7 +646,7 @@ describe('Note', () => { sensitiveWords: [ 'test', ], - }, alice); + }, root); assert.strictEqual(sensitive.status, 204); @@ -663,7 +665,7 @@ describe('Note', () => { sensitiveWords: [ '/Test/i', ], - }, alice); + }, root); assert.strictEqual(sensitive.status, 204); @@ -680,7 +682,7 @@ describe('Note', () => { sensitiveWords: [ 'Test hoge', ], - }, alice); + }, root); assert.strictEqual(sensitive.status, 204); @@ -697,7 +699,7 @@ describe('Note', () => { prohibitedWords: [ 'test', ], - }, alice); + }, root); assert.strictEqual(prohibited.status, 204); @@ -716,7 +718,7 @@ describe('Note', () => { prohibitedWords: [ '/Test/i', ], - }, alice); + }, root); assert.strictEqual(prohibited.status, 204); @@ -733,7 +735,7 @@ describe('Note', () => { prohibitedWords: [ 'Test hoge', ], - }, alice); + }, root); assert.strictEqual(prohibited.status, 204); @@ -750,7 +752,7 @@ describe('Note', () => { prohibitedWords: [ 'test', ], - }, alice); + }, root); assert.strictEqual(prohibited.status, 204); @@ -785,7 +787,7 @@ describe('Note', () => { value: 0, }, } as any, - }, alice); + }, root); assert.strictEqual(res.status, 200); @@ -794,7 +796,7 @@ describe('Note', () => { const assign = await api('admin/roles/assign', { userId: alice.id, roleId: res.body.id, - }, alice); + }, root); assert.strictEqual(assign.status, 204); @@ -810,11 +812,11 @@ describe('Note', () => { await api('admin/roles/unassign', { userId: alice.id, roleId: res.body.id, - }); + }, root); await api('admin/roles/delete', { roleId: res.body.id, - }, alice); + }, root); }); test('ダイレクト投稿もエラーになる', async () => { @@ -839,7 +841,7 @@ describe('Note', () => { value: 0, }, } as any, - }, alice); + }, root); assert.strictEqual(res.status, 200); @@ -848,7 +850,7 @@ describe('Note', () => { const assign = await api('admin/roles/assign', { userId: alice.id, roleId: res.body.id, - }, alice); + }, root); assert.strictEqual(assign.status, 204); @@ -866,11 +868,11 @@ describe('Note', () => { await api('admin/roles/unassign', { userId: alice.id, roleId: res.body.id, - }); + }, root); await api('admin/roles/delete', { roleId: res.body.id, - }, alice); + }, root); }); test('ダイレクトの宛先とメンションが同じ場合は重複してカウントしない', async () => { @@ -895,7 +897,7 @@ describe('Note', () => { value: 1, }, } as any, - }, alice); + }, root); assert.strictEqual(res.status, 200); @@ -904,7 +906,7 @@ describe('Note', () => { const assign = await api('admin/roles/assign', { userId: alice.id, roleId: res.body.id, - }, alice); + }, root); assert.strictEqual(assign.status, 204); @@ -921,11 +923,11 @@ describe('Note', () => { await api('admin/roles/unassign', { userId: alice.id, roleId: res.body.id, - }); + }, root); await api('admin/roles/delete', { roleId: res.body.id, - }, alice); + }, root); }); }); @@ -960,4 +962,61 @@ describe('Note', () => { assert.strictEqual(mainNote.repliesCount, 0); }); }); + + describe('notes/translate', () => { + describe('翻訳機能の利用が許可されていない場合', () => { + let cannotTranslateRole: misskey.entities.Role; + + beforeAll(async () => { + cannotTranslateRole = await role(root, {}, { canUseTranslator: false }); + await api('admin/roles/assign', { roleId: cannotTranslateRole.id, userId: alice.id }, root); + }); + + test('翻訳機能の利用が許可されていない場合翻訳できない', async () => { + const aliceNote = await post(alice, { text: 'Hello' }); + const res = await api('notes/translate', { + noteId: aliceNote.id, + targetLang: 'ja', + }, alice); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'UNAVAILABLE'); + }); + + afterAll(async () => { + await api('admin/roles/unassign', { roleId: cannotTranslateRole.id, userId: alice.id }, root); + }); + }); + + test('存在しないノートは翻訳できない', async () => { + const res = await api('notes/translate', { noteId: 'foo', targetLang: 'ja' }, alice); + + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'NO_SUCH_NOTE'); + }); + + test('不可視なノートは翻訳できない', async () => { + const aliceNote = await post(alice, { visibility: 'followers', text: 'Hello' }); + const bobTranslateAttempt = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, bob); + + assert.strictEqual(bobTranslateAttempt.status, 400); + assert.strictEqual(bobTranslateAttempt.body.error.code, 'CANNOT_TRANSLATE_INVISIBLE_NOTE'); + }); + + test('text: null なノートを翻訳すると空のレスポンスが返ってくる', async () => { + const aliceNote = await post(alice, { text: null, poll: { choices: ['kinoko', 'takenoko'] } }); + const res = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, alice); + + assert.strictEqual(res.status, 204); + }); + + test('サーバーに DeepL 認証キーが登録されていない場合翻訳できない', async () => { + const aliceNote = await post(alice, { text: 'Hello' }); + const res = await api('notes/translate', { noteId: aliceNote.id, targetLang: 'ja' }, alice); + + // NOTE: デフォルトでは登録されていないので落ちる + assert.strictEqual(res.status, 400); + assert.strictEqual(res.body.error.code, 'UNAVAILABLE'); + }); + }); }); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 4b2ad16b0f..b6b26c000c 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -21864,6 +21864,10 @@ export type operations = { }; }; }; + /** @description OK (without any results) */ + 204: { + content: never; + }; /** @description Client error */ 400: { content: { -- cgit v1.2.3-freya From 959cc8ff37de620bf0082f48f59963c00d045fe9 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Sun, 7 Apr 2024 21:14:13 +0900 Subject: refactor(general): use `Date.now()` instead of creating a new `Date` instance (#13671) --- packages/backend/src/core/AccountMoveService.ts | 4 ++-- packages/backend/src/core/PushNotificationService.ts | 2 +- packages/backend/src/server/api/endpoints/i/import-blocking.ts | 2 +- packages/backend/src/server/api/endpoints/i/import-following.ts | 2 +- packages/backend/src/server/api/endpoints/i/import-muting.ts | 2 +- packages/backend/src/server/api/endpoints/i/import-user-lists.ts | 2 +- packages/frontend/src/components/global/MkTime.vue | 4 ++-- packages/frontend/src/widgets/WidgetUnixClock.vue | 6 +++--- packages/sw/src/sw.ts | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index 5bd885df40..b6b591d240 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -305,7 +305,7 @@ export class AccountMoveService { let resultUser: MiLocalUser | MiRemoteUser | null = null; if (this.userEntityService.isRemoteUser(dst)) { - if ((new Date()).getTime() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { + if (Date.now() - (dst.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { await this.apPersonService.updatePerson(dst.uri); } dst = await this.apPersonService.fetchPerson(dst.uri) ?? dst; @@ -321,7 +321,7 @@ export class AccountMoveService { if (!src) continue; // oldAccountを探してもこのサーバーに存在しない場合はフォロー関係もないということなのでスルー if (this.userEntityService.isRemoteUser(dst)) { - if ((new Date()).getTime() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { + if (Date.now() - (src.lastFetchedAt?.getTime() ?? 0) > 10 * 1000) { await this.apPersonService.updatePerson(srcUri); } diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index 3b706d9854..6a845b951d 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -101,7 +101,7 @@ export class PushNotificationService implements OnApplicationShutdown { type, body: (type === 'notification' || type === 'unreadAntennaNote') ? truncateBody(type, body) : body, userId, - dateTime: (new Date()).getTime(), + dateTime: Date.now(), }), { proxy: this.config.proxy, }).catch((err: any) => { diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts index 8ddbe5663e..2606108539 100644 --- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts +++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts @@ -75,7 +75,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts index 390dd9cd71..d5e824df27 100644 --- a/packages/backend/src/server/api/endpoints/i/import-following.ts +++ b/packages/backend/src/server/api/endpoints/i/import-following.ts @@ -75,7 +75,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts index 51a9cdf5a5..0f5800404e 100644 --- a/packages/backend/src/server/api/endpoints/i/import-muting.ts +++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts @@ -75,7 +75,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts index a3b67301a7..bacdd5c88f 100644 --- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts +++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts @@ -74,7 +74,7 @@ export default class extends Endpoint { // eslint- const checkMoving = await this.accountMoveService.validateAlsoKnownAs( me, - (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > (new Date()).getTime(), + (old, src) => !!src.movedAt && src.movedAt.getTime() + 1000 * 60 * 60 * 2 > Date.now(), true, ); if (checkMoving ? file.size > 32 * 1024 * 1024 : file.size > 64 * 1024) throw new ApiError(meta.errors.tooBigFile); diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 67532268d3..23fe99bd9c 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -47,7 +47,7 @@ const invalid = Number.isNaN(_time); const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid; // eslint-disable-next-line vue/no-setup-props-destructure -const now = ref((props.origin ?? new Date()).getTime()); +const now = ref(props.origin?.getTime() ?? Date.now()); const ago = computed(() => (now.value - _time) / 1000/*ms*/); const relative = computed(() => { @@ -77,7 +77,7 @@ let tickId: number; let currentInterval: number; function tick() { - now.value = (new Date()).getTime(); + now.value = Date.now(); const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000; if (currentInterval !== nextInterval) { diff --git a/packages/frontend/src/widgets/WidgetUnixClock.vue b/packages/frontend/src/widgets/WidgetUnixClock.vue index 2ac7d1c781..832cd575cc 100644 --- a/packages/frontend/src/widgets/WidgetUnixClock.vue +++ b/packages/frontend/src/widgets/WidgetUnixClock.vue @@ -68,9 +68,9 @@ watch(showColon, (v) => { }); const tick = () => { - const now = new Date(); - ss.value = Math.floor(now.getTime() / 1000).toString(); - ms.value = Math.floor(now.getTime() % 1000 / 10).toString().padStart(2, '0'); + const now = Date.now(); + ss.value = Math.floor(now / 1000).toString(); + ms.value = Math.floor(now % 1000 / 10).toString().padStart(2, '0'); if (ss.value !== prevSec) showColon.value = true; prevSec = ss.value; }; diff --git a/packages/sw/src/sw.ts b/packages/sw/src/sw.ts index 46fe9fc90f..cc79d88713 100644 --- a/packages/sw/src/sw.ts +++ b/packages/sw/src/sw.ts @@ -76,7 +76,7 @@ globalThis.addEventListener('push', ev => { case 'notification': case 'unreadAntennaNote': // 1日以上経過している場合は無視 - if ((new Date()).getTime() - data.dateTime > 1000 * 60 * 60 * 24) break; + if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break; return createNotification(data); case 'readAllNotifications': -- cgit v1.2.3-freya From 5c7c44c9ebd12e9ae0dd6d7fab8f6dd78ba54eb7 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sat, 13 Apr 2024 20:38:25 +0900 Subject: fix(backend): 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように (#13703) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように (MisskeyIO#606) (cherry picked from commit 6b7df2bd10dc28b84f525a621b66fc49bf59cac6) * Update Changelog --------- Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com> --- CHANGELOG.md | 2 ++ packages/backend/src/server/api/endpoints/i/update-email.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index 1332da69f9..d184a0b398 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ - Fix: エンドポイント`notes/translate`のエラーを改善 - Fix: CleanRemoteFilesProcessorService report progress from 100% (#13632) - Fix: 一部の音声ファイルが映像ファイルとして扱われる問題を修正 +- Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように + (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606) ## 2024.3.1 diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index 3868278690..eea657ebbd 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -15,6 +15,7 @@ import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { UserAuthService } from '@/core/UserAuthService.js'; +import { MetaService } from '@/core/MetaService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -39,6 +40,12 @@ export const meta = { code: 'UNAVAILABLE', id: 'a2defefb-f220-8849-0af6-17f816099323', }, + + emailRequired: { + message: 'Email address is required.', + code: 'EMAIL_REQUIRED', + id: '324c7a88-59f2-492f-903f-89134f93e47e', + }, }, res: { @@ -66,6 +73,7 @@ export default class extends Endpoint { // eslint- @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, + private metaService: MetaService, private userEntityService: UserEntityService, private emailService: EmailService, private userAuthService: UserAuthService, @@ -97,6 +105,8 @@ export default class extends Endpoint { // eslint- if (!res.available) { throw new ApiError(meta.errors.unavailable); } + } else if ((await this.metaService.fetch()).emailRequiredForSignup) { + throw new ApiError(meta.errors.emailRequired); } await this.userProfilesRepository.update(me.id, { -- cgit v1.2.3-freya From 8c5d9a6295ab506b935bbd5856894239997a8158 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Sun, 14 Apr 2024 10:23:48 +0900 Subject: fix(backend): incorrect logic for determining whether Quote or not (#13700) * fix(backend): incorrect logic for determining whether Quote or not * Update CHANGELOG.md --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 1 + .../src/core/FanoutTimelineEndpointService.ts | 6 +- packages/backend/src/core/NoteCreateService.ts | 23 +++- packages/backend/src/core/NoteDeleteService.ts | 4 +- packages/backend/src/misc/is-pure-renote.ts | 15 --- packages/backend/src/misc/is-quote.ts | 12 -- packages/backend/src/misc/is-renote.ts | 36 ++++++ .../backend/src/server/ActivityPubServerService.ts | 4 +- .../src/server/api/endpoints/notes/create.ts | 6 +- packages/backend/test/unit/NoteCreateService.ts | 144 +++++++++++++++++++++ packages/backend/test/unit/misc/is-renote.ts | 88 +++++++++++++ 11 files changed, 296 insertions(+), 43 deletions(-) delete mode 100644 packages/backend/src/misc/is-pure-renote.ts delete mode 100644 packages/backend/src/misc/is-quote.ts create mode 100644 packages/backend/src/misc/is-renote.ts create mode 100644 packages/backend/test/unit/NoteCreateService.ts create mode 100644 packages/backend/test/unit/misc/is-renote.ts (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index d184a0b398..47e8e0cf19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ - Fix: エンドポイント`notes/translate`のエラーを改善 - Fix: CleanRemoteFilesProcessorService report progress from 100% (#13632) - Fix: 一部の音声ファイルが映像ファイルとして扱われる問題を修正 +- Fix: リプライのみの引用リノートと、CWのみの引用リノートが純粋なリノートとして誤って扱われてしまう問題を修正 - Fix: 登録にメール認証が必須になっている場合、登録されているメールアドレスを削除できないように (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/606) 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 } { - 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 + }; + +type Quote = + Renote & ({ + text: NonNullable + } | { + cw: NonNullable + } | { + replyId: NonNullable + reply: NonNullable + } | { + 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 { - 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 { // 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 { // 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); + }); + + 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); + }); +}); -- cgit v1.2.3-freya From 2ff90a80d453e33caee2cc39f27149d1d7386ee1 Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:36:01 +0900 Subject: fix(backend): add detailed schema to `fetch-rss` endpoint (#13764) --- .../backend/src/server/api/endpoints/fetch-rss.ts | 179 ++++++++++++++++++++- .../frontend/src/ui/_common_/statusbar-rss.vue | 5 +- packages/frontend/src/widgets/WidgetRss.vue | 7 +- packages/frontend/src/widgets/WidgetRssTicker.vue | 7 +- packages/misskey-js/src/autogen/types.ts | 47 +++++- 5 files changed, 234 insertions(+), 11 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/server/api/endpoints/fetch-rss.ts b/packages/backend/src/server/api/endpoints/fetch-rss.ts index 2085b06365..ba48b0119e 100644 --- a/packages/backend/src/server/api/endpoints/fetch-rss.ts +++ b/packages/backend/src/server/api/endpoints/fetch-rss.ts @@ -20,13 +20,188 @@ export const meta = { res: { type: 'object', properties: { + image: { + type: 'object', + optional: true, + properties: { + link: { + type: 'string', + optional: true, + }, + url: { + type: 'string', + optional: false, + }, + title: { + type: 'string', + optional: true, + }, + }, + }, + paginationLinks: { + type: 'object', + optional: true, + properties: { + self: { + type: 'string', + optional: true, + }, + first: { + type: 'string', + optional: true, + }, + next: { + type: 'string', + optional: true, + }, + last: { + type: 'string', + optional: true, + }, + prev: { + type: 'string', + optional: true, + }, + }, + }, + link: { + type: 'string', + optional: true, + }, + title: { + type: 'string', + optional: true, + }, items: { type: 'array', + optional: false, items: { type: 'object', + properties: { + link: { + type: 'string', + optional: true, + }, + guid: { + type: 'string', + optional: true, + }, + title: { + type: 'string', + optional: true, + }, + pubDate: { + type: 'string', + optional: true, + }, + creator: { + type: 'string', + optional: true, + }, + summary: { + type: 'string', + optional: true, + }, + content: { + type: 'string', + optional: true, + }, + isoDate: { + type: 'string', + optional: true, + }, + categories: { + type: 'array', + optional: true, + items: { + type: 'string', + }, + }, + contentSnippet: { + type: 'string', + optional: true, + }, + enclosure: { + type: 'object', + optional: true, + properties: { + url: { + type: 'string', + optional: false, + }, + length: { + type: 'number', + optional: true, + }, + type: { + type: 'string', + optional: true, + }, + }, + }, + }, + }, + }, + feedUrl: { + type: 'string', + optional: true, + }, + description: { + type: 'string', + optional: true, + }, + itunes: { + type: 'object', + optional: true, + additionalProperties: true, + properties: { + image: { + type: 'string', + optional: true, + }, + owner: { + type: 'object', + optional: true, + properties: { + name: { + type: 'string', + optional: true, + }, + email: { + type: 'string', + optional: true, + }, + }, + }, + author: { + type: 'string', + optional: true, + }, + summary: { + type: 'string', + optional: true, + }, + explicit: { + type: 'string', + optional: true, + }, + categories: { + type: 'array', + optional: true, + items: { + type: 'string', + }, + }, + keywords: { + type: 'array', + optional: true, + items: { + type: 'string', + }, + }, }, - } - } + }, + }, }, } as const; diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index b973a4fd6b..6e1d06eec1 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue index bcd6eb7c0f..e50b208775 100644 --- a/packages/frontend/src/pages/announcements.vue +++ b/packages/frontend/src/pages/announcements.vue @@ -21,14 +21,19 @@ SPDX-License-Identifier: AGPL-3.0-only - {{ announcement.title }} + {{ announcement.title }}
-
- -
+ +
+ {{ i18n.ts.createdAt }}: +
+
+ {{ i18n.ts.updatedAt }}: +
+
{{ i18n.ts.gotIt }} @@ -73,24 +78,24 @@ const paginationEl = ref>(); const tab = ref('current'); -async function read(announcement) { - if (announcement.needConfirmationToRead) { +async function read(target) { + if (target.needConfirmationToRead) { const confirm = await os.confirm({ type: 'question', title: i18n.ts._announcement.readConfirmTitle, - text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }), + text: i18n.tsx._announcement.readConfirmText({ title: target.title }), }); if (confirm.canceled) return; } if (!paginationEl.value) return; - paginationEl.value.updateItem(announcement.id, a => { + paginationEl.value.updateItem(target.id, a => { a.isRead = true; return a; }); - misskeyApi('i/read-announcement', { announcementId: announcement.id }); + misskeyApi('i/read-announcement', { announcementId: target.id }); updateAccount({ - unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== announcement.id), + unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id), }); } diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index c5b576f505..c12ae0fa57 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -193,6 +193,9 @@ const routes: RouteDef[] = [{ }, { path: '/announcements', component: page(() => import('@/pages/announcements.vue')), +}, { + path: '/announcements/:announcementId', + component: page(() => import('@/pages/announcement.vue')), }, { path: '/about', component: page(() => import('@/pages/about.vue')), diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue index 362c29e6c2..374bc20b54 100644 --- a/packages/frontend/src/ui/_common_/announcements.vue +++ b/packages/frontend/src/ui/_common_/announcements.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only v-for="announcement in $i.unreadAnnouncements.filter(x => x.display === 'banner')" :key="announcement.id" :class="$style.item" - to="/announcements" + :to="`/announcements/${announcement.id}`" > diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 9cdb61da87..6ff711cabb 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -336,6 +336,12 @@ type AnnouncementsRequest = operations['announcements']['requestBody']['content' // @public (undocumented) type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json']; +// @public (undocumented) +type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json']; + +// @public (undocumented) +type AnnouncementsShowResponse = operations['announcements___show']['responses']['200']['content']['application/json']; + // @public (undocumented) type Antenna = components['schemas']['Antenna']; @@ -1224,6 +1230,8 @@ declare namespace entities { AdminRolesUsersResponse, AnnouncementsRequest, AnnouncementsResponse, + AnnouncementsShowRequest, + AnnouncementsShowResponse, AntennasCreateRequest, AntennasCreateResponse, AntennasDeleteRequest, diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index 729107a78d..181f7274b7 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -851,6 +851,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * No description provided. + * + * **Credential required**: *No* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * No description provided. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index b0982e1e55..ab3baf1670 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -101,6 +101,8 @@ import type { AdminRolesUsersResponse, AnnouncementsRequest, AnnouncementsResponse, + AnnouncementsShowRequest, + AnnouncementsShowResponse, AntennasCreateRequest, AntennasCreateResponse, AntennasDeleteRequest, @@ -631,6 +633,7 @@ export type Endpoints = { 'admin/roles/update-default-policies': { req: AdminRolesUpdateDefaultPoliciesRequest; res: EmptyResponse }; 'admin/roles/users': { req: AdminRolesUsersRequest; res: AdminRolesUsersResponse }; 'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse }; + 'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse }; 'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse }; 'antennas/delete': { req: AntennasDeleteRequest; res: EmptyResponse }; 'antennas/list': { req: EmptyRequest; res: AntennasListResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index 60bf6659c0..02ca932d8a 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -104,6 +104,8 @@ export type AdminRolesUsersRequest = operations['admin___roles___users']['reques export type AdminRolesUsersResponse = operations['admin___roles___users']['responses']['200']['content']['application/json']; export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json']; export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json']; +export type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json']; +export type AnnouncementsShowResponse = operations['announcements___show']['responses']['200']['content']['application/json']; export type AntennasCreateRequest = operations['antennas___create']['requestBody']['content']['application/json']; export type AntennasCreateResponse = operations['antennas___create']['responses']['200']['content']['application/json']; export type AntennasDeleteRequest = operations['antennas___delete']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 1e9c190ca5..208f03dc3e 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -706,6 +706,15 @@ export type paths = { */ post: operations['announcements']; }; + '/announcements/show': { + /** + * announcements/show + * @description No description provided. + * + * **Credential required**: *No* + */ + post: operations['announcements___show']; + }; '/antennas/create': { /** * antennas/create @@ -9662,6 +9671,60 @@ export type operations = { }; }; }; + /** + * announcements/show + * @description No description provided. + * + * **Credential required**: *No* + */ + announcements___show: { + requestBody: { + content: { + 'application/json': { + /** Format: misskey:id */ + announcementId: string; + }; + }; + }; + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['Announcement']; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * antennas/create * @description No description provided. -- cgit v1.2.3-freya From 805a11aadbbc0f0a32531fd86443de514df74466 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Mon, 27 May 2024 17:18:12 +0900 Subject: enhance(backend): プロフィールのリンク検証にtry-catchを追加 (#13882) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(backend): プロフィールのリンク検証にtry-catchを追加 * :v: --- .../backend/src/server/api/endpoints/i/update.ts | 36 +++++++++++++--------- 1 file changed, 21 insertions(+), 15 deletions(-) (limited to 'packages/backend/src/server/api/endpoints') diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 84a1931a3d..a8e702f328 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -498,26 +498,32 @@ export default class extends Endpoint { // eslint- private async verifyLink(url: string, user: MiLocalUser) { if (!safeForSql(url)) return; - const html = await this.httpRequestService.getHtml(url); + try { + const html = await this.httpRequestService.getHtml(url); - const { window } = new JSDOM(html); - const doc = window.document; + const { window } = new JSDOM(html); + const doc = window.document; - const myLink = `${this.config.url}/@${user.username}`; + const myLink = `${this.config.url}/@${user.username}`; - const aEls = Array.from(doc.getElementsByTagName('a')); - const linkEls = Array.from(doc.getElementsByTagName('link')); + const aEls = Array.from(doc.getElementsByTagName('a')); + const linkEls = Array.from(doc.getElementsByTagName('link')); - const includesMyLink = aEls.some(a => a.href === myLink); - const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink); + const includesMyLink = aEls.some(a => a.href === myLink); + const includesRelMeLinks = [...aEls, ...linkEls].some(link => link.rel === 'me' && link.href === myLink); - if (includesMyLink || includesRelMeLinks) { - await this.userProfilesRepository.createQueryBuilder('profile').update() - .where('userId = :userId', { userId: user.id }) - .set({ - verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている - }) - .execute(); + if (includesMyLink || includesRelMeLinks) { + await this.userProfilesRepository.createQueryBuilder('profile').update() + .where('userId = :userId', { userId: user.id }) + .set({ + verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている + }) + .execute(); + } + + window.close(); + } catch (err) { + // なにもしない } } } -- cgit v1.2.3-freya From 4579be0f5401001bcfc27c4d56133cc910f3f581 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Mon, 27 May 2024 20:54:53 +0900 Subject: 新着ノートをサウンドで通知する機能をdeck UIに追加 (#13867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(deck-ui): implement note notification * chore: remove notify in antenna * docs(changelog): 新着ノートをサウンドで通知する機能をdeck UIに追加 * fix: type error in test * lint: key order * fix: remove notify column * test: remove test for notify * chore: make sound selectable * fix: add license header * fix: add license header again * Unnecessary await Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> * ファイルを選択してください -> ファイルが選択されていません * fix: i18n忘れ * fix: i18n忘れ * pleaseSelectFile > fileNotSelected --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> --- CHANGELOG.md | 1 + locales/index.d.ts | 8 ++ locales/ja-JP.yml | 2 + .../migration/1716450883149-RemoveAntennaNotify.js | 16 +++ .../src/core/entities/AntennaEntityService.ts | 1 - packages/backend/src/models/Antenna.ts | 3 - packages/backend/src/models/json-schema/antenna.ts | 4 - .../processors/ExportAntennasProcessorService.ts | 1 - .../processors/ImportAntennasProcessorService.ts | 4 +- .../src/server/api/endpoints/antennas/create.ts | 4 +- .../src/server/api/endpoints/antennas/update.ts | 2 - packages/backend/test/e2e/antennas.ts | 4 - packages/backend/test/e2e/move.ts | 2 - .../frontend/src/components/MkFormDialog.file.vue | 71 ++++++++++++++ packages/frontend/src/components/MkFormDialog.vue | 12 ++- packages/frontend/src/os.ts | 2 +- packages/frontend/src/pages/my-antennas/editor.vue | 3 - packages/frontend/src/scripts/form.ts | 30 ++++-- packages/frontend/src/ui/deck/antenna-column.vue | 24 ++++- packages/frontend/src/ui/deck/channel-column.vue | 23 ++++- packages/frontend/src/ui/deck/deck-store.ts | 2 + packages/frontend/src/ui/deck/list-column.vue | 22 ++++- .../frontend/src/ui/deck/role-timeline-column.vue | 23 ++++- packages/frontend/src/ui/deck/tl-column.vue | 20 +++- .../frontend/src/ui/deck/tl-note-notification.ts | 107 +++++++++++++++++++++ packages/misskey-js/src/autogen/types.ts | 3 - 26 files changed, 341 insertions(+), 53 deletions(-) create mode 100644 packages/backend/migration/1716450883149-RemoveAntennaNotify.js create mode 100644 packages/frontend/src/components/MkFormDialog.file.vue create mode 100644 packages/frontend/src/ui/deck/tl-note-notification.ts (limited to 'packages/backend/src/server/api/endpoints') diff --git a/CHANGELOG.md b/CHANGELOG.md index f8463f8cbf..0a70fc7a8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ - Enhance: AiScriptを0.18.0にバージョンアップ - Enhance: 通常のノートでも、お気に入りに登録したチャンネルにリノートできるように - Enhance: 長いテキストをペーストした際にテキストファイルとして添付するかどうかを選択できるように +- Enhance: 新着ノートをサウンドで通知する機能をdeck UIに追加しました - Enhance: コントロールパネルのクイックアクションからファイルを照会できるように - Enhance: コントロールパネルのクイックアクションから通常の照会を行えるように - Fix: 一部のページ内リンクが正しく動作しない問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index eb7e297aa3..d4ded0bb5b 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1280,6 +1280,10 @@ export interface Locale extends ILocale { * フォルダーを選択 */ "selectFolders": string; + /** + * ファイルが選択されていません + */ + "fileNotSelected": string; /** * ファイル名を変更 */ @@ -9143,6 +9147,10 @@ export interface Locale extends ILocale { * カラムを追加 */ "addColumn": string; + /** + * 新着ノート通知の設定 + */ + "newNoteNotificationSettings": string; /** * カラムの設定 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ebaf16745c..d7ceb971af 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -316,6 +316,7 @@ selectFile: "ファイルを選択" selectFiles: "ファイルを選択" selectFolder: "フォルダーを選択" selectFolders: "フォルダーを選択" +fileNotSelected: "ファイルが選択されていません" renameFile: "ファイル名を変更" folderName: "フォルダー名" createFolder: "フォルダーを作成" @@ -2420,6 +2421,7 @@ _deck: alwaysShowMainColumn: "常にメインカラムを表示" columnAlign: "カラムの寄せ" addColumn: "カラムを追加" + newNoteNotificationSettings: "新着ノート通知の設定" configureColumn: "カラムの設定" swapLeft: "左に移動" swapRight: "右に移動" diff --git a/packages/backend/migration/1716450883149-RemoveAntennaNotify.js b/packages/backend/migration/1716450883149-RemoveAntennaNotify.js new file mode 100644 index 0000000000..b5a2441855 --- /dev/null +++ b/packages/backend/migration/1716450883149-RemoveAntennaNotify.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class RemoveAntennaNotify1716450883149 { + name = 'RemoveAntennaNotify1716450883149' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "notify"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "antenna" ADD "notify" boolean NOT NULL`); + } +} diff --git a/packages/backend/src/core/entities/AntennaEntityService.ts b/packages/backend/src/core/entities/AntennaEntityService.ts index 3ec8efa6bf..4a17a3d80f 100644 --- a/packages/backend/src/core/entities/AntennaEntityService.ts +++ b/packages/backend/src/core/entities/AntennaEntityService.ts @@ -38,7 +38,6 @@ export class AntennaEntityService { users: antenna.users, caseSensitive: antenna.caseSensitive, localOnly: antenna.localOnly, - notify: antenna.notify, excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, diff --git a/packages/backend/src/models/Antenna.ts b/packages/backend/src/models/Antenna.ts index f5e819059e..33e6f48189 100644 --- a/packages/backend/src/models/Antenna.ts +++ b/packages/backend/src/models/Antenna.ts @@ -90,9 +90,6 @@ export class MiAntenna { }) public expression: string | null; - @Column('boolean') - public notify: boolean; - @Index() @Column('boolean', { default: true, diff --git a/packages/backend/src/models/json-schema/antenna.ts b/packages/backend/src/models/json-schema/antenna.ts index 78cf6d3ba2..c4ac358fa6 100644 --- a/packages/backend/src/models/json-schema/antenna.ts +++ b/packages/backend/src/models/json-schema/antenna.ts @@ -72,10 +72,6 @@ export const packedAntennaSchema = { optional: false, nullable: false, default: false, }, - notify: { - type: 'boolean', - optional: false, nullable: false, - }, excludeBots: { type: 'boolean', optional: false, nullable: false, diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts index 1d8e90f367..88c4ea29c0 100644 --- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -84,7 +84,6 @@ export class ExportAntennasProcessorService { excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, - notify: antenna.notify, })); if (antennas.length - 1 !== index) { write(', '); diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts index ff1c04de06..e5b7c5ac52 100644 --- a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -47,9 +47,8 @@ const validate = new Ajv().compile({ excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, - notify: { type: 'boolean' }, }, - required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], + required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'], }); @Injectable() @@ -92,7 +91,6 @@ export class ImportAntennasProcessorService { excludeBots: antenna.excludeBots, withReplies: antenna.withReplies, withFile: antenna.withFile, - notify: antenna.notify, }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); this.logger.succ('Antenna created: ' + result.id); this.globalEventService.publishInternalEvent('antennaCreated', result); diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts index 57c8eb4958..6b7bacb054 100644 --- a/packages/backend/src/server/api/endpoints/antennas/create.ts +++ b/packages/backend/src/server/api/endpoints/antennas/create.ts @@ -67,9 +67,8 @@ export const paramDef = { excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, - notify: { type: 'boolean' }, }, - required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], + required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'], } as const; @Injectable() @@ -128,7 +127,6 @@ export default class extends Endpoint { // eslint- excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, - notify: ps.notify, }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); this.globalEventService.publishInternalEvent('antennaCreated', antenna); diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts index e6720aacf8..0c30bca9e0 100644 --- a/packages/backend/src/server/api/endpoints/antennas/update.ts +++ b/packages/backend/src/server/api/endpoints/antennas/update.ts @@ -66,7 +66,6 @@ export const paramDef = { excludeBots: { type: 'boolean' }, withReplies: { type: 'boolean' }, withFile: { type: 'boolean' }, - notify: { type: 'boolean' }, }, required: ['antennaId'], } as const; @@ -124,7 +123,6 @@ export default class extends Endpoint { // eslint- excludeBots: ps.excludeBots, withReplies: ps.withReplies, withFile: ps.withFile, - notify: ps.notify, isActive: true, lastUsedAt: new Date(), }); diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index cf5c7dd130..4f78cc999d 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -38,7 +38,6 @@ describe('アンテナ', () => { excludeKeywords: [['']], keywords: [['keyword']], name: 'test', - notify: false, src: 'all' as const, userListId: null, users: [''], @@ -151,7 +150,6 @@ describe('アンテナ', () => { isActive: true, keywords: [['keyword']], name: 'test', - notify: false, src: 'all', userListId: null, users: [''], @@ -219,8 +217,6 @@ describe('アンテナ', () => { { parameters: () => ({ withReplies: true }) }, { parameters: () => ({ withFile: false }) }, { parameters: () => ({ withFile: true }) }, - { parameters: () => ({ notify: false }) }, - { parameters: () => ({ notify: true }) }, ]; test.each(antennaParamPattern)('を作成できること($#)', async ({ parameters }) => { const response = await successfulApiCall({ diff --git a/packages/backend/test/e2e/move.ts b/packages/backend/test/e2e/move.ts index 4e5306da97..35050130dc 100644 --- a/packages/backend/test/e2e/move.ts +++ b/packages/backend/test/e2e/move.ts @@ -191,7 +191,6 @@ describe('Account Move', () => { localOnly: false, withReplies: false, withFile: false, - notify: false, }, alice); antennaId = antenna.body.id; @@ -435,7 +434,6 @@ describe('Account Move', () => { localOnly: false, withReplies: false, withFile: false, - notify: false, }, alice); assert.strictEqual(res.status, 403); diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue new file mode 100644 index 0000000000..9360594236 --- /dev/null +++ b/packages/frontend/src/components/MkFormDialog.file.vue @@ -0,0 +1,71 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index deedc5badb..124f114111 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
- - + diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 70b55e8172..bb3c04cd5c 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -9,6 +9,7 @@ import { notificationTypes } from 'misskey-js'; import { Storage } from '@/pizzax.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { deepClone } from '@/scripts/clone.js'; +import { SoundStore } from '@/store.js'; type ColumnWidget = { name: string; @@ -33,6 +34,7 @@ export type Column = { withRenotes?: boolean; withReplies?: boolean; onlyFiles?: boolean; + soundSetting: SoundStore; }; export const deckStore = markRaw(new Storage('deck', { diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue index 70ea54326f..5369112494 100644 --- a/packages/frontend/src/ui/deck/list-column.vue +++ b/packages/frontend/src/ui/deck/list-column.vue @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ column.name }} - + @@ -21,6 +21,10 @@ import MkTimeline from '@/components/MkTimeline.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import { MenuItem } from '@/types/menu.js'; +import { SoundStore } from '@/store.js'; +import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js'; +import * as sound from '@/scripts/sound.js'; const props = defineProps<{ column: Column; @@ -29,6 +33,7 @@ const props = defineProps<{ const timeline = shallowRef>(); const withRenotes = ref(props.column.withRenotes ?? true); +const soundSetting = ref(props.column.soundSetting ?? { type: null, volume: 1 }); if (props.column.listId == null) { setList(); @@ -40,6 +45,10 @@ watch(withRenotes, v => { }); }); +watch(soundSetting, v => { + updateColumn(props.column.id, { soundSetting: v }); +}); + async function setList() { const lists = await misskeyApi('users/lists/list'); const { canceled, result: list } = await os.select({ @@ -59,7 +68,11 @@ function editList() { os.pageWindow('my/lists/' + props.column.listId); } -const menu = [ +function onNote() { + sound.playMisskeySfxFile(soundSetting.value); +} + +const menu: MenuItem[] = [ { icon: 'ti ti-pencil', text: i18n.ts.selectList, @@ -75,5 +88,10 @@ const menu = [ text: i18n.ts.showRenotes, ref: withRenotes, }, + { + icon: 'ti ti-bell', + text: i18n.ts._deck.newNoteNotificationSettings, + action: () => soundSettingsButton(soundSetting), + }, ]; diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue index eae2ee13f3..32ab7527b4 100644 --- a/packages/frontend/src/ui/deck/role-timeline-column.vue +++ b/packages/frontend/src/ui/deck/role-timeline-column.vue @@ -9,18 +9,22 @@ SPDX-License-Identifier: AGPL-3.0-only {{ column.name }} - +