]>
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 | 42 | import { ActorModel } from '../actor/actor' |
8c4bbd94 | 43 | import { buildLocalAccountIdsIn, buildSQLAttributes, throwIfNotValid } from '../shared' |
cde3d90d | 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 | ||
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 | } |