Cleanup invalid rates/comments/shares
authorChocobozzz <me@florianbigard.com>
Tue, 19 Mar 2019 15:23:02 +0000 (16:23 +0100)
committerChocobozzz <me@florianbigard.com>
Tue, 19 Mar 2019 15:23:02 +0000 (16:23 +0100)
16 files changed:
server/controllers/api/users/my-blocklist.ts
server/controllers/api/videos/watching.ts
server/helpers/audit-logger.ts
server/lib/activitypub/crawl.ts
server/lib/activitypub/share.ts
server/lib/activitypub/video-comments.ts
server/lib/activitypub/video-rates.ts
server/lib/activitypub/videos.ts
server/lib/job-queue/handlers/activitypub-http-fetcher.ts
server/middlewares/validators/users.ts
server/models/account/account-video-rate.ts
server/models/video/video-comment.ts
server/models/video/video-share.ts
server/models/video/video.ts
server/tests/api/activitypub/refresher.ts
server/tests/api/videos/multiple-servers.ts

index 481e75139788319040f564c3003560cb0b385dbb..713c16022322a25fed2a8ab09037fd1fd4cd02a4 100644 (file)
@@ -17,11 +17,9 @@ import {
   serversBlocklistSortValidator,
   unblockServerByAccountValidator
 } from '../../../middlewares/validators'
-import { AccountModel } from '../../../models/account/account'
 import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
 import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
 import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
-import { ServerModel } from '../../../models/server/server'
 
 const myBlocklistRouter = express.Router()
 
@@ -83,7 +81,7 @@ async function listBlockedAccounts (req: express.Request, res: express.Response)
 
 async function blockAccount (req: express.Request, res: express.Response) {
   const user = res.locals.oauth.token.User
-  const accountToBlock  = res.locals.account
+  const accountToBlock = res.locals.account
 
   await addAccountInBlocklist(user.Account.id, accountToBlock.id)
 
index 6bc60e045bc7a99dc6f44d2407bf04d5cdb6953b..dcd1f070d1662ff680a6c32ecec19e83af188f78 100644 (file)
@@ -2,7 +2,6 @@ import * as express from 'express'
 import { UserWatchingVideo } from '../../../../shared'
 import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares'
 import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
-import { UserModel } from '../../../models/account/user'
 
 const watchingRouter = express.Router()
 
index a121f0b8a1420d0bc783fc331918ce09b3ebd8ab..af37bce16cd0da150f6d780df1e7b6346c42e404 100644 (file)
@@ -6,13 +6,12 @@ import * as flatten from 'flat'
 import * as winston from 'winston'
 import { CONFIG } from '../initializers'
 import { jsonLoggerFormat, labelFormatter } from './logger'
-import { VideoDetails, User, VideoChannel, VideoAbuse, VideoImport } from '../../shared'
+import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../../shared'
 import { VideoComment } from '../../shared/models/videos/video-comment.model'
 import { CustomConfig } from '../../shared/models/server/custom-config.model'
-import { UserModel } from '../models/account/user'
 
 function getAuditIdFromRes (res: express.Response) {
-  return (res.locals.oauth.token.User as UserModel).username
+  return res.locals.oauth.token.User.username
 }
 
 enum AUDIT_TYPE {
index 2675524c63037140e6aab3a00de023751921bb74..9f4ca98bac28e494b8971535a2a7ecc415804f9f 100644 (file)
@@ -4,7 +4,10 @@ import { logger } from '../../helpers/logger'
 import * as Bluebird from 'bluebird'
 import { ActivityPubOrderedCollection } from '../../../shared/models/activitypub'
 
-async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => (Promise<any> | Bluebird<any>)) {
+type HandlerFunction<T> = (items: T[]) => (Promise<any> | Bluebird<any>)
+type CleanerFunction = (startedDate: Date) => (Promise<any> | Bluebird<any>)
+
+async function crawlCollectionPage <T> (uri: string, handler: HandlerFunction<T>, cleaner?: CleanerFunction) {
   logger.info('Crawling ActivityPub data on %s.', uri)
 
   const options = {
@@ -15,6 +18,8 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => (P
     timeout: JOB_REQUEST_TIMEOUT
   }
 
+  const startDate = new Date()
+
   const response = await doRequest<ActivityPubOrderedCollection<T>>(options)
   const firstBody = response.body
 
@@ -35,6 +40,8 @@ async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => (P
       await handler(items)
     }
   }
+
+  if (cleaner) await cleaner(startDate)
 }
 
 export {
index 1767df0aeb5b895761a01c93079f1579a54eefdd..3bece0ff72748f49d9889c82dfd803ced36d9da3 100644 (file)
@@ -54,12 +54,7 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
         url: shareUrl
       }
 
-      await VideoShareModel.findOrCreate({
-        where: {
-          url: shareUrl
-        },
-        defaults: entry
-      })
+      await VideoShareModel.upsert(entry)
     } catch (err) {
       logger.warn('Cannot add share %s.', shareUrl, { err })
     }
index e87301fe7d0b25250d67579de96ca5680828bd82..3f9d8f0fc4a4c85c46f3c84e8eb8f76180790aa3 100644 (file)
@@ -34,8 +34,7 @@ async function videoCommentActivityObjectToDBAttributes (video: VideoModel, acto
     accountId: actor.Account.id,
     inReplyToCommentId,
     originCommentId,
-    createdAt: new Date(comment.published),
-    updatedAt: new Date(comment.updated)
+    createdAt: new Date(comment.published)
   }
 }
 
