diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-09-21 18:48:15 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2023-09-21 18:48:15 +0900 |
| commit | e3f151e2307e4c0d7b9cdfc7deba2ff028adce03 (patch) | |
| tree | 15ef05643d8fc99a724fa97f8af8f55530af1379 /packages/backend/src | |
| parent | 2023.9.0-beta.10 (diff) | |
| download | sharkey-e3f151e2307e4c0d7b9cdfc7deba2ff028adce03.tar.gz sharkey-e3f151e2307e4c0d7b9cdfc7deba2ff028adce03.tar.bz2 sharkey-e3f151e2307e4c0d7b9cdfc7deba2ff028adce03.zip | |
feat: 指定したユーザーの投稿通知
Resolve #11499
Diffstat (limited to 'packages/backend/src')
12 files changed, 169 insertions, 25 deletions
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 4b0d818862..972319ddcf 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { ChannelsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { ChannelsRepository, FollowingsRepository, InstancesRepository, MutedNotesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -185,6 +185,9 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.noteThreadMutingsRepository) private noteThreadMutingsRepository: NoteThreadMutingsRepository, + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, @@ -505,6 +508,20 @@ export class NoteCreateService implements OnApplicationShutdown { this.saveReply(data.reply, note); } + if (data.reply == null) { + this.followingsRepository.findBy({ + followeeId: user.id, + notify: 'normal', + }).then(followings => { + for (const following of followings) { + this.notificationService.createNotification(following.followerId, 'note', { + notifierId: user.id, + noteId: note.id, + }); + } + }); + } + // この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき if (data.renote && (await this.noteEntityService.countSameRenotes(user.id, data.renote.id, note.id) === 0)) { if (!user.isBot) this.incRenoteCount(data.renote); diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts index 75a2f62f6e..3ee7c91f3a 100644 --- a/packages/backend/src/core/entities/NotificationEntityService.ts +++ b/packages/backend/src/core/entities/NotificationEntityService.ts @@ -20,7 +20,7 @@ import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js'; -const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); +const NOTE_REQUIRED_NOTIFICATION_TYPES = new Set(['note', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded'] as (typeof notificationTypes[number])[]); @Injectable() export class NotificationEntityService implements OnModuleInit { diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 7bef410bf9..3dd64ce625 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -146,15 +146,14 @@ export class UserEntityService implements OnModuleInit { @bindThis public async getRelation(me: MiUser['id'], target: MiUser['id']) { + const following = await this.followingsRepository.findOneBy({ + followerId: me, + followeeId: target, + }); return awaitAll({ id: target, - isFollowing: this.followingsRepository.count({ - where: { - followerId: me, - followeeId: target, - }, - take: 1, - }).then(n => n > 0), + following, + isFollowing: following != null, isFollowed: this.followingsRepository.count({ where: { followerId: target, @@ -486,6 +485,7 @@ export class UserEntityService implements OnModuleInit { isBlocked: relation.isBlocked, isMuted: relation.isMuted, isRenoteMuted: relation.isRenoteMuted, + notify: relation.following?.notify ?? 'none', } : {}), } as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>; diff --git a/packages/backend/src/models/Following.ts b/packages/backend/src/models/Following.ts index 05b729412f..8c9f965fad 100644 --- a/packages/backend/src/models/Following.ts +++ b/packages/backend/src/models/Following.ts @@ -45,6 +45,13 @@ export class MiFollowing { @JoinColumn() public follower: MiUser | null; + @Index() + @Column('varchar', { + length: 32, + nullable: true, + }) + public notify: 'normal' | null; + //#region Denormalized fields @Index() @Column('varchar', { diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index fb7f67dfd8..c0a9df2e23 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -22,18 +22,6 @@ export type MiNotification = { /** * 通知の種類。 - * follow - フォローされた - * mention - 投稿で自分が言及された - * reply - 投稿に返信された - * renote - 投稿がRenoteされた - * quote - 投稿が引用Renoteされた - * reaction - 投稿にリアクションされた - * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した - * receiveFollowRequest - フォローリクエストされた - * followRequestAccepted - 自分の送ったフォローリクエストが承認された - * achievementEarned - 実績を獲得 - * app - アプリ通知 - * test - テスト通知(サーバー側) */ type: typeof notificationTypes[number]; diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 8d0e4e72ed..f15b225a30 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -273,6 +273,10 @@ export const packedUserDetailedNotMeOnlySchema = { type: 'string', nullable: false, optional: true, }, + notify: { + type: 'string', + nullable: false, optional: true, + }, //#endregion }, } as const; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 7b9fa6c3b0..41a11bfb19 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -160,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js'; import * as ep___federation_stats from './endpoints/federation/stats.js'; import * as ep___following_create from './endpoints/following/create.js'; import * as ep___following_delete from './endpoints/following/delete.js'; +import * as ep___following_update from './endpoints/following/update.js'; import * as ep___following_invalidate from './endpoints/following/invalidate.js'; import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; @@ -507,6 +508,7 @@ const $federation_users: Provider = { provide: 'ep:federation/users', useClass: const $federation_stats: Provider = { provide: 'ep:federation/stats', useClass: ep___federation_stats.default }; const $following_create: Provider = { provide: 'ep:following/create', useClass: ep___following_create.default }; const $following_delete: Provider = { provide: 'ep:following/delete', useClass: ep___following_delete.default }; +const $following_update: Provider = { provide: 'ep:following/update', useClass: ep___following_update.default }; const $following_invalidate: Provider = { provide: 'ep:following/invalidate', useClass: ep___following_invalidate.default }; const $following_requests_accept: Provider = { provide: 'ep:following/requests/accept', useClass: ep___following_requests_accept.default }; const $following_requests_cancel: Provider = { provide: 'ep:following/requests/cancel', useClass: ep___following_requests_cancel.default }; @@ -858,6 +860,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $federation_stats, $following_create, $following_delete, + $following_update, $following_invalidate, $following_requests_accept, $following_requests_cancel, @@ -1203,6 +1206,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $federation_stats, $following_create, $following_delete, + $following_update, $following_invalidate, $following_requests_accept, $following_requests_cancel, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index a9cb7c341a..ab20a708ef 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -160,6 +160,7 @@ import * as ep___federation_users from './endpoints/federation/users.js'; import * as ep___federation_stats from './endpoints/federation/stats.js'; import * as ep___following_create from './endpoints/following/create.js'; import * as ep___following_delete from './endpoints/following/delete.js'; +import * as ep___following_update from './endpoints/following/update.js'; import * as ep___following_invalidate from './endpoints/following/invalidate.js'; import * as ep___following_requests_accept from './endpoints/following/requests/accept.js'; import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js'; @@ -505,6 +506,7 @@ const eps = [ ['federation/stats', ep___federation_stats], ['following/create', ep___following_create], ['following/delete', ep___following_delete], + ['following/update', ep___following_update], ['following/invalidate', ep___following_invalidate], ['following/requests/accept', ep___following_requests_accept], ['following/requests/cancel', ep___following_requests_cancel], diff --git a/packages/backend/src/server/api/endpoints/following/create.ts b/packages/backend/src/server/api/endpoints/following/create.ts index d80c853672..e0e7fed87a 100644 --- a/packages/backend/src/server/api/endpoints/following/create.ts +++ b/packages/backend/src/server/api/endpoints/following/create.ts @@ -19,7 +19,7 @@ export const meta = { limit: { duration: ms('1hour'), - max: 50, + max: 100, }, requireCredential: true, diff --git a/packages/backend/src/server/api/endpoints/following/invalidate.ts b/packages/backend/src/server/api/endpoints/following/invalidate.ts index 147d16556f..53ef925b2f 100644 --- a/packages/backend/src/server/api/endpoints/following/invalidate.ts +++ b/packages/backend/src/server/api/endpoints/following/invalidate.ts @@ -29,7 +29,7 @@ export const meta = { noSuchUser: { message: 'No such user.', code: 'NO_SUCH_USER', - id: '5b12c78d-2b28-4dca-99d2-f56139b42ff8', + id: 'b77e6ae6-a3e5-40da-9cc8-c240115479cc', }, followerIsYourself: { @@ -41,7 +41,7 @@ export const meta = { notFollowing: { message: 'The other use is not following you.', code: 'NOT_FOLLOWING', - id: '5dbf82f5-c92b-40b1-87d1-6c8c0741fd09', + id: '918faac3-074f-41ae-9c43-ed5d2946770d', }, }, diff --git a/packages/backend/src/server/api/endpoints/following/update.ts b/packages/backend/src/server/api/endpoints/following/update.ts new file mode 100644 index 0000000000..25f393e517 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/following/update.ts @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ms from 'ms'; +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { FollowingsRepository } from '@/models/_.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { UserFollowingService } from '@/core/UserFollowingService.js'; +import { DI } from '@/di-symbols.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['following', 'users'], + + limit: { + duration: ms('1hour'), + max: 100, + }, + + requireCredential: true, + + kind: 'write:following', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '14318698-f67e-492a-99da-5353a5ac52be', + }, + + followeeIsYourself: { + message: 'Followee is yourself.', + code: 'FOLLOWEE_IS_YOURSELF', + id: '4c4cbaf9-962a-463b-8418-a5e365dbf2eb', + }, + + notFollowing: { + message: 'You are not following that user.', + code: 'NOT_FOLLOWING', + id: 'b8dc75cf-1cb5-46c9-b14b-5f1ffbd782c9', + }, + }, + + res: { + type: 'object', + optional: false, nullable: false, + ref: 'UserLite', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + notify: { type: 'string', enum: ['normal', 'none'] }, + }, + required: ['userId', 'notify'], +} as const; + +@Injectable() +export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.followingsRepository) + private followingsRepository: FollowingsRepository, + + private userEntityService: UserEntityService, + private getterService: GetterService, + private userFollowingService: UserFollowingService, + ) { + super(meta, paramDef, async (ps, me) => { + const follower = me; + + // Check if the follower is yourself + if (me.id === ps.userId) { + throw new ApiError(meta.errors.followeeIsYourself); + } + + // Get followee + const followee = await this.getterService.getUser(ps.userId).catch(err => { + if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw err; + }); + + // Check not following + const exist = await this.followingsRepository.findOneBy({ + followerId: follower.id, + followeeId: followee.id, + }); + + if (exist == null) { + throw new ApiError(meta.errors.notFollowing); + } + + await this.followingsRepository.update({ + id: exist.id, + }, { + notify: ps.notify === 'none' ? null : ps.notify, + }); + + return await this.userEntityService.pack(follower.id, me); + }); + } +} diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 024ba01e3b..0a28d88d08 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -3,7 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export const notificationTypes = ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const; +/** + * note - 通知オンにしているユーザーが投稿した + * follow - フォローされた + * mention - 投稿で自分が言及された + * reply - 投稿に返信された + * renote - 投稿がRenoteされた + * quote - 投稿が引用Renoteされた + * reaction - 投稿にリアクションされた + * pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した + * receiveFollowRequest - フォローリクエストされた + * followRequestAccepted - 自分の送ったフォローリクエストが承認された + * achievementEarned - 実績を獲得 + * app - アプリ通知 + * test - テスト通知(サーバー側) + */ +export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; |