Fix retrieving of deleted comments when subscribing to a new instance
authorJulien Maulny <julien.maulny@protonmail.com>
Tue, 3 Dec 2019 20:48:31 +0000 (21:48 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Wed, 4 Dec 2019 08:36:45 +0000 (09:36 +0100)
server/helpers/custom-validators/activitypub/video-comments.ts
server/lib/activitypub/video-comments.ts
server/models/video/video-comment.ts
server/tests/api/videos/multiple-servers.ts
shared/models/activitypub/objects/common-objects.ts

index e04c5388f9059bf5cfc7022fbbe0fe8cb302c786..96655c3f82ecb7ddf3a2e714f658f3502ab262fa 100644 (file)
@@ -3,11 +3,28 @@ import { ACTIVITY_PUB } from '../../../initializers/constants'
 import { exists, isArray, isDateValid } from '../misc'
 import { isActivityPubUrlValid } from './misc'
 
+function isTypeValid (comment: any): boolean {
+  if (comment.type === 'Note') return true
+
+  if (comment.type === 'Tombstone' && comment.formerType === 'Note') return true
+
+  return false
+}
+
 function sanitizeAndCheckVideoCommentObject (comment: any) {
-  if (!comment || comment.type !== 'Note') return false
+  if (!comment) return false
+
+  if (!isTypeValid(comment)) return false
 
   normalizeComment(comment)
 
+  if (comment.type === 'Tombstone') {
+    return isActivityPubUrlValid(comment.id) &&
+      isDateValid(comment.published) &&
+      isDateValid(comment.deleted) &&
+      isActivityPubUrlValid(comment.url)
+  }
+
   return isActivityPubUrlValid(comment.id) &&
     isCommentContentValid(comment.content) &&
     isActivityPubUrlValid(comment.inReplyTo) &&
index 3e8306fa4c6ecf117adbc477e410b6f3bbc68d4c..1a15842cf6e62d7b44ccf07ced0a4024e4ecb218 100644 (file)
@@ -131,9 +131,9 @@ async function resolveParentComment (params: ResolveThreadParams) {
   }
 
   const actorUrl = body.attributedTo
-  if (!actorUrl) throw new Error('Miss attributed to in comment')
+  if (!actorUrl && body.type !== 'Tombstone') throw new Error('Miss attributed to in comment')
 
-  if (checkUrlsSameHost(url, actorUrl) !== true) {
+  if (actorUrl && checkUrlsSameHost(url, actorUrl) !== true) {
     throw new Error(`Actor url ${actorUrl} has not the same host than the comment url ${url}`)
   }
 
@@ -141,18 +141,19 @@ async function resolveParentComment (params: ResolveThreadParams) {
     throw new Error(`Comment url ${url} host is different from the AP object id ${body.id}`)
   }
 
-  const actor = await getOrCreateActorAndServerAndModel(actorUrl, 'all')
+  const actor = actorUrl ? await getOrCreateActorAndServerAndModel(actorUrl, 'all') : null
   const comment = new VideoCommentModel({
     url: body.id,
-    text: body.content,
+    text: body.content ? body.content : '',
     videoId: null,
-    accountId: actor.Account.id,
+    accountId: actor ? actor.Account.id : null,
     inReplyToCommentId: null,
     originCommentId: null,
     createdAt: new Date(body.published),
-    updatedAt: new Date(body.updated)
+    updatedAt: new Date(body.updated),
+    deletedAt: body.deleted ? new Date(body.deleted) : null
   }) as MCommentOwner
-  comment.Account = actor.Account
+  comment.Account = actor ? actor.Account : null
 
   return resolveThread({
     url: body.inReplyTo,
index b44d65138f1bdfd476a3a9680692ba2d587055b3..869a42afef0f14197fa8bf4643c7e87058651017 100644 (file)
@@ -507,27 +507,30 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
   }
 
   toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject | ActivityTombstoneObject {
+    let inReplyTo: string
+    // New thread, so in AS we reply to the video
+    if (this.inReplyToCommentId === null) {
+      inReplyTo = this.Video.url
+    } else {
+      inReplyTo = this.InReplyToVideoComment.url
+    }
+
     if (this.isDeleted()) {
       return {
         id: this.url,
         type: 'Tombstone',
         formerType: 'Note',
+        inReplyTo,
         published: this.createdAt.toISOString(),
         updated: this.updatedAt.toISOString(),
         deleted: this.deletedAt.toISOString()
       }
     }
 
-    let inReplyTo: string
-    // New thread, so in AS we reply to the video
-    if (this.inReplyToCommentId === null) {
-      inReplyTo = this.Video.url
-    } else {
-      inReplyTo = this.InReplyToVideoComment.url
-    }
-
     const tag: ActivityTagObject[] = []
     for (const parentComment of threadParentComments) {
+      if (!parentComment.Account) continue
+
       const actor = parentComment.Account.Actor
 
       tag.push({
index e7b57ba1f071be7e9468fc572f2b24fc515b1b62..836bdc6588ce80eb3ddafd86c81d32491ffa6ba4 100644 (file)
@@ -15,6 +15,7 @@ import {
   createUser,
   dateIsValid,
   doubleFollow,
+  flushAndRunServer,
   flushAndRunMultipleServers,
   getLocalVideos,
   getVideo,
@@ -938,7 +939,51 @@ describe('Test multiple servers', function () {
       }
     })
 
+    it('Should retrieve all comments when subscribing to a new server', async function () {
+      this.timeout(120000)
+
+      const newServer = await flushAndRunServer(4)
+
+      await setAccessTokensToServers([newServer])
+      await doubleFollow(newServer, servers[0])
+      await doubleFollow(newServer, servers[2])
+      await waitJobs([newServer, ...servers])
+
+      const res = await getVideoCommentThreads(newServer.url, videoUUID, 0, 5)
+
+      expect(res.body.total).to.equal(2)
+      expect(res.body.data).to.be.an('array')
+      expect(res.body.data).to.have.lengthOf(2)
+
+      {
+        const comment: VideoComment = res.body.data[0]
+        expect(comment).to.not.be.undefined
+        expect(comment.inReplyToCommentId).to.be.null
+        expect(comment.account.name).to.equal('root')
+        expect(comment.account.host).to.equal('localhost:' + servers[2].port)
+        expect(comment.totalReplies).to.equal(0)
+        expect(dateIsValid(comment.createdAt as string)).to.be.true
+        expect(dateIsValid(comment.updatedAt as string)).to.be.true
+      }
+
+      {
+        const deletedComment: VideoComment = res.body.data[1]
+        expect(deletedComment).to.not.be.undefined
+        expect(deletedComment.isDeleted).to.be.true
+        expect(deletedComment.deletedAt).to.not.be.null
+        expect(deletedComment.text).to.equal('')
+        expect(deletedComment.inReplyToCommentId).to.be.null
+        expect(deletedComment.account).to.be.null
+        expect(deletedComment.totalReplies).to.equal(3)
+        expect(dateIsValid(deletedComment.createdAt as string)).to.be.true
+        expect(dateIsValid(deletedComment.updatedAt as string)).to.be.true
+        expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true
+      }
+    })
+
     it('Should delete a remote thread by the origin server', async function () {
+      this.timeout(5000)
+
       const res = await getVideoCommentThreads(servers[ 0 ].url, videoUUID, 0, 5)
       const threadId = res.body.data.find(c => c.text === 'my super second comment').id
       await deleteVideoComment(servers[ 0 ].url, servers[ 0 ].accessToken, videoUUID, threadId)
index df287d5709fb1b71856e452c22b21412d79ac16b..de1116ab3fd45790dc3a77ab97036cdde558124e 100644 (file)
@@ -93,9 +93,11 @@ export interface ActivityPubAttributedTo {
 export interface ActivityTombstoneObject {
   '@context'?: any
   id: string
+  url?: string
   type: 'Tombstone'
   name?: string
   formerType?: string
+  inReplyTo?: string
   published: string
   updated: string
   deleted: string