3
3
* SPDX-License-Identifier: AGPL-3.0-only
4
4
*/
5
5
6
- import { Injectable } from '@nestjs/common' ;
6
+ import { Inject , Injectable } from '@nestjs/common' ;
7
+ import { In } from 'typeorm' ;
7
8
import type Logger from '@/logger.js' ;
8
9
import { bindThis } from '@/decorators.js' ;
9
10
import { MetaService } from '@/core/MetaService.js' ;
10
11
import { RoleService } from '@/core/RoleService.js' ;
12
+ import { EmailService } from '@/core/EmailService.js' ;
13
+ import { MiUser , type UserProfilesRepository } from '@/models/_.js' ;
14
+ import { DI } from '@/di-symbols.js' ;
15
+ import { SystemWebhookService } from '@/core/SystemWebhookService.js' ;
16
+ import { AnnouncementService } from '@/core/AnnouncementService.js' ;
11
17
import { QueueLoggerService } from '../QueueLoggerService.js' ;
12
18
13
19
// モデレーターが不在と判断する日付の閾値
14
20
const MODERATOR_INACTIVITY_LIMIT_DAYS = 7 ;
15
- const ONE_DAY_MILLI_SEC = 1000 * 60 * 60 * 24 ;
21
+ // 警告通知やログ出力を行う残日数の閾値
22
+ const MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS = 2 ;
23
+ // 期限から6時間ごとに通知を行う
24
+ const MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS = 6 ;
25
+ const ONE_HOUR_MILLI_SEC = 1000 * 60 * 60 ;
26
+ const ONE_DAY_MILLI_SEC = ONE_HOUR_MILLI_SEC * 24 ;
27
+
28
+ export type ModeratorInactivityEvaluationResult = {
29
+ isModeratorsInactive : boolean ;
30
+ inactiveModerators : MiUser [ ] ;
31
+ remainingTime : ModeratorInactivityRemainingTime ;
32
+ }
33
+
34
+ export type ModeratorInactivityRemainingTime = {
35
+ time : number ;
36
+ asHours : number ;
37
+ asDays : number ;
38
+ } ;
39
+
40
+ function generateModeratorInactivityMail ( remainingTime : ModeratorInactivityRemainingTime ) {
41
+ const subject = 'Moderator Inactivity Warning / モデレーター不在の通知' ;
42
+
43
+ const timeVariant = remainingTime . asDays === 0 ? `${ remainingTime . asHours } hours` : `${ remainingTime . asDays } days` ;
44
+ const timeVariantJa = remainingTime . asDays === 0 ? `${ remainingTime . asHours } 時間` : `${ remainingTime . asDays } 日間` ;
45
+ const message = [
46
+ 'To Moderators,' ,
47
+ '' ,
48
+ `A moderator has been inactive for a period of time. If there are ${ timeVariant } of inactivity left, it will switch to invitation only.` ,
49
+ 'If you do not wish to move to invitation only, you must log into Misskey and update your last active date and time.' ,
50
+ '' ,
51
+ '---------------' ,
52
+ '' ,
53
+ 'To モデレーター各位' ,
54
+ '' ,
55
+ `モデレーターが一定期間活動していないようです。あと${ timeVariantJa } 活動していない状態が続くと招待制に切り替わります。` ,
56
+ '招待制に切り替わることを望まない場合は、Misskeyにログインして最終アクティブ日時を更新してください。' ,
57
+ '' ,
58
+ ] ;
59
+
60
+ const html = message . join ( '<br>' ) ;
61
+ const text = message . join ( '\n' ) ;
62
+
63
+ return {
64
+ subject,
65
+ html,
66
+ text,
67
+ } ;
68
+ }
69
+
70
+ function generateInvitationOnlyChangedMail ( ) {
71
+ const subject = 'Change to Invitation-Only / 招待制に変更されました' ;
72
+
73
+ const message = [
74
+ 'To Moderators,' ,
75
+ '' ,
76
+ `Changed to invitation only because no moderator activity was detected for ${ MODERATOR_INACTIVITY_LIMIT_DAYS } days.` ,
77
+ 'To cancel the invitation only, you need to access the control panel.' ,
78
+ '' ,
79
+ '---------------' ,
80
+ '' ,
81
+ 'To モデレーター各位' ,
82
+ '' ,
83
+ `モデレーターの活動が${ MODERATOR_INACTIVITY_LIMIT_DAYS } 日間検出されなかったため、招待制に変更されました。` ,
84
+ '招待制を解除するには、コントロールパネルにアクセスする必要があります。' ,
85
+ '' ,
86
+ ] ;
87
+
88
+ const html = message . join ( '<br>' ) ;
89
+ const text = message . join ( '\n' ) ;
90
+
91
+ return {
92
+ subject,
93
+ html,
94
+ text,
95
+ } ;
96
+ }
16
97
17
98
@Injectable ( )
18
99
export class CheckModeratorsActivityProcessorService {
19
100
private logger : Logger ;
20
101
21
102
constructor (
103
+ @Inject ( DI . userProfilesRepository )
104
+ private userProfilesRepository : UserProfilesRepository ,
22
105
private metaService : MetaService ,
23
106
private roleService : RoleService ,
107
+ private emailService : EmailService ,
108
+ private announcementService : AnnouncementService ,
109
+ private systemWebhookService : SystemWebhookService ,
24
110
private queueLoggerService : QueueLoggerService ,
25
111
) {
26
112
this . logger = this . queueLoggerService . logger . createSubLogger ( 'check-moderators-activity' ) ;
@@ -42,18 +128,23 @@ export class CheckModeratorsActivityProcessorService {
42
128
43
129
@bindThis
44
130
private async processImpl ( ) {
45
- const { isModeratorsInactive , inactivityLimitCountdown } = await this . evaluateModeratorsInactiveDays ( ) ;
46
- if ( isModeratorsInactive ) {
131
+ const evaluateResult = await this . evaluateModeratorsInactiveDays ( ) ;
132
+ if ( evaluateResult . isModeratorsInactive ) {
47
133
this . logger . warn ( `The moderator has been inactive for ${ MODERATOR_INACTIVITY_LIMIT_DAYS } days. We will move to invitation only.` ) ;
48
- await this . changeToInvitationOnly ( ) ;
49
134
50
- // TODO: モデレータに通知メール+Misskey通知
51
- // TODO: SystemWebhook通知
135
+ await this . changeToInvitationOnly ( ) ;
136
+ await this . notifyChangeToInvitationOnly ( ) ;
52
137
} else {
53
- if ( inactivityLimitCountdown <= 2 ) {
54
- this . logger . warn ( `A moderator has been inactive for a period of time. If you are inactive for an additional ${ inactivityLimitCountdown } days, it will switch to invitation only.` ) ;
138
+ const remainingTime = evaluateResult . remainingTime ;
139
+ if ( remainingTime . asDays <= MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS ) {
140
+ const timeVariant = remainingTime . asDays === 0 ? `${ remainingTime . asHours } hours` : `${ remainingTime . asDays } days` ;
141
+ this . logger . warn ( `A moderator has been inactive for a period of time. If you are inactive for an additional ${ timeVariant } , it will switch to invitation only.` ) ;
55
142
56
- // TODO: 警告メール
143
+ if ( remainingTime . asHours % MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS === 0 ) {
144
+ // ジョブの実行頻度と同等だと通知が多すぎるため期限から6時間ごとに通知する
145
+ // つまり、のこり2日を切ったら6時間ごとに通知が送られる
146
+ await this . notifyInactiveModeratorsWarning ( remainingTime ) ;
147
+ }
57
148
}
58
149
}
59
150
}
@@ -87,7 +178,7 @@ export class CheckModeratorsActivityProcessorService {
87
178
* この場合、モデレータA, B, Cのアクティビティは判定基準日よりも古いため、モデレーターが不在と判断される。
88
179
*/
89
180
@bindThis
90
- public async evaluateModeratorsInactiveDays ( ) {
181
+ public async evaluateModeratorsInactiveDays ( ) : Promise < ModeratorInactivityEvaluationResult > {
91
182
const today = new Date ( ) ;
92
183
const inactivePeriod = new Date ( today ) ;
93
184
inactivePeriod . setDate ( today . getDate ( ) - MODERATOR_INACTIVITY_LIMIT_DAYS ) ;
@@ -101,12 +192,18 @@ export class CheckModeratorsActivityProcessorService {
101
192
// 残りの猶予を示したいので、最終アクティブ日時が一番若いモデレータの日数を基準に猶予を計算する
102
193
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
103
194
const newestLastActiveDate = new Date ( Math . max ( ...moderators . map ( it => it . lastActiveDate ! . getTime ( ) ) ) ) ;
104
- const inactivityLimitCountdown = Math . floor ( ( newestLastActiveDate . getTime ( ) - inactivePeriod . getTime ( ) ) / ONE_DAY_MILLI_SEC ) ;
195
+ const remainingTime = newestLastActiveDate . getTime ( ) - inactivePeriod . getTime ( ) ;
196
+ const remainingTimeAsDays = Math . floor ( remainingTime / ONE_DAY_MILLI_SEC ) ;
197
+ const remainingTimeAsHours = Math . floor ( ( remainingTime / ONE_HOUR_MILLI_SEC ) ) ;
105
198
106
199
return {
107
200
isModeratorsInactive : inactiveModerators . length === moderators . length ,
108
201
inactiveModerators,
109
- inactivityLimitCountdown,
202
+ remainingTime : {
203
+ time : remainingTime ,
204
+ asHours : remainingTimeAsHours ,
205
+ asDays : remainingTimeAsDays ,
206
+ } ,
110
207
} ;
111
208
}
112
209
@@ -115,6 +212,74 @@ export class CheckModeratorsActivityProcessorService {
115
212
await this . metaService . update ( { disableRegistration : true } ) ;
116
213
}
117
214
215
+ @bindThis
216
+ public async notifyInactiveModeratorsWarning ( remainingTime : ModeratorInactivityRemainingTime ) {
217
+ // -- モデレータへのメールとお知らせ(個人向け)送信
218
+
219
+ const moderators = await this . fetchModerators ( ) ;
220
+ const moderatorProfiles = await this . userProfilesRepository
221
+ . findBy ( { userId : In ( moderators . map ( it => it . id ) ) } )
222
+ . then ( it => new Map ( it . map ( it => [ it . userId , it ] ) ) ) ;
223
+
224
+ const mail = generateModeratorInactivityMail ( remainingTime ) ;
225
+ for ( const moderator of moderators ) {
226
+ const profile = moderatorProfiles . get ( moderator . id ) ;
227
+ if ( profile && profile . email && profile . emailVerified ) {
228
+ this . emailService . sendEmail ( profile . email , mail . subject , mail . html , mail . text ) . then ( ) ;
229
+ }
230
+ }
231
+
232
+ // -- SystemWebhook
233
+
234
+ const systemWebhooks = await this . systemWebhookService . fetchActiveSystemWebhooks ( )
235
+ . then ( it => it . filter ( it => it . on . includes ( 'inactiveModeratorsWarning' ) ) ) ;
236
+ for ( const systemWebhook of systemWebhooks ) {
237
+ this . systemWebhookService . enqueueSystemWebhook (
238
+ systemWebhook ,
239
+ 'inactiveModeratorsWarning' ,
240
+ { remainingTime : remainingTime } ,
241
+ ) . then ( ) ;
242
+ }
243
+ }
244
+
245
+ @bindThis
246
+ public async notifyChangeToInvitationOnly ( ) {
247
+ // -- モデレータへのメールとお知らせ(個人向け)送信
248
+
249
+ const moderators = await this . fetchModerators ( ) ;
250
+ const moderatorProfiles = await this . userProfilesRepository
251
+ . findBy ( { userId : In ( moderators . map ( it => it . id ) ) } )
252
+ . then ( it => new Map ( it . map ( it => [ it . userId , it ] ) ) ) ;
253
+
254
+ const mail = generateInvitationOnlyChangedMail ( ) ;
255
+ for ( const moderator of moderators ) {
256
+ this . announcementService . create ( {
257
+ title : mail . subject ,
258
+ text : mail . text ,
259
+ forExistingUsers : true ,
260
+ needConfirmationToRead : true ,
261
+ userId : moderator . id ,
262
+ } ) . then ( ) ;
263
+
264
+ const profile = moderatorProfiles . get ( moderator . id ) ;
265
+ if ( profile && profile . email && profile . emailVerified ) {
266
+ this . emailService . sendEmail ( profile . email , mail . subject , mail . html , mail . text ) . then ( ) ;
267
+ }
268
+ }
269
+
270
+ // -- SystemWebhook
271
+
272
+ const systemWebhooks = await this . systemWebhookService . fetchActiveSystemWebhooks ( )
273
+ . then ( it => it . filter ( it => it . on . includes ( 'inactiveModeratorsInvitationOnlyChanged' ) ) ) ;
274
+ for ( const systemWebhook of systemWebhooks ) {
275
+ this . systemWebhookService . enqueueSystemWebhook (
276
+ systemWebhook ,
277
+ 'inactiveModeratorsInvitationOnlyChanged' ,
278
+ { } ,
279
+ ) . then ( ) ;
280
+ }
281
+ }
282
+
118
283
@bindThis
119
284
private async fetchModerators ( ) {
120
285
// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
0 commit comments