]>
Commit | Line | Data |
---|---|---|
cde3d90d | 1 | import { FindOptions, Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize' |
57f6896f C |
2 | import { |
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 | 16 | import { getServerActor } from '@server/models/application/application' |
26d6bf65 | 17 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
cde3d90d | 18 | import { pick, uniqify } from '@shared/core-utils' |
d0800f76 | 19 | import { AttributesOnly } from '@shared/typescript-utils' |
69222afa | 20 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
ea44f375 | 21 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
2b02c520 | 22 | import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model' |
f7cc67b4 | 23 | import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor' |
444c0a0e | 24 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
f7cc67b4 | 25 | import { regexpCapture } from '../../helpers/regexp' |
444c0a0e | 26 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
453e83ea C |
27 | import { |
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 | 40 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
444c0a0e | 41 | import { AccountModel } from '../account/account' |
cde3d90d C |
42 | import { ActorModel } from '../actor/actor' |
43 | import { buildLocalAccountIdsIn, throwIfNotValid } from '../utils' | |
44 | import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder' | |
444c0a0e C |
45 | import { VideoModel } from './video' |
46 | import { VideoChannelModel } from './video-channel' | |
6d852470 | 47 | |
594d3e48 | 48 | export 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 | 114 | export 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 | } |