@@ -74,12 +73,7 @@ async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
   const entry = await videoCommentActivityObjectToDBAttributes(videoInstance, actor, body)
   if (!entry) return { created: false }
 
-  const [ comment, created ] = await VideoCommentModel.findOrCreate({
-    where: {
-      url: body.id
-    },
-    defaults: entry
-  })
+  const [ comment, created ] = await VideoCommentModel.upsert<VideoCommentModel>(entry, { returning: true })
   comment.Account = actor.Account
   comment.Video = videoInstance
 
index 7aac7911841e24fc05a4dcd875b07a14037499c4..ad7d81df6226246d13d5a52374f89fb6cdf64c81 100644 (file)
@@ -38,19 +38,14 @@ async function createRates (ratesUrl: string[], video: VideoModel, rate: VideoRa
 
       const actor = await getOrCreateActorAndServerAndModel(actorUrl)
 
-      const [ , created ] = await AccountVideoRateModel
-        .findOrCreate({
-          where: {
-            videoId: video.id,
-            accountId: actor.Account.id
-          },
-          defaults: {
-            videoId: video.id,
-            accountId: actor.Account.id,
-            type: rate,
-            url: body.id
-          }
-        })
+      const entry = {
+        videoId: video.id,
+        accountId: actor.Account.id,
+        type: rate,
+        url: body.id
+      }
+
+      const created = await AccountVideoRateModel.upsert(entry)
 
       if (created) rateCounts += 1
     } catch (err) {
index 66d0abf359ce35e97b4e2cc698cc3b969a78a5fb..2c932371b3d5dbdb240814481019ef5c26279eb5 100644 (file)
@@ -40,6 +40,9 @@ import { Notifier } from '../notifier'
 import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
 import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
 import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
+import { AccountVideoRateModel } from '../../models/account/account-video-rate'
+import { VideoShareModel } from '../../models/video/video-share'
+import { VideoCommentModel } from '../../models/video/video-comment'
 
 async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
   // If the video is not private and published, we federate it
@@ -134,31 +137,43 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid
   const jobPayloads: ActivitypubHttpFetcherPayload[] = []
 
   if (syncParam.likes === true) {
-    await crawlCollectionPage<string>(fetchedVideo.likes, items => createRates(items, video, 'like'))
+    const handler = items => createRates(items, video, 'like')
+    const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate)
+
+    await crawlCollectionPage<string>(fetchedVideo.likes, handler, cleaner)
       .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err }))
   } else {
     jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
   }
 
   if (syncParam.dislikes === true) {
-    await crawlCollectionPage<string>(fetchedVideo.dislikes, items => createRates(items, video, 'dislike'))
+    const handler = items => createRates(items, video, 'dislike')
+    const cleaner = crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate)
+
+    await crawlCollectionPage<string>(fetchedVideo.dislikes, handler, cleaner)
       .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err }))
   } else {
     jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
   }
 
   if (syncParam.shares === true) {
-    await crawlCollectionPage<string>(fetchedVideo.shares, items => addVideoShares(items, video))
+    const handler = items => addVideoShares(items, video)
+    const cleaner = crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate)
+
+    await crawlCollectionPage<string>(fetchedVideo.shares, handler, cleaner)
       .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err }))
   } else {
     jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
   }
 
   if (syncParam.comments === true) {
-    await crawlCollectionPage<string>(fetchedVideo.comments, items => addVideoComments(items, video))
+    const handler = items => addVideoComments(items, video)
+    const cleaner = crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
+
+    await crawlCollectionPage<string>(fetchedVideo.comments, handler, cleaner)
       .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err }))
   } else {
-    jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
+    jobPayloads.push({ uri: fetchedVideo.comments, videoId: video.id, type: 'video-comments' as 'video-comments' })
   }
 
   await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }))
index 52225f64fd316cc894d98806f8f80d16694e2716..23d33c26fd47e5e51779cb05a944510d3f32dc1c 100644 (file)
@@ -1,4 +1,5 @@
 import * as Bull from 'bull'
+import * as Bluebird from 'bluebird'
 import { logger } from '../../../helpers/logger'
 import { processActivities } from '../../activitypub/process'
 import { addVideoComments } from '../../activitypub/video-comments'
@@ -7,6 +8,9 @@ import { VideoModel } from '../../../models/video/video'
 import { addVideoShares, createRates } from '../../activitypub'
 import { createAccountPlaylists } from '../../activitypub/playlist'
 import { AccountModel } from '../../../models/account/account'
+import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
+import { VideoShareModel } from '../../../models/video/video-share'
+import { VideoCommentModel } from '../../../models/video/video-comment'
 
 type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments' | 'account-playlists'
 
@@ -37,7 +41,14 @@ async function processActivityPubHttpFetcher (job: Bull.Job) {
     'account-playlists': items => createAccountPlaylists(items, account)
   }
 
-  return crawlCollectionPage(payload.uri, fetcherType[payload.type])
+  const cleanerType: { [ id in FetchType ]?: (crawlStartDate: Date) => Bluebird<any> } = {
+    'video-likes': crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'like' as 'like', crawlStartDate),
+    'video-dislikes': crawlStartDate => AccountVideoRateModel.cleanOldRatesOf(video.id, 'dislike' as 'dislike', crawlStartDate),
+    'video-shares': crawlStartDate => VideoShareModel.cleanOldSharesOf(video.id, crawlStartDate),
+    'video-comments': crawlStartDate => VideoCommentModel.cleanOldCommentsOf(video.id, crawlStartDate)
+  }
+
+  return crawlCollectionPage(payload.uri, fetcherType[payload.type], cleanerType[payload.type])
 }
 
 // ---------------------------------------------------------------------------
index e8ade0f971c2b959ae2807d65f47b035c0572d0d..4be446732d1959adc44ddb48c442dac6f13f808f 100644 (file)
@@ -160,7 +160,7 @@ const usersUpdateMeValidator = [
                   .end()
       }
 
-      const user= res.locals.oauth.token.User
+      const user = res.locals.oauth.token.User
       if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
         return res.status(401)
                   .send({ error: 'currentPassword is invalid.' })
index 18762f0c55901be4bcaeb623a820bc4ee1d9147d..e5d39582bdf7698f9b36e3adf1a5e7764b054be5 100644 (file)
@@ -1,5 +1,5 @@
 import { values } from 'lodash'
-import { Transaction } from 'sequelize'
+import { Transaction, Op } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
 import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
 import { VideoRateType } from '../../../shared/models/videos'
@@ -158,4 +158,31 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
 
     return AccountVideoRateModel.findAndCountAll(query)
   }
