Add outbox
authorChocobozzz <florian.bigard@gmail.com>
Tue, 21 Nov 2017 17:23:10 +0000 (18:23 +0100)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 27 Nov 2017 18:40:53 +0000 (19:40 +0100)
server/controllers/activitypub/client.ts
server/controllers/activitypub/index.ts
server/controllers/activitypub/outbox.ts [new file with mode: 0644]
server/helpers/activitypub.ts
server/lib/activitypub/send/send-announce.ts
server/models/account/account-follow.ts
server/models/video/video-interface.ts
server/models/video/video.ts

index 7b3921770dcb80a0e61bbb1a2b15a1dbe55e2083..24c8665a5286ca182eb8027933d5e25ee86cfc7c 100644 (file)
@@ -6,7 +6,7 @@ import { executeIfActivityPub, localAccountValidator } from '../../middlewares'
 import { pageToStartAndCount } from '../../helpers'
 import { AccountInstance, VideoChannelInstance } from '../../models'
 import { activityPubCollectionPagination } from '../../helpers/activitypub'
-import { ACTIVITY_PUB } from '../../initializers/constants'
+import { ACTIVITY_PUB, CONFIG } from '../../initializers/constants'
 import { asyncMiddleware } from '../../middlewares/async'
 import { videosGetValidator } from '../../middlewares/validators/videos'
 import { VideoInstance } from '../../models/video/video-interface'
@@ -60,7 +60,7 @@ async function accountFollowersController (req: express.Request, res: express.Re
   const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
 
   const result = await db.AccountFollow.listAcceptedFollowerUrlsForApi([ account.id ], start, count)
-  const activityPubResult = activityPubCollectionPagination(req.url, page, result)
+  const activityPubResult = activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, page, result)
 
   return res.json(activityPubResult)
 }
@@ -72,7 +72,7 @@ async function accountFollowingController (req: express.Request, res: express.Re
   const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
 
   const result = await db.AccountFollow.listAcceptedFollowingUrlsForApi([ account.id ], start, count)
-  const activityPubResult = activityPubCollectionPagination(req.url, page, result)
+  const activityPubResult = activityPubCollectionPagination(CONFIG.WEBSERVER.URL + req.url, page, result)
 
   return res.json(activityPubResult)
 }
index c5bec64487758a9a2adaff78647148fbe077b42a..7e81902af7249147eeb2c8a3a4166bbca497b33a 100644 (file)
@@ -1,10 +1,12 @@
 import * as express from 'express'
 import { activityPubClientRouter } from './client'
 import { inboxRouter } from './inbox'
+import { outboxRouter } from './outbox'
 
 const activityPubRouter = express.Router()
 
 activityPubRouter.use('/', inboxRouter)
+activityPubRouter.use('/', outboxRouter)
 activityPubRouter.use('/', activityPubClientRouter)
 
 // ---------------------------------------------------------------------------
diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts
new file mode 100644 (file)
index 0000000..396fa2d
--- /dev/null
@@ -0,0 +1,60 @@
+import * as express from 'express'
+import { Activity, ActivityAdd } from '../../../shared/models/activitypub/activity'
+import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
+import { database as db } from '../../initializers'
+import { addActivityData } from '../../lib/activitypub/send/send-add'
+import { getAnnounceActivityPubUrl } from '../../lib/activitypub/url'
+import { announceActivityData } from '../../lib/index'
+import { asyncMiddleware, localAccountValidator } from '../../middlewares'
+import { AccountInstance } from '../../models/account/account-interface'
+import { pageToStartAndCount } from '../../helpers/core-utils'
+import { ACTIVITY_PUB } from '../../initializers/constants'
+
+const outboxRouter = express.Router()
+
+outboxRouter.get('/account/:name/outbox',
+  localAccountValidator,
+  asyncMiddleware(outboxController)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+  outboxRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function outboxController (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const account: AccountInstance = res.locals.account
+
+  const page = req.params.page || 1
+  const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
+
+  const data = await db.Video.listAllAndSharedByAccountForOutbox(account.id, start, count)
+  const activities: Activity[] = []
+
+  console.log(account.url)
+
+  for (const video of data.data) {
+    const videoObject = video.toActivityPubObject()
+    let addActivity: ActivityAdd = await addActivityData(video.url, account, video, video.VideoChannel.url, videoObject)
+
+    // This is a shared video
+    if (video.VideoShare !== undefined) {
+      const url = getAnnounceActivityPubUrl(video.url, account)
+      const announceActivity = await announceActivityData(url, account, addActivity)
+      activities.push(announceActivity)
+    } else {
+      activities.push(addActivity)
+    }
+  }
+
+  const newResult = {
+    data: activities,
+    total: data.total
+  }
+  const json = activityPubCollectionPagination(account.url + '/outbox', page, newResult)
+
+  return res.json(json).end()
+}
index 5c577bb612a559a3bf89bb8f8e4a32e639bc757e..04d85b8e619764758676b96e1de5fe472bbe6fde 100644 (file)
@@ -2,6 +2,7 @@ import { Activity } from '../../shared/models/activitypub/activity'
 import { ResultList } from '../../shared/models/result-list.model'
 import { AccountInstance } from '../models/account/account-interface'
 import { signObject } from './peertube-crypto'
+import { ACTIVITY_PUB } from '../initializers/constants'
 
 function activityPubContextify <T> (data: T) {
   return Object.assign(data,{
@@ -24,20 +25,32 @@ function activityPubContextify <T> (data: T) {
 }
 
 function activityPubCollectionPagination (url: string, page: number, result: ResultList<any>) {
-  const baseUrl = url.split('?').shift
+  let next: string
+  let prev: string
+
+  // There are more results
+  if (result.total > ((page + 1) * ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)) {
+    next = url + '?page=' + (page + 1)
+  }
+
+  if (page > 1) {
+    prev = url + '?page=' + (page - 1)
+  }
+
+  const orderedCollectionPagination = {
+    id: url + '?page=' + page,
+    type: 'OrderedCollectionPage',
+    prev,
+    next,
+    partOf: url,
+    orderedItems: result.data
+  }
 
   const obj = {
-    id: baseUrl,
-    type: 'Collection',
+    id: url,
+    type: 'OrderedCollection',
     totalItems: result.total,
-    first: {
-      id: baseUrl + '?page=' + page,
-      type: 'CollectionPage',
-      totalItems: result.total,
-      next: baseUrl + '?page=' + (page + 1),
-      partOf: baseUrl,
-      items: result.data
-    }
+    orderedItems: orderedCollectionPagination
   }
 
   return activityPubContextify(obj)
index 4b3a4ef758ea5438cab86bcbb698c616fb81b1ac..b8ea51bc08c81189946698eb88148e171d74b948 100644 (file)
@@ -1,10 +1,12 @@
 import { Transaction } from 'sequelize'
+import { ActivityAdd } from '../../../../shared/index'
+import { ActivityAnnounce, ActivityCreate } from '../../../../shared/models/activitypub/activity'
 import { AccountInstance, VideoInstance } from '../../../models'
 import { VideoChannelInstance } from '../../../models/video/video-channel-interface'
+import { getAnnounceActivityPubUrl } from '../url'
 import { broadcastToFollowers } from './misc'
 import { addActivityData } from './send-add'
 import { createActivityData } from './send-create'
-import { getAnnounceActivityPubUrl } from '../url'
 
 async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Transaction) {
   const url = getAnnounceActivityPubUrl(video.url, byAccount)
@@ -24,17 +26,8 @@ async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChanne
   return broadcastToFollowers(data, byAccount, [ byAccount ], t)
 }
 
-// ---------------------------------------------------------------------------
-
-export {
-  sendVideoAnnounce,
-  sendVideoChannelAnnounce
-}
-
-// ---------------------------------------------------------------------------
-
-async function announceActivityData (url: string, byAccount: AccountInstance, object: any) {
-  const activity = {
+async function announceActivityData (url: string, byAccount: AccountInstance, object: ActivityCreate | ActivityAdd) {
+  const activity: ActivityAnnounce = {
     type: 'Announce',
     id: url,
     actor: byAccount.url,
@@ -43,3 +36,11 @@ async function announceActivityData (url: string, byAccount: AccountInstance, ob
 
   return activity
 }
+
+// ---------------------------------------------------------------------------
+
+export {
+  sendVideoAnnounce,
+  sendVideoChannelAnnounce,
+  announceActivityData
+}
index 34ba3f8dbcb12649a74ee7b373f16a524fa72083..578bcda39b22b3d990b5b17872303296cd323842 100644 (file)
@@ -221,8 +221,8 @@ async function createListAcceptedFollowForApiQuery (
       'INNER JOIN "Accounts" AS "Follows" ON "AccountFollows"."' + secondJoin + '" = "Follows"."id" ' +
       'WHERE "Accounts"."id" = ANY ($accountIds) AND "AccountFollows"."state" = \'accepted\' '
 
-    if (start !== undefined) query += 'LIMIT ' + start
-    if (count !== undefined) query += ', ' + count
+    if (count !== undefined) query += 'LIMIT ' + count
+    if (start !== undefined) query += ' OFFSET ' + start
 
     const options = {
       bind: { accountIds },
index 9f29c842cc972a807b6fb0348a06c00ec1f5bc69..391ecff4390c2b7525fb8e29fbe2a1ea43d52707 100644 (file)
@@ -7,6 +7,7 @@ import { Video as FormattedVideo, VideoDetails as FormattedDetailsVideo } from '
 import { TagAttributes, TagInstance } from './tag-interface'
 import { VideoChannelInstance } from './video-channel-interface'
 import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
+import { VideoShareInstance } from './video-share-interface'
 
 export namespace VideoMethods {
   export type GetThumbnailName = (this: VideoInstance) => string
@@ -44,6 +45,11 @@ export namespace VideoMethods {
   export type ListOwnedAndPopulateAccountAndTags = () => Bluebird<VideoInstance[]>
   export type ListOwnedByAccount = (account: string) => Bluebird<VideoInstance[]>
 
+  export type ListAllAndSharedByAccountForOutbox = (
+    accountId: number,
+    start: number,
+    count: number
+  ) => Bluebird< ResultList<VideoInstance> >
   export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
   export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList<VideoInstance> >
   export type SearchAndPopulateAccountAndServerAndTags = (
@@ -73,6 +79,7 @@ export namespace VideoMethods {
 export interface VideoClass {
   generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
   list: VideoMethods.List
+  listAllAndSharedByAccountForOutbox: VideoMethods.ListAllAndSharedByAccountForOutbox
   listForApi: VideoMethods.ListForApi
   listUserVideosForApi: VideoMethods.ListUserVideosForApi
   listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
@@ -115,6 +122,7 @@ export interface VideoAttributes {
   VideoChannel?: VideoChannelInstance
   Tags?: TagInstance[]
   VideoFiles?: VideoFileInstance[]
+  VideoShare?: VideoShareInstance
 }
 
 export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.Instance<VideoAttributes> {
index e2069eb0c6d8110280433e4ba978616f627649a2..3b7e83779a0437fdc3785a00ebac9fecab2b1998 100644 (file)
@@ -78,6 +78,7 @@ let getLanguageLabel: VideoMethods.GetLanguageLabel
 let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
 let list: VideoMethods.List
 let listForApi: VideoMethods.ListForApi
+let listAllAndSharedByAccountForOutbox: VideoMethods.ListAllAndSharedByAccountForOutbox
 let listUserVideosForApi: VideoMethods.ListUserVideosForApi
 let loadByHostAndUUID: VideoMethods.LoadByHostAndUUID
 let listOwnedAndPopulateAccountAndTags: VideoMethods.ListOwnedAndPopulateAccountAndTags
@@ -266,6 +267,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
 
     generateThumbnailFromData,
     list,
+    listAllAndSharedByAccountForOutbox,
     listForApi,
     listUserVideosForApi,
     listOwnedAndPopulateAccountAndTags,
@@ -348,6 +350,14 @@ function associate (models) {
     },
     onDelete: 'cascade'
   })
+
+  Video.hasMany(models.VideoShare, {
+    foreignKey: {
+      name: 'videoId',
+      allowNull: false
+    },
+    onDelete: 'cascade'
+  })
 }
 
 function afterDestroy (video: VideoInstance) {
@@ -775,6 +785,54 @@ list = function () {
   return Video.findAll(query)
 }
 
+listAllAndSharedByAccountForOutbox = function (accountId: number, start: number, count: number) {
+  const queryVideo = 'SELECT "Video"."id" FROM "Videos" AS "Video" ' +
+                'INNER JOIN "VideoChannels" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
+                'WHERE "VideoChannel"."accountId" = ' + accountId
+  const queryVideoShare = 'SELECT "Video"."id" FROM "VideoShares" AS "VideoShare" ' +
+                          'INNER JOIN "Videos" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
+                          'INNER JOIN "VideoChannels" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
+                          'WHERE "VideoShare"."accountId" = ' + accountId
+  const rawQuery = `(${queryVideo}) UNION (${queryVideoShare}) LIMIT ${count} OFFSET ${start}`
+
+  const query = {
+    distinct: true,
+    offset: start,
+    limit: count,
+    order: [ getSort('createdAt'), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ],
+    where: {
+      id: {
+        [Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')')
+      }
+    },
+    include: [
+      {
+        model: Video['sequelize'].models.VideoShare,
+        required: false
+      },
+      {
+        model: Video['sequelize'].models.VideoChannel,
+        required: true,
+        include: [
+          {
+            model: Video['sequelize'].models.Account,
+            required: true
+          }
+        ]
+      },
+      Video['sequelize'].models.Tag,
+      Video['sequelize'].models.VideoFile
+    ]
+  }
+
+  return Video.findAndCountAll(query).then(({ rows, count }) => {
+    return {
+      data: rows,
+      total: count
+    }
+  })
+}
+
 listUserVideosForApi = function (userId: number, start: number, count: number, sort: string) {
   const query = {
     distinct: true,