]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/models/video/video-comment.ts
Merge branch 'feature/SO035' into develop
[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
C
42import { ActorModel } from '../actor/actor'
43import { buildLocalAccountIdsIn, throwIfNotValid } from '../utils'
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
b49f22d8 195 static loadById (id: number, t?: Transaction): Promise<MComment> {
1735c825 196 const query: FindOptions = {
bf1f6508
C
197 where: {
198 id
199 }
200 }
201
202 if (t !== undefined) query.transaction = t
203
204 return VideoCommentModel.findOne(query)
205 }
206
b49f22d8 207 static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Transaction): Promise<MCommentOwnerVideoReply> {
1735c825 208 const query: FindOptions = {
da854ddd
C
209 where: {
210 id
211 }
212 }
213
214 if (t !== undefined) query.transaction = t
215
216 return VideoCommentModel
217 .scope([ ScopeNames.WITH_VIDEO, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_IN_REPLY_TO ])
218 .findOne(query)
219 }
220
b49f22d8 221 static loadByUrlAndPopulateAccountAndVideo (url: string, t?: Transaction): Promise<MCommentOwnerVideo> {
1735c825 222 const query: FindOptions = {
6d852470
C
223 where: {
224 url
225 }
226 }
227
228 if (t !== undefined) query.transaction = t
229
511765c9 230 return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEO ]).findOne(query)
6d852470 231 }
bf1f6508 232
b49f22d8 233 static loadByUrlAndPopulateReplyAndVideoUrlAndAccount (url: string, t?: Transaction): Promise<MCommentOwnerReplyVideoLight> {
1735c825 234 const query: FindOptions = {
4cb6d457
C
235 where: {
236 url
6b9c966f
C
237 },
238 include: [
239 {
240 attributes: [ 'id', 'url' ],
241 model: VideoModel.unscoped()
242 }
243 ]
4cb6d457
C
244 }
245
246 if (t !== undefined) query.transaction = t
247
6b9c966f 248 return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_ACCOUNT ]).findOne(query)
4cb6d457
C
249 }
250
0f8d00e3
C
251 static listCommentsForApi (parameters: {
252 start: number
253 count: number
254 sort: string
255
0e6cd1c0 256 onLocalVideo?: boolean
0f8d00e3
C
257 isLocal?: boolean
258 search?: string
259 searchAccount?: string
260 searchVideo?: string
261 }) {
cde3d90d
C
262 const queryOptions: ListVideoCommentsOptions = {
263 ...pick(parameters, [ 'start', 'count', 'sort', 'isLocal', 'search', 'searchVideo', 'searchAccount', 'onLocalVideo' ]),
0f8d00e3 264
cde3d90d
C
265 selectType: 'api',
266 notDeleted: true
f1273314 267 }
0f8d00e3 268
d0800f76 269 return Promise.all([
cde3d90d
C
270 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(),
271 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
272 ]).then(([ rows, count ]) => {
273 return { total: count, data: rows }
274 })
0f8d00e3
C
275 }
276
b4055e1c 277 static async listThreadsForApi (parameters: {
a1587156 278 videoId: number
696d83fd 279 isVideoOwned: boolean
a1587156
C
280 start: number
281 count: number
282 sort: string
453e83ea 283 user?: MUserAccountId
b4055e1c 284 }) {
cde3d90d 285 const { videoId, user } = parameters
b4055e1c 286
cde3d90d 287 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user })
7ad9b984 288
cde3d90d
C
289 const commonOptions: ListVideoCommentsOptions = {
290 selectType: 'api',
291 videoId,
292 blockerAccountIds
9d6b9d10
C
293 }
294
cde3d90d
C
295 const listOptions: ListVideoCommentsOptions = {
296 ...commonOptions,
297 ...pick(parameters, [ 'sort', 'start', 'count' ]),
298
299 isThread: true,
300 includeReplyCounters: true
bf1f6508
C
301 }
302
cde3d90d
C
303 const countOptions: ListVideoCommentsOptions = {
304 ...commonOptions,
7ad9b984 305
cde3d90d
C
306 isThread: true
307 }
d0800f76 308
cde3d90d
C
309 const notDeletedCountOptions: ListVideoCommentsOptions = {
310 ...commonOptions,
311
312 notDeleted: true
9d6b9d10
C
313 }
314
315 return Promise.all([
cde3d90d
C
316 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, listOptions).listComments<MCommentAdminFormattable>(),
317 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, countOptions).countComments(),
318 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, notDeletedCountOptions).countComments()
d0800f76 319 ]).then(([ rows, count, totalNotDeletedComments ]) => {
9d6b9d10
C
320 return { total: count, data: rows, totalNotDeletedComments }
321 })
bf1f6508
C
322 }
323
b4055e1c 324 static async listThreadCommentsForApi (parameters: {
a1587156
C
325 videoId: number
326 threadId: number
453e83ea 327 user?: MUserAccountId
b4055e1c 328 }) {
cde3d90d 329 const { user } = parameters
b4055e1c 330
cde3d90d 331 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user })
7ad9b984 332
cde3d90d
C
333 const queryOptions: ListVideoCommentsOptions = {
334 ...pick(parameters, [ 'videoId', 'threadId' ]),
bf1f6508 335
cde3d90d
C
336 selectType: 'api',
337 sort: 'createdAt',
338
339 blockerAccountIds,
340 includeReplyCounters: true
341 }
7ad9b984 342
d0800f76 343 return Promise.all([
cde3d90d
C
344 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(),
345 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
346 ]).then(([ rows, count ]) => {
347 return { total: count, data: rows }
348 })
bf1f6508
C
349 }
350
b49f22d8 351 static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> {
d7e70384 352 const query = {
1735c825 353 order: [ [ 'createdAt', order ] ] as Order,
d7e70384 354 where: {
d7e70384 355 id: {
a1587156 356 [Op.in]: Sequelize.literal('(' +
a3cffab4 357 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' +
f7cc67b4
C
358 `SELECT id, "inReplyToCommentId" FROM "videoComment" WHERE id = ${comment.id} ` +
359 'UNION ' +
360 'SELECT "parent"."id", "parent"."inReplyToCommentId" FROM "videoComment" "parent" ' +
361 'INNER JOIN "children" ON "children"."inReplyToCommentId" = "parent"."id"' +
362 ') ' +
a3cffab4
C
363 'SELECT id FROM children' +
364 ')'),
a1587156 365 [Op.ne]: comment.id
d7e70384
C
366 }
367 },
368 transaction: t
369 }
370
371 return VideoCommentModel
372 .scope([ ScopeNames.WITH_ACCOUNT ])
373 .findAll(query)
374 }
375
cde3d90d
C
376 static async listAndCountByVideoForAP (parameters: {
377 video: MVideoImmutable
378 start: number
379 count: number
380 }) {
381 const { video } = parameters
382
383 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null })
384
385 const queryOptions: ListVideoCommentsOptions = {
386 ...pick(parameters, [ 'start', 'count' ]),
387
388 selectType: 'comment-only',
696d83fd 389 videoId: video.id,
cde3d90d 390 sort: 'createdAt',
696d83fd 391
cde3d90d 392 blockerAccountIds
8fffe21a
C
393 }
394
d0800f76 395 return Promise.all([
cde3d90d
C
396 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>(),
397 new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments()
398 ]).then(([ rows, count ]) => {
399 return { total: count, data: rows }
400 })
8fffe21a
C
401 }
402
00494d6e
RK
403 static async listForFeed (parameters: {
404 start: number
405 count: number
406 videoId?: number
407 accountId?: number
408 videoChannelId?: number
cde3d90d
C
409 }) {
410 const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null })
00494d6e 411
cde3d90d
C
412 const queryOptions: ListVideoCommentsOptions = {
413 ...pick(parameters, [ 'start', 'count', 'accountId', 'videoId', 'videoChannelId' ]),
1c58423f 414
cde3d90d 415 selectType: 'feed',
1c58423f 416
cde3d90d
C
417 sort: '-createdAt',
418 onPublicVideo: true,
419 notDeleted: true,
1df8a4d7 420
cde3d90d 421 blockerAccountIds
fe3a55b0
C
422 }
423
cde3d90d 424 return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentOwnerVideoFeed>()
fe3a55b0
C
425 }
426
444c0a0e 427 static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) {
cde3d90d
C
428 const queryOptions: ListVideoCommentsOptions = {
429 selectType: 'comment-only',
444c0a0e 430
cde3d90d
C
431 accountId: ofAccount.id,
432 videoAccountOwnerId: filter.onVideosOfAccount?.id,
433
434 notDeleted: true,
435 count: 5000
444c0a0e
C
436 }
437
cde3d90d 438 return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>()
444c0a0e
C
439 }
440
09cababd
C
441 static async getStats () {
442 const totalLocalVideoComments = await VideoCommentModel.count({
443 include: [
444 {
5e0dbb3e 445 model: AccountModel.unscoped(),
09cababd
C
446 required: true,
447 include: [
448 {
5e0dbb3e 449 model: ActorModel.unscoped(),
09cababd
C
450 required: true,
451 where: {
452 serverId: null
453 }
454 }
455 ]
456 }
457 ]
458 })
459 const totalVideoComments = await VideoCommentModel.count()
460
461 return {
462 totalLocalVideoComments,
463 totalVideoComments
464 }
465 }
466
74d249bc
C
467 static listRemoteCommentUrlsOfLocalVideos () {
468 const query = `SELECT "videoComment".url FROM "videoComment" ` +
469 `INNER JOIN account ON account.id = "videoComment"."accountId" ` +
470 `INNER JOIN actor ON actor.id = "account"."actorId" AND actor."serverId" IS NOT NULL ` +
471 `INNER JOIN video ON video.id = "videoComment"."videoId" AND video.remote IS FALSE`
472
473 return VideoCommentModel.sequelize.query<{ url: string }>(query, {
474 type: QueryTypes.SELECT,
475 raw: true
476 }).then(rows => rows.map(r => r.url))
477 }
478
2ba92871
C
479 static cleanOldCommentsOf (videoId: number, beforeUpdatedAt: Date) {
480 const query = {
481 where: {
482 updatedAt: {
1735c825 483 [Op.lt]: beforeUpdatedAt
2ba92871 484 },
6b9c966f
C
485 videoId,
486 accountId: {
487 [Op.notIn]: buildLocalAccountIdsIn()
444c0a0e
C
488 },
489 // Do not delete Tombstones
490 deletedAt: null
6b9c966f 491 }
2ba92871
C
492 }
493
494 return VideoCommentModel.destroy(query)
495 }
496
cef534ed
C
497 getCommentStaticPath () {
498 return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId()
499 }
500
d7e70384
C
501 getThreadId (): number {
502 return this.originCommentId || this.id
503 }
504
4cb6d457 505 isOwned () {
cde3d90d 506 if (!this.Account) return false
69222afa 507
4cb6d457
C
508 return this.Account.isOwned()
509 }
510
eae0365b
C
511 markAsDeleted () {
512 this.text = ''
513 this.deletedAt = new Date()
514 this.accountId = null
515 }
516
69222afa 517 isDeleted () {
a1587156 518 return this.deletedAt !== null
69222afa
JM
519 }
520
f7cc67b4 521 extractMentions () {
1f6d57e3 522 let result: string[] = []
f7cc67b4
C
523
524 const localMention = `@(${actorNameAlphabet}+)`
6dd9de95 525 const remoteMention = `${localMention}@${WEBSERVER.HOST}`
f7cc67b4 526
1f6d57e3
C
527 const mentionRegex = this.isOwned()
528 ? '(?:(?:' + remoteMention + ')|(?:' + localMention + '))' // Include local mentions?
529 : '(?:' + remoteMention + ')'
530
531 const firstMentionRegex = new RegExp(`^${mentionRegex} `, 'g')
532 const endMentionRegex = new RegExp(` ${mentionRegex}$`, 'g')
f7cc67b4 533 const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g')
f7cc67b4 534
1f6d57e3
C
535 result = result.concat(
536 regexpCapture(this.text, firstMentionRegex)
537 .map(([ , username1, username2 ]) => username1 || username2),
f7cc67b4 538
1f6d57e3
C
539 regexpCapture(this.text, endMentionRegex)
540 .map(([ , username1, username2 ]) => username1 || username2),
541
542 regexpCapture(this.text, remoteMentionsRegex)
543 .map(([ , username ]) => username)
544 )
f7cc67b4 545
1f6d57e3
C
546 // Include local mentions
547 if (this.isOwned()) {
548 const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g')
f7cc67b4 549
1f6d57e3
C
550 result = result.concat(
551 regexpCapture(this.text, localMentionsRegex)
552 .map(([ , username ]) => username)
f7cc67b4 553 )
1f6d57e3
C
554 }
555
690bb8f9 556 return uniqify(result)
f7cc67b4
C
557 }
558
1ca9f7c3 559 toFormattedJSON (this: MCommentFormattable) {
bf1f6508
C
560 return {
561 id: this.id,
562 url: this.url,
563 text: this.text,
0f8d00e3 564
8ca56654 565 threadId: this.getThreadId(),
d50acfab 566 inReplyToCommentId: this.inReplyToCommentId || null,
bf1f6508 567 videoId: this.videoId,
0f8d00e3 568
bf1f6508 569 createdAt: this.createdAt,
d3ea8975 570 updatedAt: this.updatedAt,
69222afa 571 deletedAt: this.deletedAt,
0f8d00e3 572
69222afa 573 isDeleted: this.isDeleted(),
0f8d00e3 574
5b0413dd 575 totalRepliesFromVideoAuthor: this.get('totalRepliesFromVideoAuthor') || 0,
4635f59d 576 totalReplies: this.get('totalReplies') || 0,
0f8d00e3
C
577
578 account: this.Account
579 ? this.Account.toFormattedJSON()
580 : null
bf1f6508
C
581 } as VideoComment
582 }
ea44f375 583
0f8d00e3
C
584 toFormattedAdminJSON (this: MCommentAdminFormattable) {
585 return {
586 id: this.id,
587 url: this.url,
588 text: this.text,
589
590 threadId: this.getThreadId(),
591 inReplyToCommentId: this.inReplyToCommentId || null,
592 videoId: this.videoId,
593
594 createdAt: this.createdAt,
595 updatedAt: this.updatedAt,
596
597 video: {
598 id: this.Video.id,
599 uuid: this.Video.uuid,
600 name: this.Video.name
601 },
602
603 account: this.Account
604 ? this.Account.toFormattedJSON()
605 : null
606 } as VideoCommentAdmin
607 }
608
69222afa 609 toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject | ActivityTombstoneObject {
b5206dfc
JM
610 let inReplyTo: string
611 // New thread, so in AS we reply to the video
612 if (this.inReplyToCommentId === null) {
613 inReplyTo = this.Video.url
614 } else {
615 inReplyTo = this.InReplyToVideoComment.url
616 }
617
69222afa
JM
618 if (this.isDeleted()) {
619 return {
620 id: this.url,
621 type: 'Tombstone',
622 formerType: 'Note',
b5206dfc 623 inReplyTo,
69222afa
JM
624 published: this.createdAt.toISOString(),
625 updated: this.updatedAt.toISOString(),
626 deleted: this.deletedAt.toISOString()
627 }
628 }
629
d7e70384
C
630 const tag: ActivityTagObject[] = []
631 for (const parentComment of threadParentComments) {
b5206dfc
JM
632 if (!parentComment.Account) continue
633
d7e70384
C
634 const actor = parentComment.Account.Actor
635
636 tag.push({
637 type: 'Mention',
638 href: actor.url,
639 name: `@${actor.preferredUsername}@${actor.getHost()}`
640 })
641 }
642
ea44f375
C
643 return {
644 type: 'Note' as 'Note',
645 id: this.url,
3726c372 646
ea44f375 647 content: this.text,
3726c372
C
648 mediaType: 'text/markdown',
649
ea44f375 650 inReplyTo,
da854ddd 651 updated: this.updatedAt.toISOString(),
ea44f375 652 published: this.createdAt.toISOString(),
da854ddd 653 url: this.url,
d7e70384
C
654 attributedTo: this.Account.Actor.url,
655 tag
ea44f375
C
656 }
657 }
696d83fd
C
658
659 private static async buildBlockerAccountIds (options: {
cde3d90d
C
660 user: MUserAccountId
661 }): Promise<number[]> {
662 const { user } = options
696d83fd
C
663
664 const serverActor = await getServerActor()
665 const blockerAccountIds = [ serverActor.Account.id ]
666
667 if (user) blockerAccountIds.push(user.Account.id)
668
696d83fd
C
669 return blockerAccountIds
670 }
6d852470 671}