Resolve #6192
This commit is contained in:
		
							parent
							
								
									9ea1ed8559
								
							
						
					
					
						commit
						614a1d74dd
					
				
					 21 changed files with 229 additions and 91 deletions
				
			
		
							
								
								
									
										48
									
								
								migration/1585385921215-custom-notification.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								migration/1585385921215-custom-notification.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | ||||||
|  | import {MigrationInterface, QueryRunner} from "typeorm"; | ||||||
|  | 
 | ||||||
|  | export class customNotification1585385921215 implements MigrationInterface { | ||||||
|  |     name = 'customNotification1585385921215' | ||||||
|  | 
 | ||||||
|  |     public async up(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ADD "customBody" character varying(2048)`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ADD "customHeader" character varying(256)`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ADD "customIcon" character varying(1024)`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ADD "appAccessTokenId" character varying(32)`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "notifierId" DROP NOT NULL`, undefined); | ||||||
|  |         await queryRunner.query(`COMMENT ON COLUMN "notification"."notifierId" IS 'The ID of sender user of the Notification.'`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TYPE "public"."notification_type_enum" RENAME TO "notification_type_enum_old"`, undefined); | ||||||
|  |         await queryRunner.query(`CREATE TYPE "notification_type_enum" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum" USING "type"::"text"::"notification_type_enum"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP TYPE "notification_type_enum_old"`, undefined); | ||||||
|  |         await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS 'The type of the Notification.'`, undefined); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71" ON "notification" ("notifierId") `, undefined); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_33f33cc8ef29d805a97ff4628b" ON "notification" ("type") `, undefined); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_080ab397c379af09b9d2169e5b" ON "notification" ("isRead") `, undefined); | ||||||
|  |         await queryRunner.query(`CREATE INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c" ON "notification" ("appAccessTokenId") `, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9" FOREIGN KEY ("appAccessTokenId") REFERENCES "access_token"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public async down(queryRunner: QueryRunner): Promise<any> { | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_e22bf6bda77b6adc1fd9e75c8c9"`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" DROP CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_e22bf6bda77b6adc1fd9e75c8c"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_080ab397c379af09b9d2169e5b"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_33f33cc8ef29d805a97ff4628b"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP INDEX "IDX_3b4e96eec8d36a8bbb9d02aa71"`, undefined); | ||||||
|  |         await queryRunner.query(`COMMENT ON COLUMN "notification"."type" IS ''`, undefined); | ||||||
|  |         await queryRunner.query(`CREATE TYPE "notification_type_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited')`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "type" TYPE "notification_type_enum_old" USING "type"::"text"::"notification_type_enum_old"`, undefined); | ||||||
|  |         await queryRunner.query(`DROP TYPE "notification_type_enum"`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TYPE "notification_type_enum_old" RENAME TO  "notification_type_enum"`, undefined); | ||||||
|  |         await queryRunner.query(`COMMENT ON COLUMN "notification"."notifierId" IS ''`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ALTER COLUMN "notifierId" SET NOT NULL`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" ADD CONSTRAINT "FK_3b4e96eec8d36a8bbb9d02aa710" FOREIGN KEY ("notifierId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "appAccessTokenId"`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customIcon"`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customHeader"`, undefined); | ||||||
|  |         await queryRunner.query(`ALTER TABLE "notification" DROP COLUMN "customBody"`, undefined); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -1,22 +1,24 @@ | ||||||
| <template> | <template> | ||||||
| <div class="mk-notification" :class="notification.type" v-size="[{ max: 500 }, { max: 600 }]"> | <div class="mk-notification" :class="notification.type" v-size="[{ max: 500 }, { max: 600 }]"> | ||||||
| 	<div class="head"> | 	<div class="head"> | ||||||
| 		<mk-avatar class="avatar" :user="notification.user"/> | 		<mk-avatar v-if="notification.user" class="icon" :user="notification.user"/> | ||||||
| 		<div class="icon" :class="notification.type"> | 		<img v-else class="icon" :src="notification.icon" alt=""/> | ||||||
|  | 		<div class="sub-icon" :class="notification.type"> | ||||||
| 			<fa :icon="faPlus" v-if="notification.type === 'follow'"/> | 			<fa :icon="faPlus" v-if="notification.type === 'follow'"/> | ||||||
| 			<fa :icon="faClock" v-if="notification.type === 'receiveFollowRequest'"/> | 			<fa :icon="faClock" v-else-if="notification.type === 'receiveFollowRequest'"/> | ||||||
| 			<fa :icon="faCheck" v-if="notification.type === 'followRequestAccepted'"/> | 			<fa :icon="faCheck" v-else-if="notification.type === 'followRequestAccepted'"/> | ||||||
| 			<fa :icon="faIdCardAlt" v-if="notification.type === 'groupInvited'"/> | 			<fa :icon="faIdCardAlt" v-else-if="notification.type === 'groupInvited'"/> | ||||||
| 			<fa :icon="faRetweet" v-if="notification.type === 'renote'"/> | 			<fa :icon="faRetweet" v-else-if="notification.type === 'renote'"/> | ||||||
| 			<fa :icon="faReply" v-if="notification.type === 'reply'"/> | 			<fa :icon="faReply" v-else-if="notification.type === 'reply'"/> | ||||||
| 			<fa :icon="faAt" v-if="notification.type === 'mention'"/> | 			<fa :icon="faAt" v-else-if="notification.type === 'mention'"/> | ||||||
| 			<fa :icon="faQuoteLeft" v-if="notification.type === 'quote'"/> | 			<fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/> | ||||||
| 			<x-reaction-icon v-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/> | 			<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/> | ||||||
| 		</div> | 		</div> | ||||||
| 	</div> | 	</div> | ||||||
| 	<div class="tail"> | 	<div class="tail"> | ||||||
| 		<header> | 		<header> | ||||||
| 			<router-link class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link> | 			<router-link v-if="notification.user" class="name" :to="notification.user | userPage" v-user-preview="notification.user.id"><mk-user-name :user="notification.user"/></router-link> | ||||||
|  | 			<span v-else>{{ notification.header }}</span> | ||||||
| 			<mk-time :time="notification.createdAt" v-if="withTime"/> | 			<mk-time :time="notification.createdAt" v-if="withTime"/> | ||||||
| 		</header> | 		</header> | ||||||
| 		<router-link v-if="notification.type === 'reaction'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> | 		<router-link v-if="notification.type === 'reaction'" class="text" :to="notification.note | notePage" :title="getNoteSummary(notification.note)"> | ||||||
|  | @ -42,6 +44,9 @@ | ||||||
| 		<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span> | 		<span v-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ $t('followRequestAccepted') }}</span> | ||||||
| 		<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span> | 		<span v-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ $t('receiveFollowRequest') }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ $t('reject') }}</button></div></span> | ||||||
| 		<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $t('groupInvited') }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $t('reject') }}</button></div></span> | 		<span v-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ $t('groupInvited') }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ $t('accept') }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ $t('reject') }}</button></div></span> | ||||||
|  | 		<span v-if="notification.type === 'app'" class="text"> | ||||||
|  | 			<mfm :text="notification.body" :nowrap="!full"/> | ||||||
|  | 		</span> | ||||||
| 	</div> | 	</div> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  | @ -142,14 +147,14 @@ export default Vue.extend({ | ||||||
| 		height: 42px; | 		height: 42px; | ||||||
| 		margin-right: 8px; | 		margin-right: 8px; | ||||||
| 
 | 
 | ||||||
| 		> .avatar { | 		> .icon { | ||||||
| 			display: block; | 			display: block; | ||||||
| 			width: 100%; | 			width: 100%; | ||||||
| 			height: 100%; | 			height: 100%; | ||||||
| 			border-radius: 6px; | 			border-radius: 6px; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		> .icon { | 		> .sub-icon { | ||||||
| 			position: absolute; | 			position: absolute; | ||||||
| 			z-index: 1; | 			z-index: 1; | ||||||
| 			bottom: -2px; | 			bottom: -2px; | ||||||
|  | @ -163,6 +168,10 @@ export default Vue.extend({ | ||||||
| 			font-size: 12px; | 			font-size: 12px; | ||||||
| 			pointer-events: none; | 			pointer-events: none; | ||||||
| 
 | 
 | ||||||
|  | 			&:empty { | ||||||
|  | 				display: none; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
| 			> * { | 			> * { | ||||||
| 				color: #fff; | 				color: #fff; | ||||||
| 				width: 100%; | 				width: 100%; | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import { id } from '../id'; | ||||||
| import { Note } from './note'; | import { Note } from './note'; | ||||||
| import { FollowRequest } from './follow-request'; | import { FollowRequest } from './follow-request'; | ||||||
| import { UserGroupInvitation } from './user-group-invitation'; | import { UserGroupInvitation } from './user-group-invitation'; | ||||||
|  | import { AccessToken } from './access-token'; | ||||||
| 
 | 
 | ||||||
| @Entity() | @Entity() | ||||||
| export class Notification { | export class Notification { | ||||||
|  | @ -35,11 +36,13 @@ export class Notification { | ||||||
| 	/** | 	/** | ||||||
| 	 * 通知の送信者(initiator) | 	 * 通知の送信者(initiator) | ||||||
| 	 */ | 	 */ | ||||||
|  | 	@Index() | ||||||
| 	@Column({ | 	@Column({ | ||||||
| 		...id(), | 		...id(), | ||||||
|  | 		nullable: true, | ||||||
| 		comment: 'The ID of sender user of the Notification.' | 		comment: 'The ID of sender user of the Notification.' | ||||||
| 	}) | 	}) | ||||||
| 	public notifierId: User['id']; | 	public notifierId: User['id'] | null; | ||||||
| 
 | 
 | ||||||
| 	@ManyToOne(type => User, { | 	@ManyToOne(type => User, { | ||||||
| 		onDelete: 'CASCADE' | 		onDelete: 'CASCADE' | ||||||
|  | @ -59,16 +62,19 @@ export class Notification { | ||||||
| 	 * receiveFollowRequest - フォローリクエストされた | 	 * receiveFollowRequest - フォローリクエストされた | ||||||
| 	 * followRequestAccepted - 自分の送ったフォローリクエストが承認された | 	 * followRequestAccepted - 自分の送ったフォローリクエストが承認された | ||||||
| 	 * groupInvited - グループに招待された | 	 * groupInvited - グループに招待された | ||||||
|  | 	 * app - アプリ通知 | ||||||
| 	 */ | 	 */ | ||||||
|  | 	@Index() | ||||||
| 	@Column('enum', { | 	@Column('enum', { | ||||||
| 		enum: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited'], | 		enum: ['follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app'], | ||||||
| 		comment: 'The type of the Notification.' | 		comment: 'The type of the Notification.' | ||||||
| 	}) | 	}) | ||||||
| 	public type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollVote' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited'; | 	public type: 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollVote' | 'receiveFollowRequest' | 'followRequestAccepted' | 'groupInvited' | 'app'; | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * 通知が読まれたかどうか | 	 * 通知が読まれたかどうか | ||||||
| 	 */ | 	 */ | ||||||
|  | 	@Index() | ||||||
| 	@Column('boolean', { | 	@Column('boolean', { | ||||||
| 		default: false, | 		default: false, | ||||||
| 		comment: 'Whether the Notification is read.' | 		comment: 'Whether the Notification is read.' | ||||||
|  | @ -114,10 +120,52 @@ export class Notification { | ||||||
| 	@Column('varchar', { | 	@Column('varchar', { | ||||||
| 		length: 128, nullable: true | 		length: 128, nullable: true | ||||||
| 	}) | 	}) | ||||||
| 	public reaction: string; | 	public reaction: string | null; | ||||||
| 
 | 
 | ||||||
| 	@Column('integer', { | 	@Column('integer', { | ||||||
| 		nullable: true | 		nullable: true | ||||||
| 	}) | 	}) | ||||||
| 	public choice: number; | 	public choice: number | null; | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * アプリ通知のbody | ||||||
|  | 	 */ | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 2048, nullable: true | ||||||
|  | 	}) | ||||||
|  | 	public customBody: string | null; | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * アプリ通知のheader | ||||||
|  | 	 * (省略時はアプリ名で表示されることを期待) | ||||||
|  | 	 */ | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 256, nullable: true | ||||||
|  | 	}) | ||||||
|  | 	public customHeader: string | null; | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * アプリ通知のicon(URL) | ||||||
|  | 	 * (省略時はアプリアイコンで表示されることを期待) | ||||||
|  | 	 */ | ||||||
|  | 	@Column('varchar', { | ||||||
|  | 		length: 1024, nullable: true | ||||||
|  | 	}) | ||||||
|  | 	public customIcon: string | null; | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * アプリ通知のアプリ(のトークン) | ||||||
|  | 	 */ | ||||||
|  | 	@Index() | ||||||
|  | 	@Column({ | ||||||
|  | 		...id(), | ||||||
|  | 		nullable: true | ||||||
|  | 	}) | ||||||
|  | 	public appAccessTokenId: AccessToken['id'] | null; | ||||||
|  | 
 | ||||||
|  | 	@ManyToOne(type => AccessToken, { | ||||||
|  | 		onDelete: 'CASCADE' | ||||||
|  | 	}) | ||||||
|  | 	@JoinColumn() | ||||||
|  | 	public appAccessToken: AccessToken | null; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { EntityRepository, Repository } from 'typeorm'; | import { EntityRepository, Repository } from 'typeorm'; | ||||||
| import { Users, Notes, UserGroupInvitations } from '..'; | import { Users, Notes, UserGroupInvitations, AccessTokens } from '..'; | ||||||
| import { Notification } from '../entities/notification'; | import { Notification } from '../entities/notification'; | ||||||
| import { ensure } from '../../prelude/ensure'; | import { ensure } from '../../prelude/ensure'; | ||||||
| import { awaitAll } from '../../prelude/await-all'; | import { awaitAll } from '../../prelude/await-all'; | ||||||
|  | @ -13,13 +13,14 @@ export class NotificationRepository extends Repository<Notification> { | ||||||
| 		src: Notification['id'] | Notification, | 		src: Notification['id'] | Notification, | ||||||
| 	): Promise<PackedNotification> { | 	): Promise<PackedNotification> { | ||||||
| 		const notification = typeof src === 'object' ? src : await this.findOne(src).then(ensure); | 		const notification = typeof src === 'object' ? src : await this.findOne(src).then(ensure); | ||||||
|  | 		const token = notification.appAccessTokenId ? await AccessTokens.findOne(notification.appAccessTokenId).then(ensure) : null; | ||||||
| 
 | 
 | ||||||
| 		return await awaitAll({ | 		return await awaitAll({ | ||||||
| 			id: notification.id, | 			id: notification.id, | ||||||
| 			createdAt: notification.createdAt.toISOString(), | 			createdAt: notification.createdAt.toISOString(), | ||||||
| 			type: notification.type, | 			type: notification.type, | ||||||
| 			userId: notification.notifierId, | 			userId: notification.notifierId, | ||||||
| 			user: Users.pack(notification.notifier || notification.notifierId), | 			user: notification.notifierId ? Users.pack(notification.notifier || notification.notifierId) : null, | ||||||
| 			...(notification.type === 'mention' ? { | 			...(notification.type === 'mention' ? { | ||||||
| 				note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId), | 				note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId), | ||||||
| 			} : {}), | 			} : {}), | ||||||
|  | @ -43,6 +44,11 @@ export class NotificationRepository extends Repository<Notification> { | ||||||
| 			...(notification.type === 'groupInvited' ? { | 			...(notification.type === 'groupInvited' ? { | ||||||
| 				invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!), | 				invitation: UserGroupInvitations.pack(notification.userGroupInvitationId!), | ||||||
| 			} : {}), | 			} : {}), | ||||||
|  | 			...(notification.type === 'app' ? { | ||||||
|  | 				body: notification.customBody, | ||||||
|  | 				header: notification.customHeader || token!.name, | ||||||
|  | 				icon: notification.customIcon || token!.iconUrl, | ||||||
|  | 			} : {}), | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,12 +2,9 @@ import isNativeToken from './common/is-native-token'; | ||||||
| import { User } from '../../models/entities/user'; | import { User } from '../../models/entities/user'; | ||||||
| import { Users, AccessTokens, Apps } from '../../models'; | import { Users, AccessTokens, Apps } from '../../models'; | ||||||
| import { ensure } from '../../prelude/ensure'; | import { ensure } from '../../prelude/ensure'; | ||||||
|  | import { AccessToken } from '../../models/entities/access-token'; | ||||||
| 
 | 
 | ||||||
| type App = { | export default async (token: string): Promise<[User | null | undefined, AccessToken | null | undefined]> => { | ||||||
| 	permission: string[]; |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| export default async (token: string): Promise<[User | null | undefined, App | null | undefined]> => { |  | ||||||
| 	if (token == null) { | 	if (token == null) { | ||||||
| 		return [null, null]; | 		return [null, null]; | ||||||
| 	} | 	} | ||||||
|  | @ -45,12 +42,11 @@ export default async (token: string): Promise<[User | null | undefined, App | nu | ||||||
| 				.findOne(accessToken.appId).then(ensure); | 				.findOne(accessToken.appId).then(ensure); | ||||||
| 
 | 
 | ||||||
| 			return [user, { | 			return [user, { | ||||||
|  | 				id: accessToken.id, | ||||||
| 				permission: app.permission | 				permission: app.permission | ||||||
| 			}]; | 			} as AccessToken]; | ||||||
| 		} else { | 		} else { | ||||||
| 			return [user, { | 			return [user, accessToken]; | ||||||
| 				permission: accessToken.permission |  | ||||||
| 			}]; |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | @ -4,10 +4,7 @@ import { User } from '../../models/entities/user'; | ||||||
| import endpoints from './endpoints'; | import endpoints from './endpoints'; | ||||||
| import { ApiError } from './error'; | import { ApiError } from './error'; | ||||||
| import { apiLogger } from './logger'; | import { apiLogger } from './logger'; | ||||||
| 
 | import { AccessToken } from '../../models/entities/access-token'; | ||||||
| type App = { |  | ||||||
| 	permission: string[]; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| const accessDenied = { | const accessDenied = { | ||||||
| 	message: 'Access denied.', | 	message: 'Access denied.', | ||||||
|  | @ -15,8 +12,8 @@ const accessDenied = { | ||||||
| 	id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e' | 	id: '56f35758-7dd5-468b-8439-5d6fb8ec9b8e' | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default async (endpoint: string, user: User | null | undefined, app: App | null | undefined, data: any, file?: any) => { | export default async (endpoint: string, user: User | null | undefined, token: AccessToken | null | undefined, data: any, file?: any) => { | ||||||
| 	const isSecure = user != null && app == null; | 	const isSecure = user != null && token == null; | ||||||
| 
 | 
 | ||||||
| 	const ep = endpoints.find(e => e.name === endpoint); | 	const ep = endpoints.find(e => e.name === endpoint); | ||||||
| 
 | 
 | ||||||
|  | @ -54,7 +51,7 @@ export default async (endpoint: string, user: User | null | undefined, app: App | ||||||
| 		throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); | 		throw new ApiError(accessDenied, { reason: 'You are not a moderator.' }); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (app && ep.meta.kind && !app.permission.some(p => p === ep.meta.kind)) { | 	if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) { | ||||||
| 		throw new ApiError({ | 		throw new ApiError({ | ||||||
| 			message: 'Your app does not have the necessary permissions to use this endpoint.', | 			message: 'Your app does not have the necessary permissions to use this endpoint.', | ||||||
| 			code: 'PERMISSION_DENIED', | 			code: 'PERMISSION_DENIED', | ||||||
|  | @ -76,7 +73,7 @@ export default async (endpoint: string, user: User | null | undefined, app: App | ||||||
| 
 | 
 | ||||||
| 	// API invoking
 | 	// API invoking
 | ||||||
| 	const before = performance.now(); | 	const before = performance.now(); | ||||||
| 	return await ep.exec(data, user, isSecure, file).catch((e: Error) => { | 	return await ep.exec(data, user, token, file).catch((e: Error) => { | ||||||
| 		if (e instanceof ApiError) { | 		if (e instanceof ApiError) { | ||||||
| 			throw e; | 			throw e; | ||||||
| 		} else { | 		} else { | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ import { ILocalUser } from '../../models/entities/user'; | ||||||
| import { IEndpointMeta } from './endpoints'; | import { IEndpointMeta } from './endpoints'; | ||||||
| import { ApiError } from './error'; | import { ApiError } from './error'; | ||||||
| import { SchemaType } from '../../misc/schema'; | import { SchemaType } from '../../misc/schema'; | ||||||
|  | import { AccessToken } from '../../models/entities/access-token'; | ||||||
| 
 | 
 | ||||||
| // TODO: defaultが設定されている場合はその型も考慮する
 | // TODO: defaultが設定されている場合はその型も考慮する
 | ||||||
| type Params<T extends IEndpointMeta> = { | type Params<T extends IEndpointMeta> = { | ||||||
|  | @ -14,12 +15,12 @@ type Params<T extends IEndpointMeta> = { | ||||||
| export type Response = Record<string, any> | void; | export type Response = Record<string, any> | void; | ||||||
| 
 | 
 | ||||||
| type executor<T extends IEndpointMeta> = | type executor<T extends IEndpointMeta> = | ||||||
| 	(params: Params<T>, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, isSecure: boolean, file?: any, cleanup?: Function) => | 	(params: Params<T>, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken, file?: any, cleanup?: Function) => | ||||||
| 		Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>; | 		Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>; | ||||||
| 
 | 
 | ||||||
| export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>) | export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>) | ||||||
| 		: (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, isSecure: boolean, file?: any) => Promise<any> { | 		: (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken, file?: any) => Promise<any> { | ||||||
| 	return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, isSecure: boolean, file?: any) => { | 	return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken, file?: any) => { | ||||||
| 		function cleanup() { | 		function cleanup() { | ||||||
| 			fs.unlink(file.path, () => {}); | 			fs.unlink(file.path, () => {}); | ||||||
| 		} | 		} | ||||||
|  | @ -36,7 +37,7 @@ export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>) | ||||||
| 			return Promise.reject(pserr); | 			return Promise.reject(pserr); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return cb(ps, user, isSecure, file, cleanup); | 		return cb(ps, user, token, file, cleanup); | ||||||
| 	}; | 	}; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,7 +28,9 @@ export const meta = { | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, user, isSecure) => { | export default define(meta, async (ps, user, token) => { | ||||||
|  | 	const isSecure = token == null; | ||||||
|  | 
 | ||||||
| 	// Lookup app
 | 	// Lookup app
 | ||||||
| 	const ap = await Apps.findOne(ps.appId); | 	const ap = await Apps.findOne(ps.appId); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -78,7 +78,7 @@ export const meta = { | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, user, isSecure, file, cleanup) => { | export default define(meta, async (ps, user, _, file, cleanup) => { | ||||||
| 	// Get 'name' parameter
 | 	// Get 'name' parameter
 | ||||||
| 	let name = ps.name || file.originalname; | 	let name = ps.name || file.originalname; | ||||||
| 	if (name !== undefined && name !== null) { | 	if (name !== undefined && name !== null) { | ||||||
|  |  | ||||||
|  | @ -19,7 +19,9 @@ export const meta = { | ||||||
| 	}, | 	}, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, user, isSecure) => { | export default define(meta, async (ps, user, token) => { | ||||||
|  | 	const isSecure = token == null; | ||||||
|  | 
 | ||||||
| 	return await Users.pack(user, user, { | 	return await Users.pack(user, user, { | ||||||
| 		detail: true, | 		detail: true, | ||||||
| 		includeHasUnreadNotes: true, | 		includeHasUnreadNotes: true, | ||||||
|  |  | ||||||
|  | @ -178,7 +178,9 @@ export const meta = { | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export default define(meta, async (ps, user, isSecure) => { | export default define(meta, async (ps, user, token) => { | ||||||
|  | 	const isSecure = token == null; | ||||||
|  | 
 | ||||||
| 	const updates = {} as Partial<User>; | 	const updates = {} as Partial<User>; | ||||||
| 	const profileUpdates = {} as Partial<UserProfile>; | 	const profileUpdates = {} as Partial<UserProfile>; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -132,7 +132,8 @@ export default define(meta, async (ps, user) => { | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	// Notify
 | 	// Notify
 | ||||||
| 	createNotification(note.userId, user.id, 'pollVote', { | 	createNotification(note.userId, 'pollVote', { | ||||||
|  | 		notifierId: user.id, | ||||||
| 		noteId: note.id, | 		noteId: note.id, | ||||||
| 		choice: ps.choice | 		choice: ps.choice | ||||||
| 	}); | 	}); | ||||||
|  | @ -143,7 +144,8 @@ export default define(meta, async (ps, user) => { | ||||||
| 		userId: Not(user.id), | 		userId: Not(user.id), | ||||||
| 	}).then(watchers => { | 	}).then(watchers => { | ||||||
| 		for (const watcher of watchers) { | 		for (const watcher of watchers) { | ||||||
| 			createNotification(watcher.userId, user.id, 'pollVote', { | 			createNotification(watcher.userId, 'pollVote', { | ||||||
|  | 				notifierId: user.id, | ||||||
| 				noteId: note.id, | 				noteId: note.id, | ||||||
| 				choice: ps.choice | 				choice: ps.choice | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
							
								
								
									
										37
									
								
								src/server/api/endpoints/notifications/create.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/server/api/endpoints/notifications/create.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | ||||||
|  | import $ from 'cafy'; | ||||||
|  | import define from '../../define'; | ||||||
|  | import { createNotification } from '../../../../services/create-notification'; | ||||||
|  | 
 | ||||||
|  | export const meta = { | ||||||
|  | 	tags: ['notifications'], | ||||||
|  | 
 | ||||||
|  | 	requireCredential: true as const, | ||||||
|  | 
 | ||||||
|  | 	kind: 'write:notifications', | ||||||
|  | 
 | ||||||
|  | 	params: { | ||||||
|  | 		body: { | ||||||
|  | 			validator: $.str | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		header: { | ||||||
|  | 			validator: $.optional.nullable.str | ||||||
|  | 		}, | ||||||
|  | 
 | ||||||
|  | 		icon: { | ||||||
|  | 			validator: $.optional.nullable.str | ||||||
|  | 		}, | ||||||
|  | 	}, | ||||||
|  | 
 | ||||||
|  | 	errors: { | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default define(meta, async (ps, user, token) => { | ||||||
|  | 	createNotification(user.id, 'app', { | ||||||
|  | 		appAccessTokenId: token.id, | ||||||
|  | 		customBody: ps.body, | ||||||
|  | 		customHeader: ps.header, | ||||||
|  | 		customIcon: ps.icon, | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | @ -104,7 +104,8 @@ export default define(meta, async (ps, me) => { | ||||||
| 	} as UserGroupInvitation); | 	} as UserGroupInvitation); | ||||||
| 
 | 
 | ||||||
| 	// 通知を作成
 | 	// 通知を作成
 | ||||||
| 	createNotification(user.id, me.id, 'groupInvited', { | 	createNotification(user.id, 'groupInvited', { | ||||||
|  | 		notifierId: me.id, | ||||||
| 		userGroupInvitationId: invitation.id | 		userGroupInvitationId: invitation.id | ||||||
| 	}); | 	}); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -9,10 +9,7 @@ import { EventEmitter } from 'events'; | ||||||
| import { User } from '../../../models/entities/user'; | import { User } from '../../../models/entities/user'; | ||||||
| import { Users, Followings, Mutings } from '../../../models'; | import { Users, Followings, Mutings } from '../../../models'; | ||||||
| import { ApiError } from '../error'; | import { ApiError } from '../error'; | ||||||
| 
 | import { AccessToken } from '../../../models/entities/access-token'; | ||||||
| type App = { |  | ||||||
| 	permission: string[]; |  | ||||||
| }; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Main stream connection |  * Main stream connection | ||||||
|  | @ -21,7 +18,7 @@ export default class Connection { | ||||||
| 	public user?: User; | 	public user?: User; | ||||||
| 	public following: User['id'][] = []; | 	public following: User['id'][] = []; | ||||||
| 	public muting: User['id'][] = []; | 	public muting: User['id'][] = []; | ||||||
| 	public app: App; | 	public token: AccessToken; | ||||||
| 	private wsConnection: websocket.connection; | 	private wsConnection: websocket.connection; | ||||||
| 	public subscriber: EventEmitter; | 	public subscriber: EventEmitter; | ||||||
| 	private channels: Channel[] = []; | 	private channels: Channel[] = []; | ||||||
|  | @ -33,12 +30,12 @@ export default class Connection { | ||||||
| 		wsConnection: websocket.connection, | 		wsConnection: websocket.connection, | ||||||
| 		subscriber: EventEmitter, | 		subscriber: EventEmitter, | ||||||
| 		user: User | null | undefined, | 		user: User | null | undefined, | ||||||
| 		app: App | null | undefined | 		token: AccessToken | null | undefined | ||||||
| 	) { | 	) { | ||||||
| 		this.wsConnection = wsConnection; | 		this.wsConnection = wsConnection; | ||||||
| 		this.subscriber = subscriber; | 		this.subscriber = subscriber; | ||||||
| 		if (user) this.user = user; | 		if (user) this.user = user; | ||||||
| 		if (app) this.app = app; | 		if (token) this.token = token; | ||||||
| 
 | 
 | ||||||
| 		this.wsConnection.on('message', this.onWsConnectionMessage); | 		this.wsConnection.on('message', this.onWsConnectionMessage); | ||||||
| 
 | 
 | ||||||
|  | @ -86,7 +83,7 @@ export default class Connection { | ||||||
| 		const endpoint = payload.endpoint || payload.ep; // alias
 | 		const endpoint = payload.endpoint || payload.ep; // alias
 | ||||||
| 
 | 
 | ||||||
| 		// 呼び出し
 | 		// 呼び出し
 | ||||||
| 		call(endpoint, user, this.app, payload.data).then(res => { | 		call(endpoint, user, this.token, payload.data).then(res => { | ||||||
| 			this.sendMessageToWs(`api:${payload.id}`, { res }); | 			this.sendMessageToWs(`api:${payload.id}`, { res }); | ||||||
| 		}).catch((e: ApiError) => { | 		}).catch((e: ApiError) => { | ||||||
| 			this.sendMessageToWs(`api:${payload.id}`, { | 			this.sendMessageToWs(`api:${payload.id}`, { | ||||||
|  |  | ||||||
|  | @ -3,46 +3,26 @@ import pushSw from './push-notification'; | ||||||
| import { Notifications, Mutings } from '../models'; | import { Notifications, Mutings } from '../models'; | ||||||
| import { genId } from '../misc/gen-id'; | import { genId } from '../misc/gen-id'; | ||||||
| import { User } from '../models/entities/user'; | import { User } from '../models/entities/user'; | ||||||
| import { Note } from '../models/entities/note'; |  | ||||||
| import { Notification } from '../models/entities/notification'; | import { Notification } from '../models/entities/notification'; | ||||||
| import { FollowRequest } from '../models/entities/follow-request'; |  | ||||||
| import { UserGroupInvitation } from '../models/entities/user-group-invitation'; |  | ||||||
| 
 | 
 | ||||||
| export async function createNotification( | export async function createNotification( | ||||||
| 	notifieeId: User['id'], | 	notifieeId: User['id'], | ||||||
| 	notifierId: User['id'], |  | ||||||
| 	type: Notification['type'], | 	type: Notification['type'], | ||||||
| 	content?: { | 	data: Partial<Notification> | ||||||
| 		noteId?: Note['id']; |  | ||||||
| 		reaction?: string; |  | ||||||
| 		choice?: number; |  | ||||||
| 		followRequestId?: FollowRequest['id']; |  | ||||||
| 		userGroupInvitationId?: UserGroupInvitation['id']; |  | ||||||
| 	} |  | ||||||
| ) { | ) { | ||||||
| 	if (notifieeId === notifierId) { | 	if (data.notifierId && (notifieeId === data.notifierId)) { | ||||||
| 		return null; | 		return null; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	const data = { | 	// Create notification
 | ||||||
|  | 	const notification = await Notifications.save({ | ||||||
| 		id: genId(), | 		id: genId(), | ||||||
| 		createdAt: new Date(), | 		createdAt: new Date(), | ||||||
| 		notifieeId: notifieeId, | 		notifieeId: notifieeId, | ||||||
| 		notifierId: notifierId, |  | ||||||
| 		type: type, | 		type: type, | ||||||
| 		isRead: false, | 		isRead: false, | ||||||
| 	} as Partial<Notification>; | 		...data | ||||||
| 
 | 	} as Partial<Notification>); | ||||||
| 	if (content) { |  | ||||||
| 		if (content.noteId) data.noteId = content.noteId; |  | ||||||
| 		if (content.reaction) data.reaction = content.reaction; |  | ||||||
| 		if (content.choice) data.choice = content.choice; |  | ||||||
| 		if (content.followRequestId) data.followRequestId = content.followRequestId; |  | ||||||
| 		if (content.userGroupInvitationId) data.userGroupInvitationId = content.userGroupInvitationId; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Create notification
 |  | ||||||
| 	const notification = await Notifications.save(data); |  | ||||||
| 
 | 
 | ||||||
| 	const packed = await Notifications.pack(notification); | 	const packed = await Notifications.pack(notification); | ||||||
| 
 | 
 | ||||||
|  | @ -58,7 +38,7 @@ export async function createNotification( | ||||||
| 			const mutings = await Mutings.find({ | 			const mutings = await Mutings.find({ | ||||||
| 				muterId: notifieeId | 				muterId: notifieeId | ||||||
| 			}); | 			}); | ||||||
| 			if (mutings.map(m => m.muteeId).includes(notifierId)) { | 			if (data.notifierId && mutings.map(m => m.muteeId).includes(data.notifierId)) { | ||||||
| 				return; | 				return; | ||||||
| 			} | 			} | ||||||
| 			//#endregion
 | 			//#endregion
 | ||||||
|  |  | ||||||
|  | @ -57,7 +57,9 @@ export async function insertFollowingDoc(followee: User, follower: User) { | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		// 通知を作成
 | 		// 通知を作成
 | ||||||
| 		createNotification(follower.id, followee.id, 'followRequestAccepted'); | 		createNotification(follower.id, 'followRequestAccepted', { | ||||||
|  | 			notifierId: followee.id, | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (alreadyFollowed) return; | 	if (alreadyFollowed) return; | ||||||
|  | @ -95,7 +97,9 @@ export async function insertFollowingDoc(followee: User, follower: User) { | ||||||
| 		Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'followed', packed)), | 		Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'followed', packed)), | ||||||
| 
 | 
 | ||||||
| 		// 通知を作成
 | 		// 通知を作成
 | ||||||
| 		createNotification(followee.id, follower.id, 'follow'); | 		createNotification(followee.id, 'follow', { | ||||||
|  | 			notifierId: follower.id | ||||||
|  | 		}); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -50,7 +50,8 @@ export default async function(follower: User, followee: User, requestId?: string | ||||||
| 		}).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); | 		}).then(packed => publishMainStream(followee.id, 'meUpdated', packed)); | ||||||
| 
 | 
 | ||||||
| 		// 通知を作成
 | 		// 通知を作成
 | ||||||
| 		createNotification(followee.id, follower.id, 'receiveFollowRequest', { | 		createNotification(followee.id, 'receiveFollowRequest', { | ||||||
|  | 			notifierId: follower.id, | ||||||
| 			followRequestId: followRequest.id | 			followRequestId: followRequest.id | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -78,7 +78,8 @@ class NotificationManager { | ||||||
| 
 | 
 | ||||||
| 			// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
 | 			// 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
 | ||||||
| 			if (!mentioneesMutedUserIds.includes(this.notifier.id)) { | 			if (!mentioneesMutedUserIds.includes(this.notifier.id)) { | ||||||
| 				createNotification(x.target, this.notifier.id, x.reason, { | 				createNotification(x.target, x.reason, { | ||||||
|  | 					notifierId: this.notifier.id, | ||||||
| 					noteId: this.note.id | 					noteId: this.note.id | ||||||
| 				}); | 				}); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | @ -48,7 +48,8 @@ export default async function(user: User, note: Note, choice: number) { | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	// Notify
 | 	// Notify
 | ||||||
| 	createNotification(note.userId, user.id, 'pollVote', { | 	createNotification(note.userId, 'pollVote', { | ||||||
|  | 		notifierId: user.id, | ||||||
| 		noteId: note.id, | 		noteId: note.id, | ||||||
| 		choice: choice | 		choice: choice | ||||||
| 	}); | 	}); | ||||||
|  | @ -60,7 +61,8 @@ export default async function(user: User, note: Note, choice: number) { | ||||||
| 	}) | 	}) | ||||||
| 	.then(watchers => { | 	.then(watchers => { | ||||||
| 		for (const watcher of watchers) { | 		for (const watcher of watchers) { | ||||||
| 			createNotification(watcher.userId, user.id, 'pollVote', { | 			createNotification(watcher.userId, 'pollVote', { | ||||||
|  | 				notifierId: user.id, | ||||||
| 				noteId: note.id, | 				noteId: note.id, | ||||||
| 				choice: choice | 				choice: choice | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
|  | @ -66,7 +66,8 @@ export default async (user: User, note: Note, reaction?: string) => { | ||||||
| 
 | 
 | ||||||
| 	// リアクションされたユーザーがローカルユーザーなら通知を作成
 | 	// リアクションされたユーザーがローカルユーザーなら通知を作成
 | ||||||
| 	if (note.userHost === null) { | 	if (note.userHost === null) { | ||||||
| 		createNotification(note.userId, user.id, 'reaction', { | 		createNotification(note.userId, 'reaction', { | ||||||
|  | 			notifierId: user.id, | ||||||
| 			noteId: note.id, | 			noteId: note.id, | ||||||
| 			reaction: reaction | 			reaction: reaction | ||||||
| 		}); | 		}); | ||||||
|  | @ -78,7 +79,8 @@ export default async (user: User, note: Note, reaction?: string) => { | ||||||
| 		userId: Not(user.id) | 		userId: Not(user.id) | ||||||
| 	}).then(watchers => { | 	}).then(watchers => { | ||||||
| 		for (const watcher of watchers) { | 		for (const watcher of watchers) { | ||||||
| 			createNotification(watcher.userId, user.id, 'reaction', { | 			createNotification(watcher.userId, 'reaction', { | ||||||
|  | 				notifierId: user.id, | ||||||
| 				noteId: note.id, | 				noteId: note.id, | ||||||
| 				reaction: reaction | 				reaction: reaction | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue