]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-comment.ts
Implement remote runner jobs in server
[github/Chocobozzz/PeerTube.git] / server / models / video / video-comment.ts
CommitLineData
cde3d90d 1import { FindOptions, Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize'
57f6896f
C
2import {
3 AllowNull,
57f6896f
C
4 BelongsTo,
5 Column,
6 CreatedAt,
7 DataType,
8 ForeignKey,
9 HasMany,
10 Is,
11 Model,
12 Scopes,
13 Table,
14 UpdatedAt
15} from 'sequelize-typescript'
444c0a0e 16import { getServerActor } from '@server/models/application/application'
26d6bf65 17import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
cde3d90d 18import { pick, uniqify } from '@shared/core-utils'
d0800f76 19import { AttributesOnly } from '@shared/typescript-utils'
69222afa 20import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
ea44f375 21import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
2b02c520 22import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model'
f7cc67b4 23import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
444c0a0e 24import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
f7cc67b4 25import { regexpCapture } from '../../helpers/regexp'
444c0a0e 26import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
453e83ea
C
27import {
28 MComment,
0f8d00e3 29 MCommentAdminFormattable,
b5fecbf4 30 MCommentAP,
1ca9f7c3 31 MCommentFormattable,
453e83ea
C
32 MCommentId,
33 MCommentOwner,
34 MCommentOwnerReplyVideoLight,
35 MCommentOwnerVideo,
36 MCommentOwnerVideoFeed,
696d83fd
C
37 MCommentOwnerVideoReply,
38 MVideoImmutable
26d6bf65 39} from '../../types/models/video'
57f6896f 40import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
444c0a0e 41import { AccountModel } from '../account/account'
cde3d90d 42import { ActorModel } from '../actor/actor'
8c4bbd94 43import { buildLocalAccountIdsIn, buildSQLAttributes, throwIfNotValid } from '../shared'
cde3d90d 44import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder'
444c0a0e
C
45import { VideoModel } from './video'
46import { VideoChannelModel } from './video-channel'
6d852470 47
594d3e48 48export enum ScopeNames {
ea44f375 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
4635f59d 50 WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO',
cde3d90d 51 WITH_VIDEO = 'WITH_VIDEO'
bf1f6508
C
52}
53
3acc5084 54@Scopes(() => ({
d3ea8975 55 [ScopeNames.WITH_ACCOUNT]: {
bf1f6508 56 include: [
4635f59d 57 {
453e83ea 58 model: AccountModel
4635f59d 59 }
3acc5084 60 ]
ea44f375
C
61 },
62 [ScopeNames.WITH_IN_REPLY_TO]: {
63 include: [
64 {
3acc5084 65 model: VideoCommentModel,
da854ddd
C
66 as: 'InReplyToVideoComment'
67 }
68 ]
69 },
70 [ScopeNames.WITH_VIDEO]: {
71 include: [
72 {
3acc5084 73 model: VideoModel,
4cb6d457
C
74 required: true,
75 include: [
76 {
453e83ea 77 model: VideoChannelModel,
4cb6d457
C
78 required: true,
79 include: [
80 {
3acc5084 81 model: AccountModel,
453e83ea 82 required: true
4cb6d457
C
83 }
84 ]
85 }
86 ]
ea44f375 87 }
3acc5084 88 ]
bf1f6508 89 }
3acc5084 90}))
6d852470
C
91@Table({
92 tableName: 'videoComment',
93 indexes: [
94 {
95 fields: [ 'videoId' ]
bf1f6508
C
96 },
97 {
98 fields: [ 'videoId', 'originCommentId' ]
0776d83f
C
99 },
100 {
101 fields: [ 'url' ],
102 unique: true
8cd72bd3
C
103 },
104 {
105 fields: [ 'accountId' ]
b84d4c80
C
106 },
107 {
108 fields: [
109 { name: 'createdAt', order: 'DESC' }
110 ]
6d852470
C
111 }
112 ]
113})
16c016e8 114export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoCommentModel>>> {
6d852470
C
115 @CreatedAt
116 createdAt: Date
117
118 @UpdatedAt
119 updatedAt: Date
120
69222afa
JM
121 @AllowNull(true)
122 @Column(DataType.DATE)
123 deletedAt: Date
124
6d852470
C
125 @AllowNull(false)
126 @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
127 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
128 url: string
129
130 @AllowNull(false)
131 @Column(DataType.TEXT)
132 text: string
133
134 @ForeignKey(() => VideoCommentModel)
135 @Column
136 originCommentId: number
137
138 @BelongsTo(() => VideoCommentModel, {
139 foreignKey: {
db799da3 140 name: 'originCommentId',
6d852470
C
141 allowNull: true
142 },
db799da3 143 as: 'OriginVideoComment',
6d852470
C
144 onDelete: 'CASCADE'
145 })
146 OriginVideoComment: VideoCommentModel
147
148 @ForeignKey(() => VideoCommentModel)
149 @Column
150 inReplyToCommentId: number
151
152 @BelongsTo(() => VideoCommentModel, {
153 foreignKey: {
db799da3 154 name: 'inReplyToCommentId',
6d852470
C
155 allowNull: true
156 },
da854ddd 157 as: 'InReplyToVideoComment',
6d852470
C
158 onDelete: 'CASCADE'
159 })
c1e791ba 160 InReplyToVideoComment: VideoCommentModel | null
6d852470
C
161
162 @ForeignKey(() => VideoModel)
163 @Column
164 videoId: number
165
166 @BelongsTo(() => VideoModel, {
167 foreignKey: {
168 allowNull: false
169 },
170 onDelete: 'CASCADE'
171 })
172 Video: VideoModel
173
d3ea8975 174 @ForeignKey(() => AccountModel)
6d852470 175 @Column
d3ea8975 176 accountId: number
6d852470 177
d3ea8975 178 @BelongsTo(() => AccountModel, {
6d852470 179 foreignKey: {
69222afa 180 allowNull: true
6d852470
C
181 },
182 onDelete: 'CASCADE'
183 })
d3ea8975 184 Account: AccountModel
6d852470 185
57f6896f
C
186 @HasMany(() => VideoCommentAbuseModel, {
187 foreignKey: {
310b5219 188 name: 'videoCommentId',
57f6896f
C
189 allowNull: true
190 },
191 onDelete: 'set null'
192 })
193 CommentAbuses: VideoCommentAbuseModel[]
194
eb66ee88
C
195 // ---------------------------------------------------------------------------
196
197 static getSQLAttributes (tableName: string, aliasPrefix = '') {
198 return buildSQLAttributes({
199 model: this,
200 tableName,
201 aliasPrefix
202 })
203 }
204
205 // ---------------------------------------------------------------------------
206
b49f22d8 207 static loadById (id: number, t?: Transaction): Promise<MComment> {
1735c825 208 const query: FindOptions = {
bf1f6508
C
209 where: {
210 id
211 }
212 }
213
214 if (t !== undefined) query.transaction = t
215
216 return VideoCommentModel.findOne(query)
217 }
218
b49f22d8 219 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Transaction): Promise<MCommentOwnerVideoReply> {
1735c825 220 const query: FindOptions = {
da854ddd
C
221 where: {
222 id
223 }
224 }
225
226 if (t !== undefined) query.transaction = t
227
228 return VideoCommentModel
229 .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ])
230 .findOne(query)
231 }
232
b49f22d8 233 static loadByUrlAndPopulateAccountAndVideo (url: string, t?: Transaction): Promise<MCommentOwnerVideo> {
1735c825 234 const query: FindOptions = {
6d852470
C
235 where: {
236 url
237 }
238 }
239
240 if (t !== undefined) query.transaction = t
241
511765c9 242 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEO ]).findOne(query)
6d852470 243 }
bf1f6508 244
b49f22d8 245 static loadByUrlAndPopulateReplyAndVideoUrlAndAccount (url: string, t?: Transaction): Promise<MCommentOwnerReplyVideoLight> {
1735c825 246 const query: FindOptions = {
4cb6d457
C
247 where: {
248 url
6b9c966f
C
249 },
250 include: [
251 {
252 attributes: [ 'id', 'url' ],
253 model: VideoModel.unscoped()
254 }
255 ]
4cb6d457
C
256 }
257
258 if (t !== undefined) query.transaction = t
259
6b9c966f 260 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_ACCOUNT ]).findOne(query)
4cb6d457
C
261 }
262
0f8d00e3
C
263 static listCommentsForApi (parameters: {
264 start: number
265 count: number
266 sort: string
267
0e6cd1c0 268 onLocalVideo?: boolean
0f8d00e3
C
269 isLocal?: boolean
270 search?: string
271 searchAccount?: string
272 searchVideo?: string
273 }) {
cde3d90d
C
274 const queryOptions: ListVideoCommentsOptions = {
275 ...pick(parameters, [ 'start', 'count', 'sort', 'isLocal', 'search', 'searchVideo', 'searchAccount', 'onLocalVideo' ]),
0f8d00e3 276
cde3d90d
C
277 selectType: 'api',
278 notDeleted: true
f1273314 279 }
0f8d00e3 280
d0800f76 281 return Promise.all([
cde3d90d
C
282 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(),
283 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
284 ]).then(([ rows, count ]) => {
285 return { total: count, data: rows }
286 })
0f8d00e3
C
287 }
288
b4055e1c 289 static async listThreadsForApi (parameters: {
a1587156 290 videoId: number
696d83fd 291 isVideoOwned: boolean
a1587156
C
292 start: number
293 count: number
294 sort: string
453e83ea 295 user?: MUserAccountId
b4055e1c 296 }) {
cde3d90d 297 const { videoId, user } = parameters
b4055e1c 298
cde3d90d 299 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user })
7ad9b984 300
cde3d90d
C
301 const commonOptions: ListVideoCommentsOptions = {
302 selectType: 'api',
303 videoId,
304 blockerAccountIds
9d6b9d10
C
305 }
306
cde3d90d
C
307 const listOptions: ListVideoCommentsOptions = {
308 ...commonOptions,
309 ...pick(parameters, [ 'sort', 'start', 'count' ]),
310
311 isThread: true,
312 includeReplyCounters: true
bf1f6508
C
313 }
314
cde3d90d
C
315 const countOptions: ListVideoCommentsOptions = {
316 ...commonOptions,
7ad9b984 317
cde3d90d
C
318 isThread: true
319 }
d0800f76 320
cde3d90d
C
321 const notDeletedCountOptions: ListVideoCommentsOptions = {
322 ...commonOptions,
323
324 notDeleted: true
9d6b9d10
C
325 }
326
327 return Promise.all([
cde3d90d
C
328 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, listOptions).listComments<MCommentAdminFormattable>(),
329 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, countOptions).countComments(),
330 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, notDeletedCountOptions).countComments()
d0800f76 331 ]).then(([ rows, count, totalNotDeletedComments ]) => {
9d6b9d10
C
332 return { total: count, data: rows, totalNotDeletedComments }
333 })
bf1f6508
C
334 }
335
b4055e1c 336 static async listThreadCommentsForApi (parameters: {
a1587156
C
337 videoId: number
338 threadId: number
453e83ea 339 user?: MUserAccountId
b4055e1c 340 }) {
cde3d90d 341 const { user } = parameters
b4055e1c 342
cde3d90d 343 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user })
7ad9b984 344
cde3d90d
C
345 const queryOptions: ListVideoCommentsOptions = {
346 ...pick(parameters, [ 'videoId', 'threadId' ]),
bf1f6508 347
cde3d90d
C
348 selectType: 'api',
349 sort: 'createdAt',
350
351 blockerAccountIds,
352 includeReplyCounters: true
353 }
7ad9b984 354
d0800f76 355 return Promise.all([
cde3d90d
C
356 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(),
357 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
358 ]).then(([ rows, count ]) => {
359 return { total: count, data: rows }
360 })
bf1f6508
C
361 }
362
b49f22d8 363 static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> {
d7e70384 364 const query = {
1735c825 365 order: [ [ 'createdAt', order ] ] as Order,
d7e70384 366 where: {
d7e70384 367 id: {
a1587156 368 [Op.in]: Sequelize.literal('(' +
a3cffab4 369 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
f7cc67b4
C
370 `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
371 'UNION ' +
372 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' +
373 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
374 ') ' +
a3cffab4
C
375 'SELECT id FROM children' +
376 ')'),
a1587156 377 [Op.ne]: comment.id
d7e70384
C
378 }
379 },
380 transaction: t
381 }
382
383 return VideoCommentModel
384 .scope([ ScopeNames.WITH_ACCOUNT ])
385 .findAll(query)
386 }
387
cde3d90d
C
388 static async listAndCountByVideoForAP (parameters: {
389 video: MVideoImmutable
390 start: number
391 count: number
392 }) {
393 const { video } = parameters
394
395 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null })
396
397 const queryOptions: ListVideoCommentsOptions = {
398 ...pick(parameters, [ 'start', 'count' ]),
399
400 selectType: 'comment-only',
696d83fd 401 videoId: video.id,
cde3d90d 402 sort: 'createdAt',
696d83fd 403
cde3d90d 404 blockerAccountIds
8fffe21a
C
405 }
406
d0800f76 407 return Promise.all([
cde3d90d
C
408 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>(),
409 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
410 ]).then(([ rows, count ]) => {
411 return { total: count, data: rows }
412 })
8fffe21a
C
413 }
414
00494d6e
RK
415 static async listForFeed (parameters: {
416 start: number
417 count: number
418 videoId?: number
419 accountId?: number
420 videoChannelId?: number
cde3d90d
C
421 }) {
422 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null })
00494d6e 423
cde3d90d
C
424 const queryOptions: ListVideoCommentsOptions = {
425 ...pick(parameters, [ 'start', 'count', 'accountId', 'videoId', 'videoChannelId' ]),
1c58423f 426
cde3d90d 427 selectType: 'feed',
1c58423f 428
cde3d90d
C
429 sort: '-createdAt',
430 onPublicVideo: true,
431 notDeleted: true,
1df8a4d7 432
cde3d90d 433 blockerAccountIds
fe3a55b0
C
434 }
435
cde3d90d 436 return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentOwnerVideoFeed>()
fe3a55b0
C
437 }
438
444c0a0e 439 static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) {
cde3d90d
C
440 const queryOptions: ListVideoCommentsOptions = {
441 selectType: 'comment-only',
444c0a0e 442
cde3d90d
C
443 accountId: ofAccount.id,
444 videoAccountOwnerId: filter.onVideosOfAccount?.id,
445
446 notDeleted: true,
447 count: 5000
444c0a0e
C
448 }
449
cde3d90d 450 return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>()
444c0a0e
C
451 }
452
09cababd
C
453 static async getStats () {
454 const totalLocalVideoComments = await VideoCommentModel.count({
455 include: [
456 {
5e0dbb3e 457 model: AccountModel.unscoped(),
09cababd
C
458 required: true,
459 include: [
460 {
5e0dbb3e 461 model: ActorModel.unscoped(),
09cababd
C
462 required: true,
463 where: {
464 serverId: null
465 }
466 }
467 ]
468 }
469 ]
470 })
471 const totalVideoComments = await VideoCommentModel.count()
472
473 return {
474 totalLocalVideoComments,
475 totalVideoComments
476 }
477 }
478
74d249bc
C
479 static listRemoteCommentUrlsOfLocalVideos () {
480 const query = `SELECT "videoComment".url FROM "videoComment" ` +
481 `INNER JOIN account ON account.id = "videoComment"."accountId" ` +
482 `INNER JOIN actor ON actor.id = "account"."actorId" AND actor."serverId" IS NOT NULL ` +
483 `INNER JOIN video ON video.id = "videoComment"."videoId" AND video.remote IS FALSE`
484
485 return VideoCommentModel.sequelize.query<{ url: string }>(query, {
486 type: QueryTypes.SELECT,
487 raw: true
488 }).then(rows => rows.map(r => r.url))
489 }
490
2ba92871
C
491 static cleanOldCommentsOf (videoId: number, beforeUpdatedAt: Date) {
492 const query = {
493 where: {
494 updatedAt: {
1735c825 495 [Op.lt]: beforeUpdatedAt
2ba92871 496 },
6b9c966f
C
497 videoId,
498 accountId: {
499 [Op.notIn]: buildLocalAccountIdsIn()
444c0a0e
C
500 },
501 // Do not delete Tombstones
502 deletedAt: null
6b9c966f 503 }
2ba92871
C
504 }
505
506 return VideoCommentModel.destroy(query)
507 }
508
cef534ed
C
509 getCommentStaticPath () {
510 return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId()
511 }
512
d7e70384
C
513 getThreadId (): number {
514 return this.originCommentId || this.id
515 }
516
4cb6d457 517 isOwned () {
cde3d90d 518 if (!this.Account) return false
69222afa 519
4cb6d457
C
520 return this.Account.isOwned()
521 }
522
eae0365b
C
523 markAsDeleted () {
524 this.text = ''
525 this.deletedAt = new Date()
526 this.accountId = null
527 }
528
69222afa 529 isDeleted () {
a1587156 530 return this.deletedAt !== null
69222afa
JM
531 }
532
f7cc67b4 533 extractMentions () {
1f6d57e3 534 let result: string[] = []
f7cc67b4
C
535
536 const localMention = `@(${actorNameAlphabet}+)`
6dd9de95 537 const remoteMention = `${localMention}@${WEBSERVER.HOST}`
f7cc67b4 538
1f6d57e3
C
539 const mentionRegex = this.isOwned()
540 ? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions?
541 : '(?:' + remoteMention + ')'
542
543 const firstMentionRegex = new RegExp(`^${mentionRegex} `, 'g')
544 const endMentionRegex = new RegExp(` ${mentionRegex}$`, 'g')
f7cc67b4 545 const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g')
f7cc67b4 546
1f6d57e3
C
547 result = result.concat(
548 regexpCapture(this.text, firstMentionRegex)
549 .map(([ , username1, username2 ]) => username1 || username2),
f7cc67b4 550
1f6d57e3
C
551 regexpCapture(this.text, endMentionRegex)
552 .map(([ , username1, username2 ]) => username1 || username2),
553
554 regexpCapture(this.text, remoteMentionsRegex)
555 .map(([ , username ]) => username)
556 )
f7cc67b4 557
1f6d57e3
C
558 // Include local mentions
559 if (this.isOwned()) {
560 const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g')
f7cc67b4 561
1f6d57e3
C
562 result = result.concat(
563 regexpCapture(this.text, localMentionsRegex)
564 .map(([ , username ]) => username)
f7cc67b4 565 )
1f6d57e3
C
566 }
567
690bb8f9 568 return uniqify(result)
f7cc67b4
C
569 }
570
1ca9f7c3 571 toFormattedJSON (this: MCommentFormattable) {
bf1f6508
C
572 return {
573 id: this.id,
574 url: this.url,
575 text: this.text,
0f8d00e3 576
8ca56654 577 threadId: this.getThreadId(),
d50acfab 578 inReplyToCommentId: this.inReplyToCommentId || null,
bf1f6508 579 videoId: this.videoId,
0f8d00e3 580
bf1f6508 581 createdAt: this.createdAt,
d3ea8975 582 updatedAt: this.updatedAt,
69222afa 583 deletedAt: this.deletedAt,
0f8d00e3 584
69222afa 585 isDeleted: this.isDeleted(),
0f8d00e3 586
5b0413dd 587 totalRepliesFromVideoAuthor: this.get('totalRepliesFromVideoAuthor') || 0,
4635f59d 588 totalReplies: this.get('totalReplies') || 0,
0f8d00e3
C
589
590 account: this.Account
591 ? this.Account.toFormattedJSON()
592 : null
bf1f6508
C
593 } as VideoComment
594 }
ea44f375 595
0f8d00e3
C
596 toFormattedAdminJSON (this: MCommentAdminFormattable) {
597 return {
598 id: this.id,
599 url: this.url,
600 text: this.text,
601
602 threadId: this.getThreadId(),
603 inReplyToCommentId: this.inReplyToCommentId || null,
604 videoId: this.videoId,
605
606 createdAt: this.createdAt,
607 updatedAt: this.updatedAt,
608
609 video: {
610 id: this.Video.id,
611 uuid: this.Video.uuid,
612 name: this.Video.name
613 },
614
615 account: this.Account
616 ? this.Account.toFormattedJSON()
617 : null
618 } as VideoCommentAdmin
619 }
620
69222afa 621 toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject | ActivityTombstoneObject {
b5206dfc
JM
622 let inReplyTo: string
623 // New thread, so in AS we reply to the video
624 if (this.inReplyToCommentId === null) {
625 inReplyTo = this.Video.url
626 } else {
627 inReplyTo = this.InReplyToVideoComment.url
628 }
629
69222afa
JM
630 if (this.isDeleted()) {
631 return {
632 id: this.url,
633 type: 'Tombstone',
634 formerType: 'Note',
b5206dfc 635 inReplyTo,
69222afa
JM
636 published: this.createdAt.toISOString(),
637 updated: this.updatedAt.toISOString(),
638 deleted: this.deletedAt.toISOString()
639 }
640 }
641
d7e70384
C
642 const tag: ActivityTagObject[] = []
643 for (const parentComment of threadParentComments) {
b5206dfc
JM
644 if (!parentComment.Account) continue
645
d7e70384
C
646 const actor = parentComment.Account.Actor
647
648 tag.push({
649 type: 'Mention',
650 href: actor.url,
651 name: `@${actor.preferredUsername}@${actor.getHost()}`
652 })
653 }
654
ea44f375
C
655 return {
656 type: 'Note' as 'Note',
657 id: this.url,
3726c372 658
ea44f375 659 content: this.text,
3726c372
C
660 mediaType: 'text/markdown',
661
ea44f375 662 inReplyTo,
da854ddd 663 updated: this.updatedAt.toISOString(),
ea44f375 664 published: this.createdAt.toISOString(),
da854ddd 665 url: this.url,
d7e70384
C
666 attributedTo: this.Account.Actor.url,
667 tag
ea44f375
C
668 }
669 }
696d83fd
C
670
671 private static async buildBlockerAccountIds (options: {
cde3d90d
C
672 user: MUserAccountId
673 }): Promise<number[]> {
674 const { user } = options
696d83fd
C
675
676 const serverActor = await getServerActor()
677 const blockerAccountIds = [ serverActor.Account.id ]
678
679 if (user) blockerAccountIds.push(user.Account.id)
680
696d83fd
C
681 return blockerAccountIds
682 }
6d852470 683}