aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorJulien Maulny <julien.maulny@protonmail.com>2019-11-15 19:05:08 +0100
committerChocobozzz <chocobozzz@cpy.re>2019-12-04 09:36:45 +0100
commit69222afac8f8c41d90295b33f0695bbff352851e (patch)
tree63fe1faea94dd3bfc54e633631eecb275c969e54 /server
parent69c7f7525ddf13b7ced787d8b72ac74b43665517 (diff)
downloadPeerTube-69222afac8f8c41d90295b33f0695bbff352851e.tar.gz
PeerTube-69222afac8f8c41d90295b33f0695bbff352851e.tar.zst
PeerTube-69222afac8f8c41d90295b33f0695bbff352851e.zip
Soft delete video comments instead of detroy
Diffstat (limited to 'server')
-rw-r--r--server/controllers/activitypub/client.ts13
-rw-r--r--server/controllers/api/videos/comment.ts12
-rw-r--r--server/initializers/migrations/0450-soft-delete-video-comments.ts36
-rw-r--r--server/lib/activitypub/process/process-delete.ts7
-rw-r--r--server/lib/video-comment.ts9
-rw-r--r--server/models/account/account.ts2
-rw-r--r--server/models/video/video-comment.ts33
-rw-r--r--server/tests/api/videos/multiple-servers.ts56
-rw-r--r--server/tests/api/videos/video-comments.ts24
9 files changed, 162 insertions, 30 deletions
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts
index 453ced8bf..5ed0435ff 100644
--- a/server/controllers/activitypub/client.ts
+++ b/server/controllers/activitypub/client.ts
@@ -308,13 +308,16 @@ async function videoCommentController (req: express.Request, res: express.Respon
308 308
309 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined) 309 const threadParentComments = await VideoCommentModel.listThreadParentComments(videoComment, undefined)
310 const isPublic = true // Comments are always public 310 const isPublic = true // Comments are always public
311 const audience = getAudience(videoComment.Account.Actor, isPublic) 311 let videoCommentObject = videoComment.toActivityPubObject(threadParentComments)
312 312
313 const videoCommentObject = audiencify(videoComment.toActivityPubObject(threadParentComments), audience) 313 if (videoComment.Account) {
314 const audience = getAudience(videoComment.Account.Actor, isPublic)
315 videoCommentObject = audiencify(videoCommentObject, audience)
314 316
315 if (req.path.endsWith('/activity')) { 317 if (req.path.endsWith('/activity')) {
316 const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience) 318 const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience)
317 return activityPubResponse(activityPubContextify(data), res) 319 return activityPubResponse(activityPubContextify(data), res)
320 }
318 } 321 }
319 322
320 return activityPubResponse(activityPubContextify(videoCommentObject), res) 323 return activityPubResponse(activityPubContextify(videoCommentObject), res)
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts
index b2b06b170..5f3fed5c0 100644
--- a/server/controllers/api/videos/comment.ts
+++ b/server/controllers/api/videos/comment.ts
@@ -1,10 +1,11 @@
1import * as express from 'express' 1import * as express from 'express'
2import { cloneDeep } from 'lodash'
2import { ResultList } from '../../../../shared/models' 3import { ResultList } from '../../../../shared/models'
3import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model' 4import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
4import { logger } from '../../../helpers/logger' 5import { logger } from '../../../helpers/logger'
5import { getFormattedObjects } from '../../../helpers/utils' 6import { getFormattedObjects } from '../../../helpers/utils'
6import { sequelizeTypescript } from '../../../initializers' 7import { sequelizeTypescript } from '../../../initializers'
7import { buildFormattedCommentTree, createVideoComment } from '../../../lib/video-comment' 8import { buildFormattedCommentTree, createVideoComment, markCommentAsDeleted } from '../../../lib/video-comment'
8import { 9import {
9 asyncMiddleware, 10 asyncMiddleware,
10 asyncRetryTransactionMiddleware, 11 asyncRetryTransactionMiddleware,
@@ -177,19 +178,22 @@ async function addVideoCommentReply (req: express.Request, res: express.Response
177 178
178async function removeVideoComment (req: express.Request, res: express.Response) { 179async function removeVideoComment (req: express.Request, res: express.Response) {
179 const videoCommentInstance = res.locals.videoCommentFull 180 const videoCommentInstance = res.locals.videoCommentFull
181 const videoCommentInstanceBefore = cloneDeep(videoCommentInstance)
180 182
181 await sequelizeTypescript.transaction(async t => { 183 await sequelizeTypescript.transaction(async t => {
182 await videoCommentInstance.destroy({ transaction: t })
183
184 if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) { 184 if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) {
185 await sendDeleteVideoComment(videoCommentInstance, t) 185 await sendDeleteVideoComment(videoCommentInstance, t)
186 } 186 }
187
188 markCommentAsDeleted(videoCommentInstance)
189
190 await videoCommentInstance.save()
187 }) 191 })
188 192
189 auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON())) 193 auditLogger.delete(getAuditIdFromRes(res), new CommentAuditView(videoCommentInstance.toFormattedJSON()))
190 logger.info('Video comment %d deleted.', videoCommentInstance.id) 194 logger.info('Video comment %d deleted.', videoCommentInstance.id)
191 195
192 Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstance }) 196 Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore })
193 197
194 return res.type('json').status(204).end() 198 return res.type('json').status(204).end()
195} 199}
diff --git a/server/initializers/migrations/0450-soft-delete-video-comments.ts b/server/initializers/migrations/0450-soft-delete-video-comments.ts
new file mode 100644
index 000000000..bcfb97b56
--- /dev/null
+++ b/server/initializers/migrations/0450-soft-delete-video-comments.ts
@@ -0,0 +1,36 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize,
7 db: any
8}): Promise<void> {
9 {
10 const data = {
11 type: Sequelize.INTEGER,
12 allowNull: true,
13 defaultValue: null
14 }
15
16 await utils.queryInterface.changeColumn('videoComment', 'accountId', data)
17 }
18
19 {
20 const data = {
21 type: Sequelize.DATE,
22 allowNull: true,
23 defaultValue: null
24 }
25 await utils.queryInterface.addColumn('videoComment', 'deletedAt', data)
26 }
27}
28
29function down (options) {
30 throw new Error('Not implemented.')
31}
32
33export {
34 up,
35 down
36}
diff --git a/server/lib/activitypub/process/process-delete.ts b/server/lib/activitypub/process/process-delete.ts
index 79d0e0d79..e76132f91 100644
--- a/server/lib/activitypub/process/process-delete.ts
+++ b/server/lib/activitypub/process/process-delete.ts
@@ -5,6 +5,7 @@ import { sequelizeTypescript } from '../../../initializers'
5import { ActorModel } from '../../../models/activitypub/actor' 5import { ActorModel } from '../../../models/activitypub/actor'
6import { VideoModel } from '../../../models/video/video' 6import { VideoModel } from '../../../models/video/video'
7import { VideoCommentModel } from '../../../models/video/video-comment' 7import { VideoCommentModel } from '../../../models/video/video-comment'
8import { markCommentAsDeleted } from '../../video-comment'
8import { forwardVideoRelatedActivity } from '../send/utils' 9import { forwardVideoRelatedActivity } from '../send/utils'
9import { VideoPlaylistModel } from '../../../models/video/video-playlist' 10import { VideoPlaylistModel } from '../../../models/video/video-playlist'
10import { APProcessorOptions } from '../../../typings/activitypub-processor.model' 11import { APProcessorOptions } from '../../../typings/activitypub-processor.model'
@@ -128,7 +129,11 @@ function processDeleteVideoComment (byActor: MActorSignature, videoComment: Vide
128 throw new Error(`Account ${byActor.url} does not own video comment ${videoComment.url} or video ${videoComment.Video.url}`) 129 throw new Error(`Account ${byActor.url} does not own video comment ${videoComment.url} or video ${videoComment.Video.url}`)
129 } 130 }
130 131
131 await videoComment.destroy({ transaction: t }) 132 await sequelizeTypescript.transaction(async t => {
133 markCommentAsDeleted(videoComment)
134
135 await videoComment.save()
136 })
132 137
133 if (videoComment.Video.isOwned()) { 138 if (videoComment.Video.isOwned()) {
134 // Don't resend the activity to the sender 139 // Don't resend the activity to the sender
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts
index bb811bd2c..b8074e6d2 100644
--- a/server/lib/video-comment.ts
+++ b/server/lib/video-comment.ts
@@ -73,9 +73,16 @@ function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>):
73 return thread 73 return thread
74} 74}
75 75
76function markCommentAsDeleted (comment: MCommentOwnerVideoReply): void {
77 comment.text = ''
78 comment.deletedAt = new Date()
79 comment.accountId = null
80}
81
76// --------------------------------------------------------------------------- 82// ---------------------------------------------------------------------------
77 83
78export { 84export {
79 createVideoComment, 85 createVideoComment,
80 buildFormattedCommentTree 86 buildFormattedCommentTree,
87 markCommentAsDeleted
81} 88}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index ba1094536..a818a5a4d 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -201,7 +201,7 @@ export class AccountModel extends Model<AccountModel> {
201 201
202 @HasMany(() => VideoCommentModel, { 202 @HasMany(() => VideoCommentModel, {
203 foreignKey: { 203 foreignKey: {
204 allowNull: false 204 allowNull: true
205 }, 205 },
206 onDelete: 'cascade', 206 onDelete: 'cascade',
207 hooks: true 207 hooks: true
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts
index 2e4220434..b44d65138 100644
--- a/server/models/video/video-comment.ts
+++ b/server/models/video/video-comment.ts
@@ -1,5 +1,5 @@
1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 1import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
2import { ActivityTagObject } from '../../../shared/models/activitypub/objects/common-objects' 2import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
3import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' 3import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
4import { VideoComment } from '../../../shared/models/videos/video-comment.model' 4import { VideoComment } from '../../../shared/models/videos/video-comment.model'
5import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 5import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -122,6 +122,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
122 @UpdatedAt 122 @UpdatedAt
123 updatedAt: Date 123 updatedAt: Date
124 124
125 @AllowNull(true)
126 @Column(DataType.DATE)
127 deletedAt: Date
128
125 @AllowNull(false) 129 @AllowNull(false)
126 @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) 130 @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
127 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) 131 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
@@ -177,7 +181,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
177 181
178 @BelongsTo(() => AccountModel, { 182 @BelongsTo(() => AccountModel, {
179 foreignKey: { 183 foreignKey: {
180 allowNull: false 184 allowNull: true
181 }, 185 },
182 onDelete: 'CASCADE' 186 onDelete: 'CASCADE'
183 }) 187 })
@@ -436,9 +440,17 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
436 } 440 }
437 441
438 isOwned () { 442 isOwned () {
443 if (!this.Account) {
444 return false
445 }
446
439 return this.Account.isOwned() 447 return this.Account.isOwned()
440 } 448 }
441 449
450 isDeleted () {
451 return null !== this.deletedAt
452 }
453
442 extractMentions () { 454 extractMentions () {
443 let result: string[] = [] 455 let result: string[] = []
444 456
@@ -487,12 +499,25 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
487 videoId: this.videoId, 499 videoId: this.videoId,
488 createdAt: this.createdAt, 500 createdAt: this.createdAt,
489 updatedAt: this.updatedAt, 501 updatedAt: this.updatedAt,
502 deletedAt: this.deletedAt,
503 isDeleted: this.isDeleted(),
490 totalReplies: this.get('totalReplies') || 0, 504 totalReplies: this.get('totalReplies') || 0,
491 account: this.Account.toFormattedJSON() 505 account: this.Account ? this.Account.toFormattedJSON() : null
492 } as VideoComment 506 } as VideoComment
493 } 507 }
494 508
495 toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject { 509 toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject | ActivityTombstoneObject {
510 if (this.isDeleted()) {
511 return {
512 id: this.url,
513 type: 'Tombstone',
514 formerType: 'Note',
515 published: this.createdAt.toISOString(),
516 updated: this.updatedAt.toISOString(),
517 deleted: this.deletedAt.toISOString()
518 }
519 }
520
496 let inReplyTo: string 521 let inReplyTo: string
497 // New thread, so in AS we reply to the video 522 // New thread, so in AS we reply to the video
498 if (this.inReplyToCommentId === null) { 523 if (this.inReplyToCommentId === null) {
diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts
index aeda188c2..e7b57ba1f 100644
--- a/server/tests/api/videos/multiple-servers.ts
+++ b/server/tests/api/videos/multiple-servers.ts
@@ -868,7 +868,7 @@ describe('Test multiple servers', function () {
868 await waitJobs(servers) 868 await waitJobs(servers)
869 }) 869 })
870 870
871 it('Should not have this comment anymore', async function () { 871 it('Should have this comment marked as deleted', async function () {
872 for (const server of servers) { 872 for (const server of servers) {
873 const res1 = await getVideoCommentThreads(server.url, videoUUID, 0, 5) 873 const res1 = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
874 const threadId = res1.body.data.find(c => c.text === 'my super first comment').id 874 const threadId = res1.body.data.find(c => c.text === 'my super first comment').id
@@ -880,7 +880,13 @@ describe('Test multiple servers', function () {
880 880
881 const firstChild = tree.children[0] 881 const firstChild = tree.children[0]
882 expect(firstChild.comment.text).to.equal('my super answer to thread 1') 882 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
883 expect(firstChild.children).to.have.lengthOf(0) 883 expect(firstChild.children).to.have.lengthOf(1)
884
885 const deletedComment = firstChild.children[0].comment
886 expect(deletedComment.isDeleted).to.be.true
887 expect(deletedComment.deletedAt).to.not.be.null
888 expect(deletedComment.account).to.be.null
889 expect(deletedComment.text).to.equal('')
884 890
885 const secondChild = tree.children[1] 891 const secondChild = tree.children[1]
886 expect(secondChild.comment.text).to.equal('my second answer to thread 1') 892 expect(secondChild.comment.text).to.equal('my second answer to thread 1')
@@ -897,13 +903,13 @@ describe('Test multiple servers', function () {
897 await waitJobs(servers) 903 await waitJobs(servers)
898 }) 904 })
899 905
900 it('Should have the threads deleted on other servers too', async function () { 906 it('Should have the threads marked as deleted on other servers too', async function () {
901 for (const server of servers) { 907 for (const server of servers) {
902 const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5) 908 const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
903 909
904 expect(res.body.total).to.equal(1) 910 expect(res.body.total).to.equal(2)
905 expect(res.body.data).to.be.an('array') 911 expect(res.body.data).to.be.an('array')
906 expect(res.body.data).to.have.lengthOf(1) 912 expect(res.body.data).to.have.lengthOf(2)
907 913
908 { 914 {
909 const comment: VideoComment = res.body.data[0] 915 const comment: VideoComment = res.body.data[0]
@@ -915,6 +921,20 @@ describe('Test multiple servers', function () {
915 expect(dateIsValid(comment.createdAt as string)).to.be.true 921 expect(dateIsValid(comment.createdAt as string)).to.be.true
916 expect(dateIsValid(comment.updatedAt as string)).to.be.true 922 expect(dateIsValid(comment.updatedAt as string)).to.be.true
917 } 923 }
924
925 {
926 const deletedComment: VideoComment = res.body.data[1]
927 expect(deletedComment).to.not.be.undefined
928 expect(deletedComment.isDeleted).to.be.true
929 expect(deletedComment.deletedAt).to.not.be.null
930 expect(deletedComment.text).to.equal('')
931 expect(deletedComment.inReplyToCommentId).to.be.null
932 expect(deletedComment.account).to.be.null
933 expect(deletedComment.totalReplies).to.equal(3)
934 expect(dateIsValid(deletedComment.createdAt as string)).to.be.true
935 expect(dateIsValid(deletedComment.updatedAt as string)).to.be.true
936 expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
937 }
918 } 938 }
919 }) 939 })
920 940
@@ -926,12 +946,32 @@ describe('Test multiple servers', function () {
926 await waitJobs(servers) 946 await waitJobs(servers)
927 }) 947 })
928 948
929 it('Should have the threads deleted on other servers too', async function () { 949 it('Should have the threads marked as deleted on other servers too', async function () {
930 for (const server of servers) { 950 for (const server of servers) {
931 const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5) 951 const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
932 952
933 expect(res.body.total).to.equal(0) 953 expect(res.body.total).to.equal(2)
934 expect(res.body.data).to.have.lengthOf(0) 954 expect(res.body.data).to.have.lengthOf(2)
955
956 {
957 const comment: VideoComment = res.body.data[0]
958 expect(comment.text).to.equal('')
959 expect(comment.isDeleted).to.be.true
960 expect(comment.createdAt).to.not.be.null
961 expect(comment.deletedAt).to.not.be.null
962 expect(comment.account).to.be.null
963 expect(comment.totalReplies).to.equal(0)
964 }
965
966 {
967 const comment: VideoComment = res.body.data[1]
968 expect(comment.text).to.equal('')
969 expect(comment.isDeleted).to.be.true
970 expect(comment.createdAt).to.not.be.null
971 expect(comment.deletedAt).to.not.be.null
972 expect(comment.account).to.be.null
973 expect(comment.totalReplies).to.equal(3)
974 }
935 } 975 }
936 }) 976 })
937 977
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts
index 82182cc7c..95be14c0e 100644
--- a/server/tests/api/videos/video-comments.ts
+++ b/server/tests/api/videos/video-comments.ts
@@ -172,7 +172,7 @@ describe('Test video comments', function () {
172 172
173 const tree: VideoCommentThreadTree = res.body 173 const tree: VideoCommentThreadTree = res.body
174 expect(tree.comment.text).equal('my super first comment') 174 expect(tree.comment.text).equal('my super first comment')
175 expect(tree.children).to.have.lengthOf(1) 175 expect(tree.children).to.have.lengthOf(2)
176 176
177 const firstChild = tree.children[0] 177 const firstChild = tree.children[0]
178 expect(firstChild.comment.text).to.equal('my super answer to thread 1') 178 expect(firstChild.comment.text).to.equal('my super answer to thread 1')
@@ -181,20 +181,32 @@ describe('Test video comments', function () {
181 const childOfFirstChild = firstChild.children[0] 181 const childOfFirstChild = firstChild.children[0]
182 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') 182 expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
183 expect(childOfFirstChild.children).to.have.lengthOf(0) 183 expect(childOfFirstChild.children).to.have.lengthOf(0)
184
185 const deletedChildOfFirstChild = tree.children[1]
186 expect(deletedChildOfFirstChild.comment.text).to.equal('')
187 expect(deletedChildOfFirstChild.comment.isDeleted).to.be.true
188 expect(deletedChildOfFirstChild.comment.deletedAt).to.not.be.null
189 expect(deletedChildOfFirstChild.comment.account).to.be.null
190 expect(deletedChildOfFirstChild.children).to.have.lengthOf(0)
184 }) 191 })
185 192
186 it('Should delete a complete thread', async function () { 193 it('Should delete a complete thread', async function () {
187 await deleteVideoComment(server.url, server.accessToken, videoId, threadId) 194 await deleteVideoComment(server.url, server.accessToken, videoId, threadId)
188 195
189 const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt') 196 const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt')
190 expect(res.body.total).to.equal(2) 197 expect(res.body.total).to.equal(3)
191 expect(res.body.data).to.be.an('array') 198 expect(res.body.data).to.be.an('array')
192 expect(res.body.data).to.have.lengthOf(2) 199 expect(res.body.data).to.have.lengthOf(3)
193 200
194 expect(res.body.data[0].text).to.equal('super thread 2') 201 expect(res.body.data[0].text).to.equal('')
195 expect(res.body.data[0].totalReplies).to.equal(0) 202 expect(res.body.data[0].isDeleted).to.be.true
196 expect(res.body.data[1].text).to.equal('super thread 3') 203 expect(res.body.data[0].deletedAt).to.not.be.null
204 expect(res.body.data[0].account).to.be.null
205 expect(res.body.data[0].totalReplies).to.equal(3)
206 expect(res.body.data[1].text).to.equal('super thread 2')
197 expect(res.body.data[1].totalReplies).to.equal(0) 207 expect(res.body.data[1].totalReplies).to.equal(0)
208 expect(res.body.data[2].text).to.equal('super thread 3')
209 expect(res.body.data[2].totalReplies).to.equal(0)
198 }) 210 })
199 211
200 after(async function () { 212 after(async function () {