feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする (#13758)
* feat: 通報を受けた際にメールまたはWebhookで通知を送出出来るようにする * モデログに対応&エンドポイントを単一オブジェクトでのサポートに変更(API経由で大量に作るシチュエーションもないと思うので) * fix spdx * fix migration * fix migration * fix models * add e2e webhook * tweak * fix modlog * fix bugs * add tests and fix bugs * add tests and fix bugs * add tests * fix path * regenerate locale * 混入除去 * 混入除去 * add abuseReportResolved * fix pnpm-lock.yaml * add abuseReportResolved test * fix bugs * fix ui * add tests * fix CHANGELOG.md * add tests * add RoleService.getModeratorIds tests * WebhookServiceをUserとSystemに分割 * fix CHANGELOG.md * fix test * insertOneを使う用に * fix * regenerate locales * revert version * separate webhook job queue * fix * 🎨 * Update QueueProcessorService.ts --------- Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
100
packages/backend/src/models/AbuseReportNotificationRecipient.ts
Normal file
100
packages/backend/src/models/AbuseReportNotificationRecipient.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import { id } from './util/id.js';
|
||||
import { MiUser } from './User.js';
|
||||
|
||||
/**
|
||||
* 通報受信時に通知を送信する方法.
|
||||
*/
|
||||
export type RecipientMethod = 'email' | 'webhook';
|
||||
|
||||
@Entity('abuse_report_notification_recipient')
|
||||
export class MiAbuseReportNotificationRecipient {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
/**
|
||||
* 有効かどうか.
|
||||
*/
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public isActive: boolean;
|
||||
|
||||
/**
|
||||
* 更新日時.
|
||||
*/
|
||||
@Column('timestamp with time zone', {
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
/**
|
||||
* 通知設定名.
|
||||
*/
|
||||
@Column('varchar', {
|
||||
length: 255,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
/**
|
||||
* 通知方法.
|
||||
*/
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 64,
|
||||
})
|
||||
public method: RecipientMethod;
|
||||
|
||||
/**
|
||||
* 通知先のユーザID.
|
||||
*/
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public userId: MiUser['id'] | null;
|
||||
|
||||
/**
|
||||
* 通知先のユーザ.
|
||||
*/
|
||||
@ManyToOne(type => MiUser, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'userId', referencedColumnName: 'id', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId1' })
|
||||
public user: MiUser | null;
|
||||
|
||||
/**
|
||||
* 通知先のユーザプロフィール.
|
||||
*/
|
||||
@ManyToOne(type => MiUserProfile, {})
|
||||
@JoinColumn({ name: 'userId', referencedColumnName: 'userId', foreignKeyConstraintName: 'FK_abuse_report_notification_recipient_userId2' })
|
||||
public userProfile: MiUserProfile | null;
|
||||
|
||||
/**
|
||||
* 通知先のシステムWebhookId.
|
||||
*/
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public systemWebhookId: string | null;
|
||||
|
||||
/**
|
||||
* 通知先のシステムWebhook.
|
||||
*/
|
||||
@ManyToOne(type => MiSystemWebhook, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public systemWebhook: MiSystemWebhook | null;
|
||||
}
|
@@ -3,11 +3,83 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { Provider } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MiRepository, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame, miRepository } from './_.js';
|
||||
import {
|
||||
MiAbuseReportNotificationRecipient,
|
||||
MiAbuseUserReport,
|
||||
MiAccessToken,
|
||||
MiAd,
|
||||
MiAnnouncement,
|
||||
MiAnnouncementRead,
|
||||
MiAntenna,
|
||||
MiApp,
|
||||
MiAuthSession,
|
||||
MiAvatarDecoration,
|
||||
MiBlocking,
|
||||
MiBubbleGameRecord,
|
||||
MiChannel,
|
||||
MiChannelFavorite,
|
||||
MiChannelFollowing,
|
||||
MiClip,
|
||||
MiClipFavorite,
|
||||
MiClipNote,
|
||||
MiDriveFile,
|
||||
MiDriveFolder,
|
||||
MiEmoji,
|
||||
MiFlash,
|
||||
MiFlashLike,
|
||||
MiFollowing,
|
||||
MiFollowRequest,
|
||||
MiGalleryLike,
|
||||
MiGalleryPost,
|
||||
MiHashtag,
|
||||
MiInstance,
|
||||
MiMeta,
|
||||
MiModerationLog,
|
||||
MiMuting,
|
||||
MiNote,
|
||||
MiNoteFavorite,
|
||||
MiNoteReaction,
|
||||
MiNoteThreadMuting,
|
||||
MiNoteUnread,
|
||||
MiPage,
|
||||
MiPageLike,
|
||||
MiPasswordResetRequest,
|
||||
MiPoll,
|
||||
MiPollVote,
|
||||
MiPromoNote,
|
||||
MiPromoRead,
|
||||
MiRegistrationTicket,
|
||||
MiRegistryItem,
|
||||
MiRelay,
|
||||
MiRenoteMuting,
|
||||
MiRepository,
|
||||
miRepository,
|
||||
MiRetentionAggregation,
|
||||
MiReversiGame,
|
||||
MiRole,
|
||||
MiRoleAssignment,
|
||||
MiSignin,
|
||||
MiSwSubscription,
|
||||
MiSystemWebhook,
|
||||
MiUsedUsername,
|
||||
MiUser,
|
||||
MiUserIp,
|
||||
MiUserKeypair,
|
||||
MiUserList,
|
||||
MiUserListFavorite,
|
||||
MiUserListMembership,
|
||||
MiUserMemo,
|
||||
MiUserNotePining,
|
||||
MiUserPending,
|
||||
MiUserProfile,
|
||||
MiUserPublickey,
|
||||
MiUserSecurityKey,
|
||||
MiWebhook
|
||||
} from './_.js';
|
||||
import type { DataSource } from 'typeorm';
|
||||
import type { Provider } from '@nestjs/common';
|
||||
|
||||
const $usersRepository: Provider = {
|
||||
provide: DI.usersRepository,
|
||||
@@ -225,6 +297,12 @@ const $abuseUserReportsRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $abuseReportNotificationRecipientRepository: Provider = {
|
||||
provide: DI.abuseReportNotificationRecipientRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiAbuseReportNotificationRecipient),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $registrationTicketsRepository: Provider = {
|
||||
provide: DI.registrationTicketsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository<MiRegistrationTicket>),
|
||||
@@ -351,6 +429,12 @@ const $webhooksRepository: Provider = {
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $systemWebhooksRepository: Provider = {
|
||||
provide: DI.systemWebhooksRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiSystemWebhook),
|
||||
inject: [DI.db],
|
||||
};
|
||||
|
||||
const $adsRepository: Provider = {
|
||||
provide: DI.adsRepository,
|
||||
useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository<MiAd>),
|
||||
@@ -412,8 +496,7 @@ const $reversiGamesRepository: Provider = {
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
],
|
||||
imports: [],
|
||||
providers: [
|
||||
$usersRepository,
|
||||
$notesRepository,
|
||||
@@ -451,6 +534,7 @@ const $reversiGamesRepository: Provider = {
|
||||
$swSubscriptionsRepository,
|
||||
$hashtagsRepository,
|
||||
$abuseUserReportsRepository,
|
||||
$abuseReportNotificationRecipientRepository,
|
||||
$registrationTicketsRepository,
|
||||
$authSessionsRepository,
|
||||
$accessTokensRepository,
|
||||
@@ -472,6 +556,7 @@ const $reversiGamesRepository: Provider = {
|
||||
$channelFavoritesRepository,
|
||||
$registryItemsRepository,
|
||||
$webhooksRepository,
|
||||
$systemWebhooksRepository,
|
||||
$adsRepository,
|
||||
$passwordResetRequestsRepository,
|
||||
$retentionAggregationsRepository,
|
||||
@@ -520,6 +605,7 @@ const $reversiGamesRepository: Provider = {
|
||||
$swSubscriptionsRepository,
|
||||
$hashtagsRepository,
|
||||
$abuseUserReportsRepository,
|
||||
$abuseReportNotificationRecipientRepository,
|
||||
$registrationTicketsRepository,
|
||||
$authSessionsRepository,
|
||||
$accessTokensRepository,
|
||||
@@ -541,6 +627,7 @@ const $reversiGamesRepository: Provider = {
|
||||
$channelFavoritesRepository,
|
||||
$registryItemsRepository,
|
||||
$webhooksRepository,
|
||||
$systemWebhooksRepository,
|
||||
$adsRepository,
|
||||
$passwordResetRequestsRepository,
|
||||
$retentionAggregationsRepository,
|
||||
@@ -553,4 +640,5 @@ const $reversiGamesRepository: Provider = {
|
||||
$reversiGamesRepository,
|
||||
],
|
||||
})
|
||||
export class RepositoryModule {}
|
||||
export class RepositoryModule {
|
||||
}
|
||||
|
98
packages/backend/src/models/SystemWebhook.ts
Normal file
98
packages/backend/src/models/SystemWebhook.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Column, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||
import { Serialized } from '@/types.js';
|
||||
import { id } from './util/id.js';
|
||||
|
||||
export const systemWebhookEventTypes = [
|
||||
// ユーザからの通報を受けたとき
|
||||
'abuseReport',
|
||||
// 通報を処理したとき
|
||||
'abuseReportResolved',
|
||||
] as const;
|
||||
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
|
||||
|
||||
@Entity('system_webhook')
|
||||
export class MiSystemWebhook {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
/**
|
||||
* 有効かどうか.
|
||||
*/
|
||||
@Index('IDX_system_webhook_isActive', { synchronize: false })
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public isActive: boolean;
|
||||
|
||||
/**
|
||||
* 更新日時.
|
||||
*/
|
||||
@Column('timestamp with time zone', {
|
||||
default: () => 'CURRENT_TIMESTAMP',
|
||||
})
|
||||
public updatedAt: Date;
|
||||
|
||||
/**
|
||||
* 最後に送信された日時.
|
||||
*/
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public latestSentAt: Date | null;
|
||||
|
||||
/**
|
||||
* 最後に送信されたステータスコード
|
||||
*/
|
||||
@Column('integer', {
|
||||
nullable: true,
|
||||
})
|
||||
public latestStatus: number | null;
|
||||
|
||||
/**
|
||||
* 通知設定名.
|
||||
*/
|
||||
@Column('varchar', {
|
||||
length: 255,
|
||||
})
|
||||
public name: string;
|
||||
|
||||
/**
|
||||
* イベント種別.
|
||||
*/
|
||||
@Index('IDX_system_webhook_on', { synchronize: false })
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
array: true,
|
||||
default: '{}',
|
||||
})
|
||||
public on: SystemWebhookEventType[];
|
||||
|
||||
/**
|
||||
* Webhook送信先のURL.
|
||||
*/
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public url: string;
|
||||
|
||||
/**
|
||||
* Webhook検証用の値.
|
||||
*/
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public secret: string;
|
||||
|
||||
static deserialize(obj: Serialized<MiSystemWebhook>): MiSystemWebhook {
|
||||
return {
|
||||
...obj,
|
||||
updatedAt: new Date(obj.updatedAt),
|
||||
latestSentAt: obj.latestSentAt ? new Date(obj.latestSentAt) : null,
|
||||
};
|
||||
}
|
||||
}
|
@@ -11,6 +11,7 @@ import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transfor
|
||||
import { ObjectUtils } from 'typeorm/util/ObjectUtils.js';
|
||||
import { OrmUtils } from 'typeorm/util/OrmUtils.js';
|
||||
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js';
|
||||
import { MiAccessToken } from '@/models/AccessToken.js';
|
||||
import { MiAd } from '@/models/Ad.js';
|
||||
import { MiAnnouncement } from '@/models/Announcement.js';
|
||||
@@ -68,6 +69,7 @@ import { MiUserPublickey } from '@/models/UserPublickey.js';
|
||||
import { MiUserSecurityKey } from '@/models/UserSecurityKey.js';
|
||||
import { MiUserMemo } from '@/models/UserMemo.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { MiSystemWebhook } from '@/models/SystemWebhook.js';
|
||||
import { MiChannel } from '@/models/Channel.js';
|
||||
import { MiRetentionAggregation } from '@/models/RetentionAggregation.js';
|
||||
import { MiRole } from '@/models/Role.js';
|
||||
@@ -144,6 +146,7 @@ export const miRepository = {
|
||||
|
||||
export {
|
||||
MiAbuseUserReport,
|
||||
MiAbuseReportNotificationRecipient,
|
||||
MiAccessToken,
|
||||
MiAd,
|
||||
MiAnnouncement,
|
||||
@@ -201,6 +204,7 @@ export {
|
||||
MiUserPublickey,
|
||||
MiUserSecurityKey,
|
||||
MiWebhook,
|
||||
MiSystemWebhook,
|
||||
MiChannel,
|
||||
MiRetentionAggregation,
|
||||
MiRole,
|
||||
@@ -213,6 +217,7 @@ export {
|
||||
};
|
||||
|
||||
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
|
||||
export type AbuseReportNotificationRecipientRepository = Repository<MiAbuseReportNotificationRecipient> & MiRepository<MiAbuseReportNotificationRecipient>;
|
||||
export type AccessTokensRepository = Repository<MiAccessToken> & MiRepository<MiAccessToken>;
|
||||
export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>;
|
||||
export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>;
|
||||
@@ -270,6 +275,7 @@ export type UserProfilesRepository = Repository<MiUserProfile> & MiRepository<Mi
|
||||
export type UserPublickeysRepository = Repository<MiUserPublickey> & MiRepository<MiUserPublickey>;
|
||||
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey> & MiRepository<MiUserSecurityKey>;
|
||||
export type WebhooksRepository = Repository<MiWebhook> & MiRepository<MiWebhook>;
|
||||
export type SystemWebhooksRepository = Repository<MiSystemWebhook> & MiRepository<MiWebhook>;
|
||||
export type ChannelsRepository = Repository<MiChannel> & MiRepository<MiChannel>;
|
||||
export type RetentionAggregationsRepository = Repository<MiRetentionAggregation> & MiRepository<MiRetentionAggregation>;
|
||||
export type RolesRepository = Repository<MiRole> & MiRepository<MiRole>;
|
||||
|
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export const packedAbuseReportNotificationRecipientSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isActive: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
method: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: ['email', 'webhook'],
|
||||
},
|
||||
userId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
optional: true, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
systemWebhookId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: false,
|
||||
},
|
||||
systemWebhook: {
|
||||
type: 'object',
|
||||
optional: true, nullable: false,
|
||||
ref: 'SystemWebhook',
|
||||
},
|
||||
},
|
||||
} as const;
|
54
packages/backend/src/models/json-schema/system-webhook.ts
Normal file
54
packages/backend/src/models/json-schema/system-webhook.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
|
||||
|
||||
export const packedSystemWebhookSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
isActive: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
updatedAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
latestSentAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
latestStatus: {
|
||||
type: 'number',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
on: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
enum: systemWebhookEventTypes,
|
||||
},
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
secret: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
Reference in New Issue
Block a user