Skip to content

Commit

Permalink
feat: 指定したユーザーの投稿通知
Browse files Browse the repository at this point in the history
Resolve #11499
  • Loading branch information
syuilo committed Sep 21, 2023
1 parent f195fa4 commit e3f151e
Show file tree
Hide file tree
Showing 25 changed files with 238 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
- Feat: 二要素認証のバックアップコードが生成されるようになりました
- ref. https://github.com/MisskeyIO/misskey/pull/121
- Feat: 二要素認証でパスキーをサポートするようになりました
- Feat: 指定したユーザーが投稿したときに通知できるようになりました
- Feat: プロフィールでのリンク検証
- Feat: 通知をテストできるようになりました
- Feat: PWAのアイコンが設定できるようになりました
Expand Down
3 changes: 3 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,8 @@ export interface Locale {
"pinnedList": string;
"keepScreenOn": string;
"verifiedLink": string;
"notifyNotes": string;
"unnotifyNotes": string;
"_announcement": {
"forExistingUsers": string;
"forExistingUsersDescription": string;
Expand Down Expand Up @@ -2150,6 +2152,7 @@ export interface Locale {
"youReceivedFollowRequest": string;
"yourFollowRequestAccepted": string;
"pollEnded": string;
"newNote": string;
"unreadAntennaNote": string;
"emptyPushNotificationMessage": string;
"achievementEarned": string;
Expand Down
3 changes: 3 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1114,6 +1114,8 @@ loadConversation: "会話を見る"
pinnedList: "ピン留めされたリスト"
keepScreenOn: "デバイスの画面を常にオンにする"
verifiedLink: "このリンク先の所有者であることが確認されました"
notifyNotes: "投稿を通知"
unnotifyNotes: "投稿の通知を解除"

_announcement:
forExistingUsers: "既存ユーザーのみ"
Expand Down Expand Up @@ -2064,6 +2066,7 @@ _notification:
youReceivedFollowRequest: "フォローリクエストが来ました"
yourFollowRequestAccepted: "フォローリクエストが承認されました"
pollEnded: "アンケートの結果が出ました"
newNote: "新しい投稿"
unreadAntennaNote: "アンテナ {name}"
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得"
Expand Down
13 changes: 13 additions & 0 deletions packages/backend/migration/1695288787870-following-notify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class FollowingNotify1695288787870 {
name = 'FollowingNotify1695288787870'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "following" ADD "notify" character varying(32)`);
await queryRunner.query(`CREATE INDEX "IDX_5108098457488634a4768e1d12" ON "following" ("notify") `);
}

async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_5108098457488634a4768e1d12"`);
await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "notify"`);
}
}
19 changes: 18 additions & 1 deletion packages/backend/src/core/NoteCreateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 7 additions & 7 deletions packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>>;

Expand Down
7 changes: 7 additions & 0 deletions packages/backend/src/models/Following.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
12 changes: 0 additions & 12 deletions packages/backend/src/models/Notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/models/json-schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string',
nullable: false, optional: true,
},
notify: {
type: 'string',
nullable: false, optional: true,
},
//#endregion
},
} as const;
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/server/api/EndpointsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/server/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const meta = {

limit: {
duration: ms('1hour'),
max: 50,
max: 100,
},

requireCredential: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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',
},
},

Expand Down
107 changes: 107 additions & 0 deletions packages/backend/src/server/api/endpoints/following/update.ts
Original file line number Diff line number Diff line change
@@ -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);
});
}
}
17 changes: 16 additions & 1 deletion packages/backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/test/e2e/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ describe('ユーザー', () => {
{ parameters: (): object => ({ mutedWords: [] }) },
{ parameters: (): object => ({ mutedInstances: ['xxxx.xxxxx'] }) },
{ parameters: (): object => ({ mutedInstances: [] }) },
{ parameters: (): object => ({ mutingNotificationTypes: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
{ parameters: (): object => ({ mutingNotificationTypes: ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app'] }) },
{ parameters: (): object => ({ mutingNotificationTypes: [] }) },
{ parameters: (): object => ({ emailNotificationTypes: ['mention', 'reply', 'quote', 'follow', 'receiveFollowRequest'] }) },
{ parameters: (): object => ({ emailNotificationTypes: [] }) },
Expand Down
Loading

0 comments on commit e3f151e

Please sign in to comment.