diff options
Diffstat (limited to 'server/models/video/video-comment.ts')
-rw-r--r-- | server/models/video/video-comment.ts | 683 |
1 files changed, 0 insertions, 683 deletions
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts deleted file mode 100644 index ff5142809..000000000 --- a/server/models/video/video-comment.ts +++ /dev/null | |||
@@ -1,683 +0,0 @@ | |||
1 | import { FindOptions, Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize' | ||
2 | import { | ||
3 | AllowNull, | ||
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' | ||
16 | import { getServerActor } from '@server/models/application/application' | ||
17 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | ||
18 | import { pick, uniqify } from '@shared/core-utils' | ||
19 | import { AttributesOnly } from '@shared/typescript-utils' | ||
20 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | ||
21 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | ||
22 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' | ||
23 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' | ||
24 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | ||
25 | import { regexpCapture } from '../../helpers/regexp' | ||
26 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | ||
27 | import { | ||
28 | MComment, | ||
29 | MCommentAdminFormattable, | ||
30 | MCommentAP, | ||
31 | MCommentFormattable, | ||
32 | MCommentId, | ||
33 | MCommentOwner, | ||
34 | MCommentOwnerReplyVideoLight, | ||
35 | MCommentOwnerVideo, | ||
36 | MCommentOwnerVideoFeed, | ||
37 | MCommentOwnerVideoReply, | ||
38 | MVideoImmutable | ||
39 | } from '../../types/models/video' | ||
40 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | ||
41 | import { AccountModel } from '../account/account' | ||
42 | import { ActorModel } from '../actor/actor' | ||
43 | import { buildLocalAccountIdsIn, buildSQLAttributes, throwIfNotValid } from '../shared' | ||
44 | import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder' | ||
45 | import { VideoModel } from './video' | ||
46 | import { VideoChannelModel } from './video-channel' | ||
47 | |||
48 | export enum ScopeNames { | ||
49 | WITH_ACCOUNT = 'WITH_ACCOUNT', | ||
50 | WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', | ||
51 | WITH_VIDEO = 'WITH_VIDEO' | ||
52 | } | ||
53 | |||
54 | @Scopes(() => ({ | ||
55 | [ScopeNames.WITH_ACCOUNT]: { | ||
56 | include: [ | ||
57 | { | ||
58 | model: AccountModel | ||
59 | } | ||
60 | ] | ||
61 | }, | ||
62 | [ScopeNames.WITH_IN_REPLY_TO]: { | ||
63 | include: [ | ||
64 | { | ||
65 | model: VideoCommentModel, | ||
66 | as: 'InReplyToVideoComment' | ||
67 | } | ||
68 | ] | ||
69 | }, | ||
70 | [ScopeNames.WITH_VIDEO]: { | ||
71 | include: [ | ||
72 | { | ||
73 | model: VideoModel, | ||
74 | required: true, | ||
75 | include: [ | ||
76 | { | ||
77 | model: VideoChannelModel, | ||
78 | required: true, | ||
79 | include: [ | ||
80 | { | ||
81 | model: AccountModel, | ||
82 | required: true | ||
83 | } | ||
84 | ] | ||
85 | } | ||
86 | ] | ||
87 | } | ||
88 | ] | ||
89 | } | ||
90 | })) | ||
91 | @Table({ | ||
92 | tableName: 'videoComment', | ||
93 | indexes: [ | ||
94 | { | ||
95 | fields: [ 'videoId' ] | ||
96 | }, | ||
97 | { | ||
98 | fields: [ 'videoId', 'originCommentId' ] | ||
99 | }, | ||
100 | { | ||
101 | fields: [ 'url' ], | ||
102 | unique: true | ||
103 | }, | ||
104 | { | ||
105 | fields: [ 'accountId' ] | ||
106 | }, | ||
107 | { | ||
108 | fields: [ | ||
109 | { name: 'createdAt', order: 'DESC' } | ||
110 | ] | ||
111 | } | ||
112 | ] | ||
113 | }) | ||
114 | export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoCommentModel>>> { | ||
115 | @CreatedAt | ||
116 | createdAt: Date | ||
117 | |||
118 | @UpdatedAt | ||
119 | updatedAt: Date | ||
120 | |||
121 | @AllowNull(true) | ||
122 | @Column(DataType.DATE) | ||
123 | deletedAt: Date | ||
124 | |||
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: { | ||
140 | name: 'originCommentId', | ||
141 | allowNull: true | ||
142 | }, | ||
143 | as: 'OriginVideoComment', | ||
144 | onDelete: 'CASCADE' | ||
145 | }) | ||
146 | OriginVideoComment: VideoCommentModel | ||
147 | |||
148 | @ForeignKey(() => VideoCommentModel) | ||
149 | @Column | ||
150 | inReplyToCommentId: number | ||
151 | |||
152 | @BelongsTo(() => VideoCommentModel, { | ||
153 | foreignKey: { | ||
154 | name: 'inReplyToCommentId', | ||
155 | allowNull: true | ||
156 | }, | ||
157 | as: 'InReplyToVideoComment', | ||
158 | onDelete: 'CASCADE' | ||
159 | }) | ||
160 | InReplyToVideoComment: VideoCommentModel | null | ||
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 | |||
174 | @ForeignKey(() => AccountModel) | ||
175 | @Column | ||
176 | accountId: number | ||
177 | |||
178 | @BelongsTo(() => AccountModel, { | ||
179 | foreignKey: { | ||
180 | allowNull: true | ||
181 | }, | ||
182 | onDelete: 'CASCADE' | ||
183 | }) | ||
184 | Account: AccountModel | ||
185 | |||
186 | @HasMany(() => VideoCommentAbuseModel, { | ||
187 | foreignKey: { | ||
188 | name: 'videoCommentId', | ||
189 | allowNull: true | ||
190 | }, | ||
191 | onDelete: 'set null' | ||
192 | }) | ||
193 | CommentAbuses: VideoCommentAbuseModel[] | ||
194 | |||
195 | // --------------------------------------------------------------------------- | ||
196 | |||
197 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
198 | return buildSQLAttributes({ | ||
199 | model: this, | ||
200 | tableName, | ||
201 | aliasPrefix | ||
202 | }) | ||
203 | } | ||
204 | |||
205 | // --------------------------------------------------------------------------- | ||
206 | |||
207 | static loadById (id: number, t?: Transaction): Promise<MComment> { | ||
208 | const query: FindOptions = { | ||
209 | where: { | ||
210 | id | ||
211 | } | ||
212 | } | ||
213 | |||
214 | if (t !== undefined) query.transaction = t | ||
215 | |||
216 | return VideoCommentModel.findOne(query) | ||
217 | } | ||
218 | |||
219 | static loadByIdAndPopulateVideoAndAccountAndReply (id: number, t?: Transaction): Promise<MCommentOwnerVideoReply> { | ||
220 | const query: FindOptions = { | ||
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 | |||
233 | static loadByUrlAndPopulateAccountAndVideo (url: string, t?: Transaction): Promise<MCommentOwnerVideo> { | ||
234 | const query: FindOptions = { | ||
235 | where: { | ||
236 | url | ||
237 | } | ||
238 | } | ||
239 | |||
240 | if (t !== undefined) query.transaction = t | ||
241 | |||
242 | return VideoCommentModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEO ]).findOne(query) | ||
243 | } | ||
244 | |||
245 | static loadByUrlAndPopulateReplyAndVideoUrlAndAccount (url: string, t?: Transaction): Promise<MCommentOwnerReplyVideoLight> { | ||
246 | const query: FindOptions = { | ||
247 | where: { | ||
248 | url | ||
249 | }, | ||
250 | include: [ | ||
251 | { | ||
252 | attributes: [ 'id', 'url' ], | ||
253 | model: VideoModel.unscoped() | ||
254 | } | ||
255 | ] | ||
256 | } | ||
257 | |||
258 | if (t !== undefined) query.transaction = t | ||
259 | |||
260 | return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_ACCOUNT ]).findOne(query) | ||
261 | } | ||
262 | |||
263 | static listCommentsForApi (parameters: { | ||
264 | start: number | ||
265 | count: number | ||
266 | sort: string | ||
267 | |||
268 | onLocalVideo?: boolean | ||
269 | isLocal?: boolean | ||
270 | search?: string | ||
271 | searchAccount?: string | ||
272 | searchVideo?: string | ||
273 | }) { | ||
274 | const queryOptions: ListVideoCommentsOptions = { | ||
275 | ...pick(parameters, [ 'start', 'count', 'sort', 'isLocal', 'search', 'searchVideo', 'searchAccount', 'onLocalVideo' ]), | ||
276 | |||
277 | selectType: 'api', | ||
278 | notDeleted: true | ||
279 | } | ||
280 | |||
281 | return Promise.all([ | ||
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 | }) | ||
287 | } | ||
288 | |||
289 | static async listThreadsForApi (parameters: { | ||
290 | videoId: number | ||
291 | isVideoOwned: boolean | ||
292 | start: number | ||
293 | count: number | ||
294 | sort: string | ||
295 | user?: MUserAccountId | ||
296 | }) { | ||
297 | const { videoId, user } = parameters | ||
298 | |||
299 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user }) | ||
300 | |||
301 | const commonOptions: ListVideoCommentsOptions = { | ||
302 | selectType: 'api', | ||
303 | videoId, | ||
304 | blockerAccountIds | ||
305 | } | ||
306 | |||
307 | const listOptions: ListVideoCommentsOptions = { | ||
308 | ...commonOptions, | ||
309 | ...pick(parameters, [ 'sort', 'start', 'count' ]), | ||
310 | |||
311 | isThread: true, | ||
312 | includeReplyCounters: true | ||
313 | } | ||
314 | |||
315 | const countOptions: ListVideoCommentsOptions = { | ||
316 | ...commonOptions, | ||
317 | |||
318 | isThread: true | ||
319 | } | ||
320 | |||
321 | const notDeletedCountOptions: ListVideoCommentsOptions = { | ||
322 | ...commonOptions, | ||
323 | |||
324 | notDeleted: true | ||
325 | } | ||
326 | |||
327 | return Promise.all([ | ||
328 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, listOptions).listComments<MCommentAdminFormattable>(), | ||
329 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, countOptions).countComments(), | ||
330 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, notDeletedCountOptions).countComments() | ||
331 | ]).then(([ rows, count, totalNotDeletedComments ]) => { | ||
332 | return { total: count, data: rows, totalNotDeletedComments } | ||
333 | }) | ||
334 | } | ||
335 | |||
336 | static async listThreadCommentsForApi (parameters: { | ||
337 | videoId: number | ||
338 | threadId: number | ||
339 | user?: MUserAccountId | ||
340 | }) { | ||
341 | const { user } = parameters | ||
342 | |||
343 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user }) | ||
344 | |||
345 | const queryOptions: ListVideoCommentsOptions = { | ||
346 | ...pick(parameters, [ 'videoId', 'threadId' ]), | ||
347 | |||
348 | selectType: 'api', | ||
349 | sort: 'createdAt', | ||
350 | |||
351 | blockerAccountIds, | ||
352 | includeReplyCounters: true | ||
353 | } | ||
354 | |||
355 | return Promise.all([ | ||
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 | }) | ||
361 | } | ||
362 | |||
363 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { | ||
364 | const query = { | ||
365 | order: [ [ 'createdAt', order ] ] as Order, | ||
366 | where: { | ||
367 | id: { | ||
368 | [Op.in]: Sequelize.literal('(' + | ||
369 | 'WITH RECURSIVE children (id, "inReplyToCommentId") AS ( ' + | ||
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 | ') ' + | ||
375 | 'SELECT id FROM children' + | ||
376 | ')'), | ||
377 | [Op.ne]: comment.id | ||
378 | } | ||
379 | }, | ||
380 | transaction: t | ||
381 | } | ||
382 | |||
383 | return VideoCommentModel | ||
384 | .scope([ ScopeNames.WITH_ACCOUNT ]) | ||
385 | .findAll(query) | ||
386 | } | ||
387 | |||
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', | ||
401 | videoId: video.id, | ||
402 | sort: 'createdAt', | ||
403 | |||
404 | blockerAccountIds | ||
405 | } | ||
406 | |||
407 | return Promise.all([ | ||
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 | }) | ||
413 | } | ||
414 | |||
415 | static async listForFeed (parameters: { | ||
416 | start: number | ||
417 | count: number | ||
418 | videoId?: number | ||
419 | accountId?: number | ||
420 | videoChannelId?: number | ||
421 | }) { | ||
422 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null }) | ||
423 | |||
424 | const queryOptions: ListVideoCommentsOptions = { | ||
425 | ...pick(parameters, [ 'start', 'count', 'accountId', 'videoId', 'videoChannelId' ]), | ||
426 | |||
427 | selectType: 'feed', | ||
428 | |||
429 | sort: '-createdAt', | ||
430 | onPublicVideo: true, | ||
431 | notDeleted: true, | ||
432 | |||
433 | blockerAccountIds | ||
434 | } | ||
435 | |||
436 | return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentOwnerVideoFeed>() | ||
437 | } | ||
438 | |||
439 | static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { | ||
440 | const queryOptions: ListVideoCommentsOptions = { | ||
441 | selectType: 'comment-only', | ||
442 | |||
443 | accountId: ofAccount.id, | ||
444 | videoAccountOwnerId: filter.onVideosOfAccount?.id, | ||
445 | |||
446 | notDeleted: true, | ||
447 | count: 5000 | ||
448 | } | ||
449 | |||
450 | return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>() | ||
451 | } | ||
452 | |||
453 | static async getStats () { | ||
454 | const totalLocalVideoComments = await VideoCommentModel.count({ | ||
455 | include: [ | ||
456 | { | ||
457 | model: AccountModel.unscoped(), | ||
458 | required: true, | ||
459 | include: [ | ||
460 | { | ||
461 | model: ActorModel.unscoped(), | ||
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 | |||
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 | |||
491 | static cleanOldCommentsOf (videoId: number, beforeUpdatedAt: Date) { | ||
492 | const query = { | ||
493 | where: { | ||
494 | updatedAt: { | ||
495 | [Op.lt]: beforeUpdatedAt | ||
496 | }, | ||
497 | videoId, | ||
498 | accountId: { | ||
499 | [Op.notIn]: buildLocalAccountIdsIn() | ||
500 | }, | ||
501 | // Do not delete Tombstones | ||
502 | deletedAt: null | ||
503 | } | ||
504 | } | ||
505 | |||
506 | return VideoCommentModel.destroy(query) | ||
507 | } | ||
508 | |||
509 | getCommentStaticPath () { | ||
510 | return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId() | ||
511 | } | ||
512 | |||
513 | getThreadId (): number { | ||
514 | return this.originCommentId || this.id | ||
515 | } | ||
516 | |||
517 | isOwned () { | ||
518 | if (!this.Account) return false | ||
519 | |||
520 | return this.Account.isOwned() | ||
521 | } | ||
522 | |||
523 | markAsDeleted () { | ||
524 | this.text = '' | ||
525 | this.deletedAt = new Date() | ||
526 | this.accountId = null | ||
527 | } | ||
528 | |||
529 | isDeleted () { | ||
530 | return this.deletedAt !== null | ||
531 | } | ||
532 | |||
533 | extractMentions () { | ||
534 | let result: string[] = [] | ||
535 | |||
536 | const localMention = `@(${actorNameAlphabet}+)` | ||
537 | const remoteMention = `${localMention}@${WEBSERVER.HOST}` | ||
538 | |||
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') | ||
545 | const remoteMentionsRegex = new RegExp(' ' + remoteMention + ' ', 'g') | ||
546 | |||
547 | result = result.concat( | ||
548 | regexpCapture(this.text, firstMentionRegex) | ||
549 | .map(([ , username1, username2 ]) => username1 || username2), | ||
550 | |||
551 | regexpCapture(this.text, endMentionRegex) | ||
552 | .map(([ , username1, username2 ]) => username1 || username2), | ||
553 | |||
554 | regexpCapture(this.text, remoteMentionsRegex) | ||
555 | .map(([ , username ]) => username) | ||
556 | ) | ||
557 | |||
558 | // Include local mentions | ||
559 | if (this.isOwned()) { | ||
560 | const localMentionsRegex = new RegExp(' ' + localMention + ' ', 'g') | ||
561 | |||
562 | result = result.concat( | ||
563 | regexpCapture(this.text, localMentionsRegex) | ||
564 | .map(([ , username ]) => username) | ||
565 | ) | ||
566 | } | ||
567 | |||
568 | return uniqify(result) | ||
569 | } | ||
570 | |||
571 | toFormattedJSON (this: MCommentFormattable) { | ||
572 | return { | ||
573 | id: this.id, | ||
574 | url: this.url, | ||
575 | text: this.text, | ||
576 | |||
577 | threadId: this.getThreadId(), | ||
578 | inReplyToCommentId: this.inReplyToCommentId || null, | ||
579 | videoId: this.videoId, | ||
580 | |||
581 | createdAt: this.createdAt, | ||
582 | updatedAt: this.updatedAt, | ||
583 | deletedAt: this.deletedAt, | ||
584 | |||
585 | isDeleted: this.isDeleted(), | ||
586 | |||
587 | totalRepliesFromVideoAuthor: this.get('totalRepliesFromVideoAuthor') || 0, | ||
588 | totalReplies: this.get('totalReplies') || 0, | ||
589 | |||
590 | account: this.Account | ||
591 | ? this.Account.toFormattedJSON() | ||
592 | : null | ||
593 | } as VideoComment | ||
594 | } | ||
595 | |||
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 | |||
621 | toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject | ActivityTombstoneObject { | ||
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 | |||
630 | if (this.isDeleted()) { | ||
631 | return { | ||
632 | id: this.url, | ||
633 | type: 'Tombstone', | ||
634 | formerType: 'Note', | ||
635 | inReplyTo, | ||
636 | published: this.createdAt.toISOString(), | ||
637 | updated: this.updatedAt.toISOString(), | ||
638 | deleted: this.deletedAt.toISOString() | ||
639 | } | ||
640 | } | ||
641 | |||
642 | const tag: ActivityTagObject[] = [] | ||
643 | for (const parentComment of threadParentComments) { | ||
644 | if (!parentComment.Account) continue | ||
645 | |||
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 | |||
655 | return { | ||
656 | type: 'Note' as 'Note', | ||
657 | id: this.url, | ||
658 | |||
659 | content: this.text, | ||
660 | mediaType: 'text/markdown', | ||
661 | |||
662 | inReplyTo, | ||
663 | updated: this.updatedAt.toISOString(), | ||
664 | published: this.createdAt.toISOString(), | ||
665 | url: this.url, | ||
666 | attributedTo: this.Account.Actor.url, | ||
667 | tag | ||
668 | } | ||
669 | } | ||
670 | |||
671 | private static async buildBlockerAccountIds (options: { | ||
672 | user: MUserAccountId | ||
673 | }): Promise<number[]> { | ||
674 | const { user } = options | ||
675 | |||
676 | const serverActor = await getServerActor() | ||
677 | const blockerAccountIds = [ serverActor.Account.id ] | ||
678 | |||
679 | if (user) blockerAccountIds.push(user.Account.id) | ||
680 | |||
681 | return blockerAccountIds | ||
682 | } | ||
683 | } | ||