+
+  static cleanOldRatesOf (videoId: number, type: VideoRateType, beforeUpdatedAt: Date) {
+    return AccountVideoRateModel.sequelize.transaction(async t => {
+      const query = {
+        where: {
+          updatedAt: {
+            [Op.lt]: beforeUpdatedAt
+          },
+          videoId,
+          type
+        },
+        transaction: t
+      }
+
+      const deleted = await AccountVideoRateModel.destroy(query)
+
+      const options = {
+        transaction: t,
+        where: {
+          id: videoId
+        }
+      }
+
+      if (type === 'like') await VideoModel.increment({ likes: -deleted }, options)
+      else if (type === 'dislike') await VideoModel.increment({ dislikes: -deleted }, options)
+    })
+  }
 }
index 1163f9a0eb1d3f073cdd69ee8367b8d1e8587d6f..e733138c1369ece6a74c003321250ad292c898a5 100644 (file)
@@ -1,4 +1,5 @@
 import * as Sequelize from 'sequelize'
+import { Op } from 'sequelize'
 import {
   AllowNull,
   BeforeDestroy,
@@ -453,6 +454,19 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
     }
   }
 
+  static cleanOldCommentsOf (videoId: number, beforeUpdatedAt: Date) {
+    const query = {
+      where: {
+        updatedAt: {
+          [Op.lt]: beforeUpdatedAt
+        },
+        videoId
+      }
+    }
+
+    return VideoCommentModel.destroy(query)
+  }
+
   getCommentStaticPath () {
     return this.Video.getWatchStaticPath() + ';threadId=' + this.getThreadId()
   }
index 7df0ed18df7e89b83aa464035730d2e878ff622b..fb52b35d932e86c0abb32277a72767f15a745d46 100644 (file)
@@ -1,4 +1,5 @@
 import * as Sequelize from 'sequelize'
+import { Op } from 'sequelize'
 import * as Bluebird from 'bluebird'
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -200,4 +201,17 @@ export class VideoShareModel extends Model<VideoShareModel> {
 
     return VideoShareModel.findAndCountAll(query)
   }
+
+  static cleanOldSharesOf (videoId: number, beforeUpdatedAt: Date) {
+    const query = {
+      where: {
+        updatedAt: {
+          [Op.lt]: beforeUpdatedAt
+        },
+        videoId
+      }
+    }
+
+    return VideoShareModel.destroy(query)
+  }
 }
index fb037c21a19ec4819c0567510c87186d56c3a74b..b0d92b674ccc0bf310ae051c5fb1dd4e9d2d707c 100644 (file)
@@ -1547,7 +1547,7 @@ export class VideoModel extends Model<VideoModel> {
       attributes: query.attributes,
       order: [ // Keep original order
         Sequelize.literal(
-          ids.map(id => `"VideoModel".id = ${id}`).join(', ')
+          ids.map(id => `"VideoModel".id = ${id} DESC`).join(', ')
         )
       ]
     }
index ae485907631c5e92c0cce7b47fa7f264b513bb56..665a9f9f0fb0f5e01c6ec60c6fb4303507515648 100644 (file)
@@ -8,7 +8,7 @@ import {
   generateUserAccessToken,
   getVideo,
   getVideoPlaylist,
-  killallServers,
+  killallServers, rateVideo,
   reRunServer,
   ServerInfo,
   setAccessTokensToServers,
index 7e2fcb630ef43831c0ddfd96f4d9b4b9d3ca3d98..f91678140de1a3fa904a5276d32efaedbbfacd7d 100644 (file)
@@ -579,15 +579,15 @@ describe('Test multiple servers', function () {
       this.timeout(20000)
 
       await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'like')
-      await wait(200)
+      await wait(500)
       await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'dislike')
-      await wait(200)
+      await wait(500)
       await rateVideo(servers[0].url, servers[0].accessToken, remoteVideosServer1[0], 'like')
       await rateVideo(servers[2].url, servers[2].accessToken, localVideosServer3[1], 'like')
-      await wait(200)
+      await wait(500)
       await rateVideo(servers[2].url, servers[2].accessToken, localVideosServer3[1], 'dislike')
       await rateVideo(servers[2].url, servers[2].accessToken, remoteVideosServer3[1], 'dislike')
-      await wait(200)
+      await wait(500)
       await rateVideo(servers[2].url, servers[2].accessToken, remoteVideosServer3[0], 'like')
 
       await waitJobs(servers)