From e71bcc0f4b31ecfd84a786411febfc6d18a85258 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 21 Nov 2017 18:23:10 +0100 Subject: [PATCH] Add outbox --- server/controllers/activitypub/client.ts | 6 +- server/controllers/activitypub/index.ts | 2 + server/controllers/activitypub/outbox.ts | 60 ++++++++++++++++++++ server/helpers/activitypub.ts | 35 ++++++++---- server/lib/activitypub/send/send-announce.ts | 25 ++++---- server/models/account/account-follow.ts | 4 +- server/models/video/video-interface.ts | 8 +++ server/models/video/video.ts | 58 +++++++++++++++++++ 8 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 server/controllers/activitypub/outbox.ts diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 7b3921770..24c8665a5 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -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) } diff --git a/server/controllers/activitypub/index.ts b/server/controllers/activitypub/index.ts index c5bec6448..7e81902af 100644 --- a/server/controllers/activitypub/index.ts +++ b/server/controllers/activitypub/index.ts @@ -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 index 000000000..396fa2db5 --- /dev/null +++ b/server/controllers/activitypub/outbox.ts @@ -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() +} diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index 5c577bb61..04d85b8e6 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts @@ -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 (data: T) { return Object.assign(data,{ @@ -24,20 +25,32 @@ function activityPubContextify (data: T) { } function activityPubCollectionPagination (url: string, page: number, result: ResultList) { - 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) diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts index 4b3a4ef75..b8ea51bc0 100644 --- a/server/lib/activitypub/send/send-announce.ts +++ b/server/lib/activitypub/send/send-announce.ts @@ -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 +} diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts index 34ba3f8db..578bcda39 100644 --- a/server/models/account/account-follow.ts +++ b/server/models/account/account-follow.ts @@ -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 }, diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index 9f29c842c..391ecff43 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts @@ -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 export type ListOwnedByAccount = (account: string) => Bluebird + export type ListAllAndSharedByAccountForOutbox = ( + accountId: number, + start: number, + count: number + ) => Bluebird< ResultList > export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList > export type ListUserVideosForApi = (userId: number, start: number, count: number, sort: string) => Bluebird< ResultList > 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 { diff --git a/server/models/video/video.ts b/server/models/video/video.ts index e2069eb0c..3b7e83779 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -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,