diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts index fd3695886..807d0bdf4 100644 --- a/server/controllers/activitypub/inbox.ts +++ b/server/controllers/activitypub/inbox.ts @@ -2,12 +2,12 @@ import * as express from 'express' import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, ActivityType, RootActivity } from '../../../shared' import { logger } from '../../helpers' import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' -import { processCreateActivity, processUpdateActivity } from '../../lib' -import { processAcceptActivity } from '../../lib/activitypub/process-accept' -import { processAddActivity } from '../../lib/activitypub/process-add' -import { processAnnounceActivity } from '../../lib/activitypub/process-announce' -import { processDeleteActivity } from '../../lib/activitypub/process-delete' -import { processFollowActivity } from '../../lib/activitypub/process-follow' +import { processCreateActivity, processUpdateActivity, processUndoActivity } from '../../lib' +import { processAcceptActivity } from '../../lib/activitypub/process/process-accept' +import { processAddActivity } from '../../lib/activitypub/process/process-add' +import { processAnnounceActivity } from '../../lib/activitypub/process/process-announce' +import { processDeleteActivity } from '../../lib/activitypub/process/process-delete' +import { processFollowActivity } from '../../lib/activitypub/process/process-follow' import { asyncMiddleware, checkSignature, localAccountValidator, signatureValidator } from '../../middlewares' import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' import { AccountInstance } from '../../models/account/account-interface' @@ -19,7 +19,8 @@ const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccoun Delete: processDeleteActivity, Follow: processFollowActivity, Accept: processAcceptActivity, - Announce: processAnnounceActivity + Announce: processAnnounceActivity, + Undo: processUndoActivity } const inboxRouter = express.Router() diff --git a/server/controllers/api/server/follows.ts b/server/controllers/api/server/follows.ts index 3d184ec1f..8fc70f34f 100644 --- a/server/controllers/api/server/follows.ts +++ b/server/controllers/api/server/follows.ts @@ -6,14 +6,16 @@ import { getServerAccount } from '../../../helpers/utils' import { getAccountFromWebfinger } from '../../../helpers/webfinger' import { SERVER_ACCOUNT_NAME } from '../../../initializers/constants' import { database as db } from '../../../initializers/database' -import { sendFollow } from '../../../lib/activitypub/send-request' -import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../../middlewares' +import { asyncMiddleware, paginationValidator, removeFollowingValidator, setFollowersSort, setPagination } from '../../../middlewares' import { authenticate } from '../../../middlewares/oauth' import { setBodyHostsPort } from '../../../middlewares/servers' import { setFollowingSort } from '../../../middlewares/sort' import { ensureUserHasRight } from '../../../middlewares/user-right' -import { followValidator } from '../../../middlewares/validators/servers' +import { followValidator } from '../../../middlewares/validators/follows' import { followersSortValidator, followingSortValidator } from '../../../middlewares/validators/sort' +import { AccountFollowInstance } from '../../../models/index' +import { sendFollow } from '../../../lib/index' +import { sendUndoFollow } from '../../../lib/activitypub/send/send-undo' const serverFollowsRouter = express.Router() @@ -33,6 +35,13 @@ serverFollowsRouter.post('/following', asyncMiddleware(follow) ) +serverFollowsRouter.delete('/following/:accountId', + authenticate, + ensureUserHasRight(UserRight.MANAGE_SERVER_FOLLOW), + removeFollowingValidator, + asyncMiddleware(removeFollow) +) + serverFollowsRouter.get('/followers', paginationValidator, followersSortValidator, @@ -96,10 +105,12 @@ async function follow (req: express.Request, res: express.Response, next: expres }, transaction: t }) + accountFollow.AccountFollowing = targetAccount + accountFollow.AccountFollower = fromAccount // Send a notification to remote server if (accountFollow.state === 'pending') { - await sendFollow(fromAccount, targetAccount, t) + await sendFollow(accountFollow, t) } }) }) @@ -117,6 +128,17 @@ async function follow (req: express.Request, res: express.Response, next: expres return res.status(204).end() } +async function removeFollow (req: express.Request, res: express.Response, next: express.NextFunction) { + const following: AccountFollowInstance = res.locals.following + + await db.sequelize.transaction(async t => { + await sendUndoFollow(following, t) + await following.destroy({ transaction: t }) + }) + + return res.status(204).end() +} + async function loadLocalOrGetAccountFromWebfinger (name: string, host: string) { let loadedFromDB = true let account = await db.Account.loadByNameAndHost(name, host) diff --git a/server/controllers/api/videos/channel.ts b/server/controllers/api/videos/channel.ts index 8f3df2550..ce2656e71 100644 --- a/server/controllers/api/videos/channel.ts +++ b/server/controllers/api/videos/channel.ts @@ -17,7 +17,7 @@ import { videoChannelsUpdateValidator } from '../../../middlewares' import { AccountInstance, VideoChannelInstance } from '../../../models' -import { sendUpdateVideoChannel } from '../../../lib/activitypub/send-request' +import { sendUpdateVideoChannel } from '../../../lib/activitypub/send/send-update' const videoChannelRouter = express.Router() @@ -128,9 +128,9 @@ async function updateVideoChannel (req: express.Request, res: express.Response) if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name) if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description) - await videoChannelInstance.save(sequelizeOptions) + const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) - await sendUpdateVideoChannel(videoChannelInstance, t) + await sendUpdateVideoChannel(videoChannelInstanceUpdated, t) }) logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.uuid) diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 22a88620a..8c9b0aa50 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -12,10 +12,11 @@ import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers' -import { getActivityPubUrl, shareVideoByServer } from '../../../helpers/activitypub' +import { getVideoActivityPubUrl, shareVideoByServer } from '../../../helpers/activitypub' import { CONFIG, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_MIMETYPE_EXT, VIDEO_PRIVACIES } from '../../../initializers' import { database as db } from '../../../initializers/database' -import { sendAddVideo, sendUpdateVideo } from '../../../lib/activitypub/send-request' +import { sendAddVideo } from '../../../lib/activitypub/send/send-add' +import { sendUpdateVideo } from '../../../lib/activitypub/send/send-update' import { transcodingJobScheduler } from '../../../lib/jobs/transcoding-job-scheduler/transcoding-job-scheduler' import { asyncMiddleware, @@ -175,7 +176,7 @@ async function addVideo (req: express.Request, res: express.Response, videoPhysi channelId: res.locals.videoChannel.id } const video = db.Video.build(videoData) - video.url = getActivityPubUrl('video', video.uuid) + video.url = getVideoActivityPubUrl(video) const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename) const videoFileHeight = await getVideoFileHeight(videoFilePath) @@ -274,7 +275,7 @@ async function updateVideo (req: express.Request, res: express.Response) { if (videoInfoToUpdate.privacy !== undefined) videoInstance.set('privacy', videoInfoToUpdate.privacy) if (videoInfoToUpdate.description !== undefined) videoInstance.set('description', videoInfoToUpdate.description) - await videoInstance.save(sequelizeOptions) + const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) if (videoInfoToUpdate.tags) { const tagInstances = await db.Tag.findOrCreateTags(videoInfoToUpdate.tags, t) @@ -285,7 +286,7 @@ async function updateVideo (req: express.Request, res: express.Response) { // Now we'll update the video's meta data to our friends if (wasPrivateVideo === false) { - await sendUpdateVideo(videoInstance, t) + await sendUpdateVideo(videoInstanceUpdated, t) } // Video is not private anymore, send a create action to remote servers diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index aff58515a..9622a1801 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts @@ -9,18 +9,20 @@ import { VideoChannelObject } from '../../shared/models/activitypub/objects/vide import { ResultList } from '../../shared/models/result-list.model' import { database as db, REMOTE_SCHEME } from '../initializers' import { ACTIVITY_PUB, CONFIG, STATIC_PATHS } from '../initializers/constants' -import { videoChannelActivityObjectToDBAttributes } from '../lib/activitypub/misc' -import { sendVideoAnnounce } from '../lib/activitypub/send-request' +import { videoChannelActivityObjectToDBAttributes } from '../lib/activitypub/process/misc' +import { sendVideoAnnounce } from '../lib/activitypub/send/send-announce' import { sendVideoChannelAnnounce } from '../lib/index' +import { AccountFollowInstance } from '../models/account/account-follow-interface' import { AccountInstance } from '../models/account/account-interface' +import { VideoAbuseInstance } from '../models/video/video-abuse-interface' import { VideoChannelInstance } from '../models/video/video-channel-interface' import { VideoInstance } from '../models/video/video-interface' import { isRemoteAccountValid } from './custom-validators' -import { isVideoChannelObjectValid } from './custom-validators/activitypub/videos' import { logger } from './logger' import { signObject } from './peertube-crypto' import { doRequest, doRequestAndSaveToFile } from './requests' import { getServerAccount } from './utils' +import { isVideoChannelObjectValid } from './custom-validators/activitypub/video-channels' function generateThumbnailFromUrl (video: VideoInstance, icon: ActivityIconObject) { const thumbnailName = video.getThumbnailName() @@ -55,13 +57,46 @@ async function shareVideoByServer (video: VideoInstance, t: Sequelize.Transactio return sendVideoAnnounce(serverAccount, video, t) } -function getActivityPubUrl (type: 'video' | 'videoChannel' | 'account' | 'videoAbuse', id: string) { - if (type === 'video') return CONFIG.WEBSERVER.URL + '/videos/watch/' + id - else if (type === 'videoChannel') return CONFIG.WEBSERVER.URL + '/video-channels/' + id - else if (type === 'account') return CONFIG.WEBSERVER.URL + '/account/' + id - else if (type === 'videoAbuse') return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + id +function getVideoActivityPubUrl (video: VideoInstance) { + return CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid +} - return '' +function getVideoChannelActivityPubUrl (videoChannel: VideoChannelInstance) { + return CONFIG.WEBSERVER.URL + '/video-channels/' + videoChannel.uuid +} + +function getAccountActivityPubUrl (accountName: string) { + return CONFIG.WEBSERVER.URL + '/account/' + accountName +} + +function getVideoAbuseActivityPubUrl (videoAbuse: VideoAbuseInstance) { + return CONFIG.WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id +} + +function getAccountFollowActivityPubUrl (accountFollow: AccountFollowInstance) { + const me = accountFollow.AccountFollower + const following = accountFollow.AccountFollowing + + return me.url + '#follows/' + following.id +} + +function getAccountFollowAcceptActivityPubUrl (accountFollow: AccountFollowInstance) { + const follower = accountFollow.AccountFollower + const me = accountFollow.AccountFollowing + + return follower.url + '#accepts/follows/' + me.id +} + +function getAnnounceActivityPubUrl (originalUrl: string, byAccount: AccountInstance) { + return originalUrl + '#announces/' + byAccount.id +} + +function getUpdateActivityPubUrl (originalUrl: string, updatedAt: string) { + return originalUrl + '#updates/' + updatedAt +} + +function getUndoActivityPubUrl (originalUrl: string) { + return originalUrl + '/undo' } async function getOrCreateAccount (accountUrl: string) { @@ -257,7 +292,6 @@ export { fetchRemoteAccountAndCreateServer, activityPubContextify, activityPubCollectionPagination, - getActivityPubUrl, generateThumbnailFromUrl, getOrCreateAccount, fetchRemoteVideoPreview, @@ -265,7 +299,16 @@ export { shareVideoChannelByServer, shareVideoByServer, getOrCreateVideoChannel, - buildSignedActivity + buildSignedActivity, + getVideoActivityPubUrl, + getVideoChannelActivityPubUrl, + getAccountActivityPubUrl, + getVideoAbuseActivityPubUrl, + getAccountFollowActivityPubUrl, + getAccountFollowAcceptActivityPubUrl, + getAnnounceActivityPubUrl, + getUpdateActivityPubUrl, + getUndoActivityPubUrl } // --------------------------------------------------------------------------- diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index 8084cf7b0..9305e092c 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts @@ -1,11 +1,11 @@ import * as validator from 'validator' +import { Activity, ActivityType } from '../../../../shared/models/activitypub/activity' import { isAccountAcceptActivityValid, isAccountDeleteActivityValid, isAccountFollowActivityValid } from './account' +import { isAnnounceValid } from './announce' import { isActivityPubUrlValid } from './misc' +import { isUndoValid } from './undo' +import { isVideoChannelCreateActivityValid, isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels' import { - isAnnounceValid, - isVideoChannelCreateActivityValid, - isVideoChannelDeleteActivityValid, - isVideoChannelUpdateActivityValid, isVideoFlagValid, isVideoTorrentAddActivityValid, isVideoTorrentDeleteActivityValid, @@ -25,18 +25,23 @@ function isRootActivityValid (activity: any) { ) } +const activityCheckers: { [ P in ActivityType ]: (activity: Activity) => boolean } = { + Create: checkCreateActivity, + Add: checkAddActivity, + Update: checkUpdateActivity, + Delete: checkDeleteActivity, + Follow: checkFollowActivity, + Accept: checkAcceptActivity, + Announce: checkAnnounceActivity, + Undo: checkUndoActivity +} + function isActivityValid (activity: any) { - return isVideoTorrentAddActivityValid(activity) || - isVideoChannelCreateActivityValid(activity) || - isVideoTorrentUpdateActivityValid(activity) || - isVideoChannelUpdateActivityValid(activity) || - isVideoTorrentDeleteActivityValid(activity) || - isVideoChannelDeleteActivityValid(activity) || - isAccountDeleteActivityValid(activity) || - isAccountFollowActivityValid(activity) || - isAccountAcceptActivityValid(activity) || - isVideoFlagValid(activity) || - isAnnounceValid(activity) + const checker = activityCheckers[activity.type] + // Unknown activity type + if (!checker) return false + + return checker(activity) } // --------------------------------------------------------------------------- @@ -45,3 +50,41 @@ export { isRootActivityValid, isActivityValid } + +// --------------------------------------------------------------------------- + +function checkCreateActivity (activity: any) { + return isVideoChannelCreateActivityValid(activity) || + isVideoFlagValid(activity) +} + +function checkAddActivity (activity: any) { + return isVideoTorrentAddActivityValid(activity) +} + +function checkUpdateActivity (activity: any) { + return isVideoTorrentUpdateActivityValid(activity) || + isVideoChannelUpdateActivityValid(activity) +} + +function checkDeleteActivity (activity: any) { + return isVideoTorrentDeleteActivityValid(activity) || + isVideoChannelDeleteActivityValid(activity) || + isAccountDeleteActivityValid(activity) +} + +function checkFollowActivity (activity: any) { + return isAccountFollowActivityValid(activity) +} + +function checkAcceptActivity (activity: any) { + return isAccountAcceptActivityValid(activity) +} + +function checkAnnounceActivity (activity: any) { + return isAnnounceValid(activity) +} + +function checkUndoActivity (activity: any) { + return isUndoValid(activity) +} diff --git a/server/helpers/custom-validators/activitypub/announce.ts b/server/helpers/custom-validators/activitypub/announce.ts new file mode 100644 index 000000000..4ba99d1ea --- /dev/null +++ b/server/helpers/custom-validators/activitypub/announce.ts @@ -0,0 +1,15 @@ +import { isBaseActivityValid } from './misc' +import { isVideoTorrentAddActivityValid } from './videos' +import { isVideoChannelCreateActivityValid } from './video-channels' + +function isAnnounceValid (activity: any) { + return isBaseActivityValid(activity, 'Announce') && + ( + isVideoChannelCreateActivityValid(activity.object) || + isVideoTorrentAddActivityValid(activity.object) + ) +} + +export { + isAnnounceValid +} diff --git a/server/helpers/custom-validators/activitypub/index.ts b/server/helpers/custom-validators/activitypub/index.ts index 0eba06a7b..6685b269f 100644 --- a/server/helpers/custom-validators/activitypub/index.ts +++ b/server/helpers/custom-validators/activitypub/index.ts @@ -1,5 +1,7 @@ export * from './account' export * from './activity' -export * from './signature' export * from './misc' +export * from './signature' +export * from './undo' +export * from './video-channels' export * from './videos' diff --git a/server/helpers/custom-validators/activitypub/undo.ts b/server/helpers/custom-validators/activitypub/undo.ts new file mode 100644 index 000000000..a9a2a3a41 --- /dev/null +++ b/server/helpers/custom-validators/activitypub/undo.ts @@ -0,0 +1,13 @@ +import { isAccountFollowActivityValid } from './account' +import { isBaseActivityValid } from './misc' + +function isUndoValid (activity: any) { + return isBaseActivityValid(activity, 'Undo') && + ( + isAccountFollowActivityValid(activity.object) + ) +} + +export { + isUndoValid +} diff --git a/server/helpers/custom-validators/activitypub/video-channels.ts b/server/helpers/custom-validators/activitypub/video-channels.ts new file mode 100644 index 000000000..9fd3bb149 --- /dev/null +++ b/server/helpers/custom-validators/activitypub/video-channels.ts @@ -0,0 +1,36 @@ +import { isDateValid, isUUIDValid } from '../misc' +import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' +import { isActivityPubUrlValid, isBaseActivityValid } from './misc' + +function isVideoChannelCreateActivityValid (activity: any) { + return isBaseActivityValid(activity, 'Create') && + isVideoChannelObjectValid(activity.object) +} + +function isVideoChannelUpdateActivityValid (activity: any) { + return isBaseActivityValid(activity, 'Update') && + isVideoChannelObjectValid(activity.object) +} + +function isVideoChannelDeleteActivityValid (activity: any) { + return isBaseActivityValid(activity, 'Delete') +} + +function isVideoChannelObjectValid (videoChannel: any) { + return videoChannel.type === 'VideoChannel' && + isActivityPubUrlValid(videoChannel.id) && + isVideoChannelNameValid(videoChannel.name) && + isVideoChannelDescriptionValid(videoChannel.content) && + isDateValid(videoChannel.published) && + isDateValid(videoChannel.updated) && + isUUIDValid(videoChannel.uuid) +} + +// --------------------------------------------------------------------------- + +export { + isVideoChannelCreateActivityValid, + isVideoChannelUpdateActivityValid, + isVideoChannelDeleteActivityValid, + isVideoChannelObjectValid +} diff --git a/server/helpers/custom-validators/activitypub/videos.ts b/server/helpers/custom-validators/activitypub/videos.ts index 728511e3d..faeedd3df 100644 --- a/server/helpers/custom-validators/activitypub/videos.ts +++ b/server/helpers/custom-validators/activitypub/videos.ts @@ -1,7 +1,6 @@ import * as validator from 'validator' import { ACTIVITY_PUB } from '../../../initializers' import { exists, isDateValid, isUUIDValid } from '../misc' -import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' import { isVideoAbuseReasonValid, isVideoDurationValid, @@ -28,6 +27,13 @@ function isVideoTorrentDeleteActivityValid (activity: any) { return isBaseActivityValid(activity, 'Delete') } +function isVideoFlagValid (activity: any) { + return isBaseActivityValid(activity, 'Create') && + activity.object.type === 'Flag' && + isVideoAbuseReasonValid(activity.object.content) && + isActivityPubUrlValid(activity.object.object) +} + function isActivityPubVideoDurationValid (value: string) { // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration return exists(value) && @@ -57,57 +63,13 @@ function isVideoTorrentObjectValid (video: any) { video.url.length !== 0 } -function isVideoFlagValid (activity: any) { - return isBaseActivityValid(activity, 'Create') && - activity.object.type === 'Flag' && - isVideoAbuseReasonValid(activity.object.content) && - isActivityPubUrlValid(activity.object.object) -} - -function isAnnounceValid (activity: any) { - return isBaseActivityValid(activity, 'Announce') && - ( - isVideoChannelCreateActivityValid(activity.object) || - isVideoTorrentAddActivityValid(activity.object) - ) -} - -function isVideoChannelCreateActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Create') && - isVideoChannelObjectValid(activity.object) -} - -function isVideoChannelUpdateActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Update') && - isVideoChannelObjectValid(activity.object) -} - -function isVideoChannelDeleteActivityValid (activity: any) { - return isBaseActivityValid(activity, 'Delete') -} - -function isVideoChannelObjectValid (videoChannel: any) { - return videoChannel.type === 'VideoChannel' && - isActivityPubUrlValid(videoChannel.id) && - isVideoChannelNameValid(videoChannel.name) && - isVideoChannelDescriptionValid(videoChannel.content) && - isDateValid(videoChannel.published) && - isDateValid(videoChannel.updated) && - isUUIDValid(videoChannel.uuid) -} - // --------------------------------------------------------------------------- export { isVideoTorrentAddActivityValid, - isVideoChannelCreateActivityValid, isVideoTorrentUpdateActivityValid, - isVideoChannelUpdateActivityValid, - isVideoChannelDeleteActivityValid, isVideoTorrentDeleteActivityValid, - isVideoFlagValid, - isAnnounceValid, - isVideoChannelObjectValid + isVideoFlagValid } // --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/index.ts b/server/lib/activitypub/index.ts index e08108aac..1bea0a412 100644 --- a/server/lib/activitypub/index.ts +++ b/server/lib/activitypub/index.ts @@ -1,8 +1,2 @@ -export * from './process-accept' -export * from './process-add' -export * from './process-announce' -export * from './process-create' -export * from './process-delete' -export * from './process-follow' -export * from './process-update' -export * from './send-request' +export * from './process' +export * from './send' diff --git a/server/lib/activitypub/process/index.ts b/server/lib/activitypub/process/index.ts new file mode 100644 index 000000000..e80b46b6f --- /dev/null +++ b/server/lib/activitypub/process/index.ts @@ -0,0 +1,8 @@ +export * from './process-accept' +export * from './process-add' +export * from './process-announce' +export * from './process-create' +export * from './process-delete' +export * from './process-follow' +export * from './process-undo' +export * from './process-update' diff --git a/server/lib/activitypub/misc.ts b/server/lib/activitypub/process/misc.ts similarity index 80% rename from server/lib/activitypub/misc.ts rename to server/lib/activitypub/process/misc.ts index 4c210eb10..e90a793fc 100644 --- a/server/lib/activitypub/misc.ts +++ b/server/lib/activitypub/process/misc.ts @@ -1,13 +1,13 @@ import * as magnetUtil from 'magnet-uri' -import { VideoTorrentObject } from '../../../shared' -import { VideoChannelObject } from '../../../shared/models/activitypub/objects/video-channel-object' -import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos' -import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../initializers/constants' -import { AccountInstance } from '../../models/account/account-interface' -import { VideoChannelInstance } from '../../models/video/video-channel-interface' -import { VideoFileAttributes } from '../../models/video/video-file-interface' -import { VideoAttributes, VideoInstance } from '../../models/video/video-interface' -import { VideoPrivacy } from '../../../shared/models/videos/video-privacy.enum' +import { VideoTorrentObject } from '../../../../shared' +import { VideoChannelObject } from '../../../../shared/models/activitypub/objects/video-channel-object' +import { isVideoFileInfoHashValid } from '../../../helpers/custom-validators/videos' +import { ACTIVITY_PUB, VIDEO_MIMETYPE_EXT } from '../../../initializers/constants' +import { AccountInstance } from '../../../models/account/account-interface' +import { VideoChannelInstance } from '../../../models/video/video-channel-interface' +import { VideoFileAttributes } from '../../../models/video/video-file-interface' +import { VideoAttributes, VideoInstance } from '../../../models/video/video-interface' +import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' function videoChannelActivityObjectToDBAttributes (videoChannelObject: VideoChannelObject, account: AccountInstance) { return { diff --git a/server/lib/activitypub/process-accept.ts b/server/lib/activitypub/process/process-accept.ts similarity index 79% rename from server/lib/activitypub/process-accept.ts rename to server/lib/activitypub/process/process-accept.ts index 9e0cd4032..e159c41b5 100644 --- a/server/lib/activitypub/process-accept.ts +++ b/server/lib/activitypub/process/process-accept.ts @@ -1,6 +1,6 @@ -import { ActivityAccept } from '../../../shared/models/activitypub/activity' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' +import { ActivityAccept } from '../../../../shared/models/activitypub/activity' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: AccountInstance) { if (inboxAccount === undefined) throw new Error('Need to accept on explicit inbox.') diff --git a/server/lib/activitypub/process-add.ts b/server/lib/activitypub/process/process-add.ts similarity index 86% rename from server/lib/activitypub/process-add.ts rename to server/lib/activitypub/process/process-add.ts index e1769bee8..f064c1ab6 100644 --- a/server/lib/activitypub/process-add.ts +++ b/server/lib/activitypub/process/process-add.ts @@ -1,11 +1,11 @@ import * as Bluebird from 'bluebird' -import { VideoTorrentObject } from '../../../shared' -import { ActivityAdd } from '../../../shared/models/activitypub/activity' -import { generateThumbnailFromUrl, getOrCreateAccount, logger, retryTransactionWrapper } from '../../helpers' -import { getOrCreateVideoChannel } from '../../helpers/activitypub' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' -import { VideoChannelInstance } from '../../models/video/video-channel-interface' +import { VideoTorrentObject } from '../../../../shared' +import { ActivityAdd } from '../../../../shared/models/activitypub/activity' +import { generateThumbnailFromUrl, getOrCreateAccount, logger, retryTransactionWrapper } from '../../../helpers' +import { getOrCreateVideoChannel } from '../../../helpers/activitypub' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' +import { VideoChannelInstance } from '../../../models/video/video-channel-interface' import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' async function processAddActivity (activity: ActivityAdd) { diff --git a/server/lib/activitypub/process-announce.ts b/server/lib/activitypub/process/process-announce.ts similarity index 75% rename from server/lib/activitypub/process-announce.ts rename to server/lib/activitypub/process/process-announce.ts index eb38aecca..656db08a9 100644 --- a/server/lib/activitypub/process-announce.ts +++ b/server/lib/activitypub/process/process-announce.ts @@ -1,9 +1,9 @@ -import { ActivityAnnounce } from '../../../shared/models/activitypub/activity' -import { getOrCreateAccount } from '../../helpers/activitypub' -import { logger } from '../../helpers/logger' -import { database as db } from '../../initializers/index' -import { VideoInstance } from '../../models/index' -import { VideoChannelInstance } from '../../models/video/video-channel-interface' +import { ActivityAnnounce } from '../../../../shared/models/activitypub/activity' +import { getOrCreateAccount } from '../../../helpers/activitypub' +import { logger } from '../../../helpers/logger' +import { database as db } from '../../../initializers/index' +import { VideoInstance } from '../../../models/index' +import { VideoChannelInstance } from '../../../models/video/video-channel-interface' import { processAddActivity } from './process-add' import { processCreateActivity } from './process-create' @@ -35,7 +35,8 @@ async function processAnnounceActivity (activity: ActivityAnnounce) { 'Unknown activity object type %s -> %s when announcing activity.', announcedActivity.type, announcedActivity.object.type, { activity: activity.id } ) - return Promise.resolve(undefined) + + return undefined } // --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/process-create.ts b/server/lib/activitypub/process/process-create.ts similarity index 85% rename from server/lib/activitypub/process-create.ts rename to server/lib/activitypub/process/process-create.ts index de8e09adf..aac941a6c 100644 --- a/server/lib/activitypub/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -1,9 +1,9 @@ -import { ActivityCreate, VideoChannelObject } from '../../../shared' -import { VideoAbuseObject } from '../../../shared/models/activitypub/objects/video-abuse-object' -import { logger, retryTransactionWrapper } from '../../helpers' -import { getActivityPubUrl, getOrCreateAccount } from '../../helpers/activitypub' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' +import { ActivityCreate, VideoChannelObject } from '../../../../shared' +import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects/video-abuse-object' +import { logger, retryTransactionWrapper } from '../../../helpers' +import { getOrCreateAccount, getVideoChannelActivityPubUrl } from '../../../helpers/activitypub' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' import { videoChannelActivityObjectToDBAttributes } from './misc' async function processCreateActivity (activity: ActivityCreate) { @@ -47,7 +47,7 @@ function addRemoteVideoChannel (account: AccountInstance, videoChannelToCreateDa const videoChannelData = videoChannelActivityObjectToDBAttributes(videoChannelToCreateData, account) videoChannel = db.VideoChannel.build(videoChannelData) - videoChannel.url = getActivityPubUrl('videoChannel', videoChannel.uuid) + videoChannel.url = getVideoChannelActivityPubUrl(videoChannel) videoChannel = await videoChannel.save({ transaction: t }) logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid) diff --git a/server/lib/activitypub/process-delete.ts b/server/lib/activitypub/process/process-delete.ts similarity index 84% rename from server/lib/activitypub/process-delete.ts rename to server/lib/activitypub/process/process-delete.ts index 0d5756e9c..af5d964d4 100644 --- a/server/lib/activitypub/process-delete.ts +++ b/server/lib/activitypub/process/process-delete.ts @@ -1,11 +1,11 @@ -import { ActivityDelete } from '../../../shared/models/activitypub/activity' -import { getOrCreateAccount } from '../../helpers/activitypub' -import { retryTransactionWrapper } from '../../helpers/database-utils' -import { logger } from '../../helpers/logger' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' -import { VideoChannelInstance } from '../../models/video/video-channel-interface' -import { VideoInstance } from '../../models/video/video-interface' +import { ActivityDelete } from '../../../../shared/models/activitypub/activity' +import { getOrCreateAccount } from '../../../helpers/activitypub' +import { retryTransactionWrapper } from '../../../helpers/database-utils' +import { logger } from '../../../helpers/logger' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' +import { VideoChannelInstance } from '../../../models/video/video-channel-interface' +import { VideoInstance } from '../../../models/video/video-interface' async function processDeleteActivity (activity: ActivityDelete) { const account = await getOrCreateAccount(activity.actor) diff --git a/server/lib/activitypub/process-follow.ts b/server/lib/activitypub/process/process-follow.ts similarity index 71% rename from server/lib/activitypub/process-follow.ts rename to server/lib/activitypub/process/process-follow.ts index a805c0757..553639580 100644 --- a/server/lib/activitypub/process-follow.ts +++ b/server/lib/activitypub/process/process-follow.ts @@ -1,9 +1,9 @@ -import { ActivityFollow } from '../../../shared/models/activitypub/activity' -import { getOrCreateAccount, retryTransactionWrapper } from '../../helpers' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' -import { sendAccept } from './send-request' -import { logger } from '../../helpers/logger' +import { ActivityFollow } from '../../../../shared/models/activitypub/activity' +import { getOrCreateAccount, retryTransactionWrapper } from '../../../helpers' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' +import { logger } from '../../../helpers/logger' +import { sendAccept } from '../send/send-accept' async function processFollowActivity (activity: ActivityFollow) { const activityObject = activity.object @@ -33,10 +33,10 @@ async function follow (account: AccountInstance, targetAccountURL: string) { await db.sequelize.transaction(async t => { const targetAccount = await db.Account.loadByUrl(targetAccountURL, t) - if (targetAccount === undefined) throw new Error('Unknown account') + if (!targetAccount) throw new Error('Unknown account') if (targetAccount.isOwned() === false) throw new Error('This is not a local account.') - await db.AccountFollow.findOrCreate({ + const [ accountFollow ] = await db.AccountFollow.findOrCreate({ where: { accountId: account.id, targetAccountId: targetAccount.id @@ -48,9 +48,11 @@ async function follow (account: AccountInstance, targetAccountURL: string) { }, transaction: t }) + accountFollow.AccountFollower = account + accountFollow.AccountFollowing = targetAccount // Target sends to account he accepted the follow request - return sendAccept(targetAccount, account, t) + return sendAccept(accountFollow, t) }) logger.info('Account uuid %s is followed by account %s.', account.url, targetAccountURL) diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts new file mode 100644 index 000000000..5d09423e1 --- /dev/null +++ b/server/lib/activitypub/process/process-undo.ts @@ -0,0 +1,31 @@ +import { ActivityUndo } from '../../../../shared/models/activitypub/activity' +import { logger } from '../../../helpers/logger' +import { database as db } from '../../../initializers' + +async function processUndoActivity (activity: ActivityUndo) { + const activityToUndo = activity.object + + if (activityToUndo.type === 'Follow') { + const follower = await db.Account.loadByUrl(activity.actor) + const following = await db.Account.loadByUrl(activityToUndo.object) + const accountFollow = await db.AccountFollow.loadByAccountAndTarget(follower.id, following.id) + + if (!accountFollow) throw new Error(`'Unknown account follow (${follower.id} -> ${following.id}.`) + + await accountFollow.destroy() + + return undefined + } + + logger.warn('Unknown activity object type %s -> %s when undo activity.', activityToUndo.type, { activity: activity.id }) + + return undefined +} + +// --------------------------------------------------------------------------- + +export { + processUndoActivity +} + +// --------------------------------------------------------------------------- diff --git a/server/lib/activitypub/process-update.ts b/server/lib/activitypub/process/process-update.ts similarity index 90% rename from server/lib/activitypub/process-update.ts rename to server/lib/activitypub/process/process-update.ts index a9aa5eeb4..a3bfb1baf 100644 --- a/server/lib/activitypub/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts @@ -1,12 +1,12 @@ -import { VideoChannelObject, VideoTorrentObject } from '../../../shared' -import { ActivityUpdate } from '../../../shared/models/activitypub/activity' -import { getOrCreateAccount } from '../../helpers/activitypub' -import { retryTransactionWrapper } from '../../helpers/database-utils' -import { logger } from '../../helpers/logger' -import { resetSequelizeInstance } from '../../helpers/utils' -import { database as db } from '../../initializers' -import { AccountInstance } from '../../models/account/account-interface' -import { VideoInstance } from '../../models/video/video-interface' +import { VideoChannelObject, VideoTorrentObject } from '../../../../shared' +import { ActivityUpdate } from '../../../../shared/models/activitypub/activity' +import { getOrCreateAccount } from '../../../helpers/activitypub' +import { retryTransactionWrapper } from '../../../helpers/database-utils' +import { logger } from '../../../helpers/logger' +import { resetSequelizeInstance } from '../../../helpers/utils' +import { database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' +import { VideoInstance } from '../../../models/video/video-interface' import { videoActivityObjectToDBAttributes, videoFileActivityUrlToDBAttributes } from './misc' import Bluebird = require('bluebird') diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts deleted file mode 100644 index 261ff04ab..000000000 --- a/server/lib/activitypub/send-request.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { Transaction } from 'sequelize' -import { - ActivityAccept, - ActivityAdd, - ActivityCreate, - ActivityDelete, - ActivityFollow, - ActivityUpdate -} from '../../../shared/models/activitypub/activity' -import { getActivityPubUrl } from '../../helpers/activitypub' -import { logger } from '../../helpers/logger' -import { database as db } from '../../initializers' -import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../models' -import { VideoAbuseInstance } from '../../models/video/video-abuse-interface' -import { activitypubHttpJobScheduler } from '../jobs' -import { ACTIVITY_PUB } from '../../initializers/constants' -import { VideoPrivacy } from '../../../shared/models/videos/video-privacy.enum' - -async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { - const byAccount = videoChannel.Account - - const videoChannelObject = videoChannel.toActivityPubObject() - const data = await createActivityData(videoChannel.url, byAccount, videoChannelObject) - - return broadcastToFollowers(data, byAccount, [ byAccount ], t) -} - -async function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { - const byAccount = videoChannel.Account - - const videoChannelObject = videoChannel.toActivityPubObject() - const data = await updateActivityData(videoChannel.url, byAccount, videoChannelObject) - - const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id) - accountsInvolved.push(byAccount) - - return broadcastToFollowers(data, byAccount, accountsInvolved, t) -} - -async function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { - const byAccount = videoChannel.Account - - const data = await deleteActivityData(videoChannel.url, byAccount) - - const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id) - accountsInvolved.push(byAccount) - - return broadcastToFollowers(data, byAccount, accountsInvolved, t) -} - -async function sendAddVideo (video: VideoInstance, t: Transaction) { - const byAccount = video.VideoChannel.Account - - const videoObject = video.toActivityPubObject() - const data = await addActivityData(video.url, byAccount, video, video.VideoChannel.url, videoObject) - - return broadcastToFollowers(data, byAccount, [ byAccount ], t) -} - -async function sendUpdateVideo (video: VideoInstance, t: Transaction) { - const byAccount = video.VideoChannel.Account - - const videoObject = video.toActivityPubObject() - const data = await updateActivityData(video.url, byAccount, videoObject) - - const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id) - accountsInvolved.push(byAccount) - - return broadcastToFollowers(data, byAccount, accountsInvolved, t) -} - -async function sendDeleteVideo (video: VideoInstance, t: Transaction) { - const byAccount = video.VideoChannel.Account - - const data = await deleteActivityData(video.url, byAccount) - - const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id) - accountsInvolved.push(byAccount) - - return broadcastToFollowers(data, byAccount, accountsInvolved, t) -} - -async function sendDeleteAccount (account: AccountInstance, t: Transaction) { - const data = await deleteActivityData(account.url, account) - - return broadcastToFollowers(data, account, [ account ], t) -} - -async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) { - const url = getActivityPubUrl('videoChannel', videoChannel.uuid) + '#announce' - const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject()) - - const data = await announceActivityData(url, byAccount, announcedActivity) - return broadcastToFollowers(data, byAccount, [ byAccount ], t) -} - -async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { - const url = getActivityPubUrl('video', video.uuid) + '#announce' - - const videoChannel = video.VideoChannel - const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject()) - - const data = await announceActivityData(url, byAccount, announcedActivity) - return broadcastToFollowers(data, byAccount, [ byAccount ], t) -} - -async function sendVideoAbuse (byAccount: AccountInstance, videoAbuse: VideoAbuseInstance, video: VideoInstance, t: Transaction) { - const url = getActivityPubUrl('videoAbuse', videoAbuse.id.toString()) - const data = await createActivityData(url, byAccount, videoAbuse.toActivityPubObject()) - - return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) -} - -async function sendAccept (byAccount: AccountInstance, toAccount: AccountInstance, t: Transaction) { - const data = await acceptActivityData(byAccount) - - return unicastTo(data, byAccount, toAccount.inboxUrl, t) -} - -async function sendFollow (byAccount: AccountInstance, toAccount: AccountInstance, t: Transaction) { - const data = await followActivityData(toAccount.url, byAccount) - - return unicastTo(data, byAccount, toAccount.inboxUrl, t) -} - -// --------------------------------------------------------------------------- - -export { - sendCreateVideoChannel, - sendUpdateVideoChannel, - sendDeleteVideoChannel, - sendAddVideo, - sendUpdateVideo, - sendDeleteVideo, - sendDeleteAccount, - sendAccept, - sendFollow, - sendVideoAbuse, - sendVideoChannelAnnounce, - sendVideoAnnounce -} - -// --------------------------------------------------------------------------- - -async function broadcastToFollowers (data: any, byAccount: AccountInstance, toAccountFollowers: AccountInstance[], t: Transaction) { - const toAccountFollowerIds = toAccountFollowers.map(a => a.id) - const result = await db.AccountFollow.listAcceptedFollowerSharedInboxUrls(toAccountFollowerIds) - if (result.data.length === 0) { - logger.info('Not broadcast because of 0 followers for %s.', toAccountFollowerIds.join(', ')) - return undefined - } - - const jobPayload = { - uris: result.data, - signatureAccountId: byAccount.id, - body: data - } - - return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpBroadcastHandler', jobPayload) -} - -async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: string, t: Transaction) { - const jobPayload = { - uris: [ toAccountUrl ], - signatureAccountId: byAccount.id, - body: data - } - - return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload) -} - -async function getAudience (accountSender: AccountInstance, isPublic = true) { - const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls() - - // Thanks Mastodon: https://github.com/tootsuite/mastodon/blob/master/app/lib/activitypub/tag_manager.rb#L47 - let to = [] - let cc = [] - - if (isPublic) { - to = [ ACTIVITY_PUB.PUBLIC ] - cc = followerInboxUrls - } else { // Unlisted - to = followerInboxUrls - cc = [ ACTIVITY_PUB.PUBLIC ] - } - - return { to, cc } -} - -async function createActivityData (url: string, byAccount: AccountInstance, object: any) { - const { to, cc } = await getAudience(byAccount) - const activity: ActivityCreate = { - type: 'Create', - id: url, - actor: byAccount.url, - to, - cc, - object - } - - return activity -} - -async function updateActivityData (url: string, byAccount: AccountInstance, object: any) { - const { to, cc } = await getAudience(byAccount) - const activity: ActivityUpdate = { - type: 'Update', - id: url, - actor: byAccount.url, - to, - cc, - object - } - - return activity -} - -async function deleteActivityData (url: string, byAccount: AccountInstance) { - const activity: ActivityDelete = { - type: 'Delete', - id: url, - actor: byAccount.url - } - - return activity -} - -async function addActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, target: string, object: any) { - const videoPublic = video.privacy === VideoPrivacy.PUBLIC - - const { to, cc } = await getAudience(byAccount, videoPublic) - const activity: ActivityAdd = { - type: 'Add', - id: url, - actor: byAccount.url, - to, - cc, - object, - target - } - - return activity -} - -async function announceActivityData (url: string, byAccount: AccountInstance, object: any) { - const activity = { - type: 'Announce', - id: url, - actor: byAccount.url, - object - } - - return activity -} - -async function followActivityData (url: string, byAccount: AccountInstance) { - const activity: ActivityFollow = { - type: 'Follow', - id: byAccount.url, - actor: byAccount.url, - object: url - } - - return activity -} - -async function acceptActivityData (byAccount: AccountInstance) { - const activity: ActivityAccept = { - type: 'Accept', - id: byAccount.url, - actor: byAccount.url - } - - return activity -} diff --git a/server/lib/activitypub/send/index.ts b/server/lib/activitypub/send/index.ts new file mode 100644 index 000000000..5f15dd4b5 --- /dev/null +++ b/server/lib/activitypub/send/index.ts @@ -0,0 +1,7 @@ +export * from './send-accept' +export * from './send-add' +export * from './send-announce' +export * from './send-create' +export * from './send-delete' +export * from './send-follow' +export * from './send-update' diff --git a/server/lib/activitypub/send/misc.ts b/server/lib/activitypub/send/misc.ts new file mode 100644 index 000000000..bea955b67 --- /dev/null +++ b/server/lib/activitypub/send/misc.ts @@ -0,0 +1,58 @@ +import { Transaction } from 'sequelize' +import { logger } from '../../../helpers/logger' +import { ACTIVITY_PUB, database as db } from '../../../initializers' +import { AccountInstance } from '../../../models/account/account-interface' +import { activitypubHttpJobScheduler } from '../../jobs/activitypub-http-job-scheduler/activitypub-http-job-scheduler' + +async function broadcastToFollowers (data: any, byAccount: AccountInstance, toAccountFollowers: AccountInstance[], t: Transaction) { + const toAccountFollowerIds = toAccountFollowers.map(a => a.id) + const result = await db.AccountFollow.listAcceptedFollowerSharedInboxUrls(toAccountFollowerIds) + if (result.data.length === 0) { + logger.info('Not broadcast because of 0 followers for %s.', toAccountFollowerIds.join(', ')) + return undefined + } + + const jobPayload = { + uris: result.data, + signatureAccountId: byAccount.id, + body: data + } + + return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpBroadcastHandler', jobPayload) +} + +async function unicastTo (data: any, byAccount: AccountInstance, toAccountUrl: string, t: Transaction) { + const jobPayload = { + uris: [ toAccountUrl ], + signatureAccountId: byAccount.id, + body: data + } + + return activitypubHttpJobScheduler.createJob(t, 'activitypubHttpUnicastHandler', jobPayload) +} + +async function getAudience (accountSender: AccountInstance, isPublic = true) { + const followerInboxUrls = await accountSender.getFollowerSharedInboxUrls() + + // Thanks Mastodon: https://github.com/tootsuite/mastodon/blob/master/app/lib/activitypub/tag_manager.rb#L47 + let to = [] + let cc = [] + + if (isPublic) { + to = [ ACTIVITY_PUB.PUBLIC ] + cc = followerInboxUrls + } else { // Unlisted + to = followerInboxUrls + cc = [ ACTIVITY_PUB.PUBLIC ] + } + + return { to, cc } +} + +// --------------------------------------------------------------------------- + +export { + broadcastToFollowers, + unicastTo, + getAudience +} diff --git a/server/lib/activitypub/send/send-accept.ts b/server/lib/activitypub/send/send-accept.ts new file mode 100644 index 000000000..0324a30fa --- /dev/null +++ b/server/lib/activitypub/send/send-accept.ts @@ -0,0 +1,34 @@ +import { Transaction } from 'sequelize' +import { ActivityAccept } from '../../../../shared/models/activitypub/activity' +import { AccountInstance } from '../../../models' +import { AccountFollowInstance } from '../../../models/account/account-follow-interface' +import { unicastTo } from './misc' +import { getAccountFollowAcceptActivityPubUrl } from '../../../helpers/activitypub' + +async function sendAccept (accountFollow: AccountFollowInstance, t: Transaction) { + const follower = accountFollow.AccountFollower + const me = accountFollow.AccountFollowing + + const url = getAccountFollowAcceptActivityPubUrl(accountFollow) + const data = await acceptActivityData(url, me) + + return unicastTo(data, me, follower.inboxUrl, t) +} + +// --------------------------------------------------------------------------- + +export { + sendAccept +} + +// --------------------------------------------------------------------------- + +async function acceptActivityData (url: string, byAccount: AccountInstance) { + const activity: ActivityAccept = { + type: 'Accept', + id: url, + actor: byAccount.url + } + + return activity +} diff --git a/server/lib/activitypub/send/send-add.ts b/server/lib/activitypub/send/send-add.ts new file mode 100644 index 000000000..3012b7533 --- /dev/null +++ b/server/lib/activitypub/send/send-add.ts @@ -0,0 +1,38 @@ +import { Transaction } from 'sequelize' +import { ActivityAdd } from '../../../../shared/models/activitypub/activity' +import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum' +import { AccountInstance, VideoInstance } from '../../../models' +import { broadcastToFollowers, getAudience } from './misc' + +async function sendAddVideo (video: VideoInstance, t: Transaction) { + const byAccount = video.VideoChannel.Account + + const videoObject = video.toActivityPubObject() + const data = await addActivityData(video.url, byAccount, video, video.VideoChannel.url, videoObject) + + return broadcastToFollowers(data, byAccount, [ byAccount ], t) +} + +async function addActivityData (url: string, byAccount: AccountInstance, video: VideoInstance, target: string, object: any) { + const videoPublic = video.privacy === VideoPrivacy.PUBLIC + + const { to, cc } = await getAudience(byAccount, videoPublic) + const activity: ActivityAdd = { + type: 'Add', + id: url, + actor: byAccount.url, + to, + cc, + object, + target + } + + return activity +} + +// --------------------------------------------------------------------------- + +export { + addActivityData, + sendAddVideo +} diff --git a/server/lib/activitypub/send/send-announce.ts b/server/lib/activitypub/send/send-announce.ts new file mode 100644 index 000000000..b9217e4f6 --- /dev/null +++ b/server/lib/activitypub/send/send-announce.ts @@ -0,0 +1,45 @@ +import { Transaction } from 'sequelize' +import { AccountInstance, VideoInstance } from '../../../models' +import { VideoChannelInstance } from '../../../models/video/video-channel-interface' +import { broadcastToFollowers } from './misc' +import { addActivityData } from './send-add' +import { createActivityData } from './send-create' +import { getAnnounceActivityPubUrl } from '../../../helpers/activitypub' + +async function sendVideoAnnounce (byAccount: AccountInstance, video: VideoInstance, t: Transaction) { + const url = getAnnounceActivityPubUrl(video.url, byAccount) + + const videoChannel = video.VideoChannel + const announcedActivity = await addActivityData(url, videoChannel.Account, video, videoChannel.url, video.toActivityPubObject()) + + const data = await announceActivityData(url, byAccount, announcedActivity) + return broadcastToFollowers(data, byAccount, [ byAccount ], t) +} + +async function sendVideoChannelAnnounce (byAccount: AccountInstance, videoChannel: VideoChannelInstance, t: Transaction) { + const url = getAnnounceActivityPubUrl(videoChannel.url, byAccount) + const announcedActivity = await createActivityData(url, videoChannel.Account, videoChannel.toActivityPubObject()) + + const data = await announceActivityData(url, byAccount, announcedActivity) + return broadcastToFollowers(data, byAccount, [ byAccount ], t) +} + +// --------------------------------------------------------------------------- + +export { + sendVideoAnnounce, + sendVideoChannelAnnounce +} + +// --------------------------------------------------------------------------- + +async function announceActivityData (url: string, byAccount: AccountInstance, object: any) { + const activity = { + type: 'Announce', + id: url, + actor: byAccount.url, + object + } + + return activity +} diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts new file mode 100644 index 000000000..66bfeee89 --- /dev/null +++ b/server/lib/activitypub/send/send-create.ts @@ -0,0 +1,44 @@ +import { Transaction } from 'sequelize' +import { ActivityCreate } from '../../../../shared/models/activitypub/activity' +import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models' +import { VideoAbuseInstance } from '../../../models/video/video-abuse-interface' +import { broadcastToFollowers, getAudience, unicastTo } from './misc' +import { getVideoAbuseActivityPubUrl } from '../../../helpers/activitypub' + +async function sendCreateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { + const byAccount = videoChannel.Account + + const videoChannelObject = videoChannel.toActivityPubObject() + const data = await createActivityData(videoChannel.url, byAccount, videoChannelObject) + + return broadcastToFollowers(data, byAccount, [ byAccount ], t) +} + +async function sendVideoAbuse (byAccount: AccountInstance, videoAbuse: VideoAbuseInstance, video: VideoInstance, t: Transaction) { + const url = getVideoAbuseActivityPubUrl(videoAbuse) + const data = await createActivityData(url, byAccount, videoAbuse.toActivityPubObject()) + + return unicastTo(data, byAccount, video.VideoChannel.Account.sharedInboxUrl, t) +} + +async function createActivityData (url: string, byAccount: AccountInstance, object: any) { + const { to, cc } = await getAudience(byAccount) + const activity: ActivityCreate = { + type: 'Create', + id: url, + actor: byAccount.url, + to, + cc, + object + } + + return activity +} + +// --------------------------------------------------------------------------- + +export { + sendCreateVideoChannel, + sendVideoAbuse, + createActivityData +} diff --git a/server/lib/activitypub/send/send-delete.ts b/server/lib/activitypub/send/send-delete.ts new file mode 100644 index 000000000..5be0e2d24 --- /dev/null +++ b/server/lib/activitypub/send/send-delete.ts @@ -0,0 +1,53 @@ +import { Transaction } from 'sequelize' +import { ActivityDelete } from '../../../../shared/models/activitypub/activity' +import { database as db } from '../../../initializers' +import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models' +import { broadcastToFollowers } from './misc' + +async function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { + const byAccount = videoChannel.Account + + const data = await deleteActivityData(videoChannel.url, byAccount) + + const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id) + accountsInvolved.push(byAccount) + + return broadcastToFollowers(data, byAccount, accountsInvolved, t) +} + +async function sendDeleteVideo (video: VideoInstance, t: Transaction) { + const byAccount = video.VideoChannel.Account + + const data = await deleteActivityData(video.url, byAccount) + + const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id) + accountsInvolved.push(byAccount) + + return broadcastToFollowers(data, byAccount, accountsInvolved, t) +} + +async function sendDeleteAccount (account: AccountInstance, t: Transaction) { + const data = await deleteActivityData(account.url, account) + + return broadcastToFollowers(data, account, [ account ], t) +} + +// --------------------------------------------------------------------------- + +export { + sendDeleteVideoChannel, + sendDeleteVideo, + sendDeleteAccount +} + +// --------------------------------------------------------------------------- + +async function deleteActivityData (url: string, byAccount: AccountInstance) { + const activity: ActivityDelete = { + type: 'Delete', + id: url, + actor: byAccount.url + } + + return activity +} diff --git a/server/lib/activitypub/send/send-follow.ts b/server/lib/activitypub/send/send-follow.ts new file mode 100644 index 000000000..48d641c22 --- /dev/null +++ b/server/lib/activitypub/send/send-follow.ts @@ -0,0 +1,34 @@ +import { Transaction } from 'sequelize' +import { ActivityFollow } from '../../../../shared/models/activitypub/activity' +import { AccountInstance } from '../../../models' +import { AccountFollowInstance } from '../../../models/account/account-follow-interface' +import { unicastTo } from './misc' +import { getAccountFollowActivityPubUrl } from '../../../helpers/activitypub' + +async function sendFollow (accountFollow: AccountFollowInstance, t: Transaction) { + const me = accountFollow.AccountFollower + const following = accountFollow.AccountFollowing + + const url = getAccountFollowActivityPubUrl(accountFollow) + const data = await followActivityData(url, me, following) + + return unicastTo(data, me, following.inboxUrl, t) +} + +async function followActivityData (url: string, byAccount: AccountInstance, targetAccount: AccountInstance) { + const activity: ActivityFollow = { + type: 'Follow', + id: url, + actor: byAccount.url, + object: targetAccount.url + } + + return activity +} + +// --------------------------------------------------------------------------- + +export { + sendFollow, + followActivityData +} diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts new file mode 100644 index 000000000..39da824f3 --- /dev/null +++ b/server/lib/activitypub/send/send-undo.ts @@ -0,0 +1,39 @@ +import { Transaction } from 'sequelize' +import { ActivityFollow, ActivityUndo } from '../../../../shared/models/activitypub/activity' +import { AccountInstance } from '../../../models' +import { AccountFollowInstance } from '../../../models/account/account-follow-interface' +import { unicastTo } from './misc' +import { getAccountFollowActivityPubUrl, getUndoActivityPubUrl } from '../../../helpers/activitypub' +import { followActivityData } from './send-follow' + +async function sendUndoFollow (accountFollow: AccountFollowInstance, t: Transaction) { + const me = accountFollow.AccountFollower + const following = accountFollow.AccountFollowing + + const followUrl = getAccountFollowActivityPubUrl(accountFollow) + const undoUrl = getUndoActivityPubUrl(followUrl) + + const object = await followActivityData(followUrl, me, following) + const data = await undoActivityData(undoUrl, me, object) + + return unicastTo(data, me, following.inboxUrl, t) +} + +// --------------------------------------------------------------------------- + +export { + sendUndoFollow +} + +// --------------------------------------------------------------------------- + +async function undoActivityData (url: string, byAccount: AccountInstance, object: ActivityFollow) { + const activity: ActivityUndo = { + type: 'Undo', + id: url, + actor: byAccount.url, + object + } + + return activity +} diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts new file mode 100644 index 000000000..42738f973 --- /dev/null +++ b/server/lib/activitypub/send/send-update.ts @@ -0,0 +1,55 @@ +import { Transaction } from 'sequelize' +import { ActivityUpdate } from '../../../../shared/models/activitypub/activity' +import { getUpdateActivityPubUrl } from '../../../helpers/activitypub' +import { database as db } from '../../../initializers' +import { AccountInstance, VideoChannelInstance, VideoInstance } from '../../../models' +import { broadcastToFollowers, getAudience } from './misc' + +async function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Transaction) { + const byAccount = videoChannel.Account + + const url = getUpdateActivityPubUrl(videoChannel.url, videoChannel.updatedAt.toISOString()) + const videoChannelObject = videoChannel.toActivityPubObject() + const data = await updateActivityData(url, byAccount, videoChannelObject) + + const accountsInvolved = await db.VideoChannelShare.loadAccountsByShare(videoChannel.id) + accountsInvolved.push(byAccount) + + return broadcastToFollowers(data, byAccount, accountsInvolved, t) +} + +async function sendUpdateVideo (video: VideoInstance, t: Transaction) { + const byAccount = video.VideoChannel.Account + + const url = getUpdateActivityPubUrl(video.url, video.updatedAt.toISOString()) + const videoObject = video.toActivityPubObject() + const data = await updateActivityData(url, byAccount, videoObject) + + const accountsInvolved = await db.VideoShare.loadAccountsByShare(video.id) + accountsInvolved.push(byAccount) + + return broadcastToFollowers(data, byAccount, accountsInvolved, t) +} + +// --------------------------------------------------------------------------- + +export { + sendUpdateVideoChannel, + sendUpdateVideo +} + +// --------------------------------------------------------------------------- + +async function updateActivityData (url: string, byAccount: AccountInstance, object: any) { + const { to, cc } = await getAudience(byAccount) + const activity: ActivityUpdate = { + type: 'Update', + id: url, + actor: byAccount.url, + to, + cc, + object + } + + return activity +} diff --git a/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts b/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts index 6443899d3..f26110973 100644 --- a/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts +++ b/server/lib/jobs/transcoding-job-scheduler/video-file-optimizer-handler.ts @@ -3,10 +3,11 @@ import { computeResolutionsToTranscode, logger } from '../../../helpers' import { database as db } from '../../../initializers/database' import { VideoInstance } from '../../../models' -import { sendAddVideo } from '../../activitypub/send-request' + import { JobScheduler } from '../job-scheduler' import { TranscodingJobPayload } from './transcoding-job-scheduler' import { shareVideoByServer } from '../../../helpers/activitypub' +import { sendAddVideo } from '../../activitypub/send/send-add' async function process (data: TranscodingJobPayload, jobId: number) { const video = await db.Video.loadByUUIDAndPopulateAccountAndServerAndTags(data.videoUUID) diff --git a/server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts b/server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts index 4f2ce3d24..867580200 100644 --- a/server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts +++ b/server/lib/jobs/transcoding-job-scheduler/video-file-transcoder-handler.ts @@ -2,7 +2,7 @@ import { VideoResolution } from '../../../../shared' import { logger } from '../../../helpers' import { database as db } from '../../../initializers/database' import { VideoInstance } from '../../../models' -import { sendUpdateVideo } from '../../activitypub/send-request' +import { sendUpdateVideo } from '../../activitypub/send/send-update' async function process (data: { videoUUID: string, resolution: VideoResolution }, jobId: number) { const video = await db.Video.loadByUUIDAndPopulateAccountAndServerAndTags(data.videoUUID) diff --git a/server/lib/user.ts b/server/lib/user.ts index 2d7b36b4f..d54ffc916 100644 --- a/server/lib/user.ts +++ b/server/lib/user.ts @@ -1,11 +1,11 @@ import * as Sequelize from 'sequelize' -import { getActivityPubUrl } from '../helpers/activitypub' import { createPrivateAndPublicKeys } from '../helpers/peertube-crypto' import { database as db } from '../initializers' import { CONFIG } from '../initializers/constants' import { UserInstance } from '../models' import { createVideoChannel } from './video-channel' import { logger } from '../helpers/logger' +import { getAccountActivityPubUrl } from '../helpers/activitypub' async function createUserAccountAndChannel (user: UserInstance, validateUser = true) { const { account, videoChannel } = await db.sequelize.transaction(async t => { @@ -36,7 +36,7 @@ async function createUserAccountAndChannel (user: UserInstance, validateUser = t } async function createLocalAccountWithoutKeys (name: string, userId: number, applicationId: number, t: Sequelize.Transaction) { - const url = getActivityPubUrl('account', name) + const url = getAccountActivityPubUrl(name) const accountInstance = db.Account.build({ name, diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts index 5bb1814ea..5235d9cb5 100644 --- a/server/lib/video-channel.ts +++ b/server/lib/video-channel.ts @@ -1,9 +1,9 @@ import * as Sequelize from 'sequelize' import { VideoChannelCreate } from '../../shared/models' import { logger } from '../helpers' -import { getActivityPubUrl } from '../helpers/activitypub' import { database as db } from '../initializers' import { AccountInstance } from '../models' +import { getVideoChannelActivityPubUrl } from '../helpers/activitypub' async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account: AccountInstance, t: Sequelize.Transaction) { const videoChannelData = { @@ -14,7 +14,7 @@ async function createVideoChannel (videoChannelInfo: VideoChannelCreate, account } const videoChannel = db.VideoChannel.build(videoChannelData) - videoChannel.set('url', getActivityPubUrl('videoChannel', videoChannel.uuid)) + videoChannel.set('url', getVideoChannelActivityPubUrl(videoChannel)) const options = { transaction: t } diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts new file mode 100644 index 000000000..e22349726 --- /dev/null +++ b/server/middlewares/validators/follows.ts @@ -0,0 +1,62 @@ +import * as express from 'express' +import { body } from 'express-validator/check' +import { isTestInstance } from '../../helpers/core-utils' +import { isAccountIdValid } from '../../helpers/custom-validators/activitypub/account' +import { isEachUniqueHostValid } from '../../helpers/custom-validators/servers' +import { logger } from '../../helpers/logger' +import { CONFIG, database as db } from '../../initializers' +import { checkErrors } from './utils' +import { getServerAccount } from '../../helpers/utils' + +const followValidator = [ + body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + // Force https if the administrator wants to make friends + if (isTestInstance() === false && CONFIG.WEBSERVER.SCHEME === 'http') { + return res.status(400) + .json({ + error: 'Cannot follow non HTTPS web server.' + }) + .end() + } + + logger.debug('Checking follow parameters', { parameters: req.body }) + + checkErrors(req, res, next) + } +] + +const removeFollowingValidator = [ + body('accountId').custom(isAccountIdValid).withMessage('Should have a valid account id'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking follow parameters', { parameters: req.body }) + + checkErrors(req, res, async () => { + try { + const serverAccount = await getServerAccount() + const following = await db.AccountFollow.loadByAccountAndTarget(serverAccount.id, req.params.accountId) + + if (!following) { + return res.status(404) + .end() + } + + res.locals.following = following + + return next() + } catch (err) { + logger.error('Error in remove following validator.', err) + return res.sendStatus(500) + } + }) + } +] + +// --------------------------------------------------------------------------- + +export { + followValidator, + removeFollowingValidator +} diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 3f5afe5b3..9840e8f65 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts @@ -2,7 +2,7 @@ export * from './account' export * from './oembed' export * from './activitypub' export * from './pagination' -export * from './servers' +export * from './follows' export * from './sort' export * from './users' export * from './videos' diff --git a/server/middlewares/validators/servers.ts b/server/middlewares/validators/servers.ts deleted file mode 100644 index 95b69b789..000000000 --- a/server/middlewares/validators/servers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as express from 'express' -import { body } from 'express-validator/check' -import { isEachUniqueHostValid } from '../../helpers/custom-validators/servers' -import { isTestInstance } from '../../helpers/core-utils' -import { CONFIG } from '../../initializers/constants' -import { logger } from '../../helpers/logger' -import { checkErrors } from './utils' - -const followValidator = [ - body('hosts').custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'), - - (req: express.Request, res: express.Response, next: express.NextFunction) => { - // Force https if the administrator wants to make friends - if (isTestInstance() === false && CONFIG.WEBSERVER.SCHEME === 'http') { - return res.status(400) - .json({ - error: 'Cannot follow non HTTPS web server.' - }) - .end() - } - - logger.debug('Checking follow parameters', { parameters: req.body }) - - checkErrors(req, res, next) - } -] - -// --------------------------------------------------------------------------- - -export { - followValidator -} diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts index cc9b7c42b..f00c7dcd9 100644 --- a/server/models/account/account-follow.ts +++ b/server/models/account/account-follow.ts @@ -78,7 +78,19 @@ loadByAccountAndTarget = function (accountId: number, targetAccountId: number) { where: { accountId, targetAccountId - } + }, + include: [ + { + model: AccountFollow[ 'sequelize' ].models.Account, + required: true, + as: 'AccountFollower' + }, + { + model: AccountFollow['sequelize'].models.Account, + required: true, + as: 'AccountFollowing' + } + ] } return AccountFollow.findOne(query) diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts index 1a567fb7a..e30260f76 100644 --- a/server/models/account/account-interface.ts +++ b/server/models/account/account-interface.ts @@ -37,7 +37,7 @@ export interface AccountClass { export interface AccountAttributes { name: string - url: string + url?: string publicKey: string privateKey: string followersCount: number diff --git a/server/models/account/account.ts b/server/models/account/account.ts index faf5fa841..9a2921501 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -1,5 +1,4 @@ import * as Sequelize from 'sequelize' - import { activityPubContextify, isAccountFollowersCountValid, @@ -15,7 +14,7 @@ import { isUserUsernameValid } from '../../helpers' import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' -import { sendDeleteAccount } from '../../lib/activitypub/send-request' +import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete' import { addMethodsToModel } from '../utils' import { AccountAttributes, AccountInstance, AccountMethods } from './account-interface' diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index f8414d4a8..93566a5c6 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -1,17 +1,11 @@ import * as Sequelize from 'sequelize' - -import { isVideoChannelNameValid, isVideoChannelDescriptionValid } from '../../helpers' - -import { addMethodsToModel, getSort } from '../utils' -import { - VideoChannelInstance, - VideoChannelAttributes, - - VideoChannelMethods -} from './video-channel-interface' -import { sendDeleteVideoChannel } from '../../lib/activitypub/send-request' +import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers' import { isVideoChannelUrlValid } from '../../helpers/custom-validators/video-channels' import { CONSTRAINTS_FIELDS } from '../../initializers/constants' +import { sendDeleteVideoChannel } from '../../lib/activitypub/send/send-delete' + +import { addMethodsToModel, getSort } from '../utils' +import { VideoChannelAttributes, VideoChannelInstance, VideoChannelMethods } from './video-channel-interface' let VideoChannel: Sequelize.Model let toFormattedJSON: VideoChannelMethods.ToFormattedJSON diff --git a/server/models/video/video.ts b/server/models/video/video.ts index dc10aca1a..e2069eb0c 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -9,7 +9,6 @@ import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/v import { createTorrentPromise, generateImageFromVideoFile, - getActivityPubUrl, getVideoFileHeight, isVideoCategoryValid, isVideoDescriptionValid, @@ -40,13 +39,13 @@ import { VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../initializers' -import { sendDeleteVideo } from '../../lib/activitypub/send-request' import { addMethodsToModel, getSort } from '../utils' import { TagInstance } from './tag-interface' import { VideoFileInstance, VideoFileModel } from './video-file-interface' import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface' +import { sendDeleteVideo } from '../../lib/index' const Buffer = safeBuffer.Buffer @@ -584,7 +583,7 @@ toActivityPubObject = function (this: VideoInstance) { const videoObject: VideoTorrentObject = { type: 'Video' as 'Video', - id: getActivityPubUrl('video', this.uuid), + id: this.url, name: this.name, // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration duration: 'PT' + this.duration + 'S', @@ -615,7 +614,7 @@ toActivityPubObject = function (this: VideoInstance) { width: THUMBNAILS_SIZE.width, height: THUMBNAILS_SIZE.height }, - url + url // FIXME: needed? } return videoObject diff --git a/server/tests/api/multiple-servers.ts b/server/tests/api/multiple-servers.ts index b6a57ab6d..cdbd24f56 100644 --- a/server/tests/api/multiple-servers.ts +++ b/server/tests/api/multiple-servers.ts @@ -316,7 +316,7 @@ describe('Test multiple servers', function () { expect(video1.serverHost).to.equal('localhost:9003') expect(video1.duration).to.equal(5) expect(video1.tags).to.deep.equal([ 'tag1p3' ]) - expect(video1.author).to.equal('root') + expect(video1.account).to.equal('root') expect(dateIsValid(video1.createdAt)).to.be.true expect(dateIsValid(video1.updatedAt)).to.be.true @@ -342,7 +342,7 @@ describe('Test multiple servers', function () { expect(video2.serverHost).to.equal('localhost:9003') expect(video2.duration).to.equal(5) expect(video2.tags).to.deep.equal([ 'tag2p3', 'tag3p3', 'tag4p3' ]) - expect(video2.author).to.equal('root') + expect(video2.account).to.equal('root') expect(dateIsValid(video2.createdAt)).to.be.true expect(dateIsValid(video2.updatedAt)).to.be.true @@ -690,7 +690,7 @@ describe('Test multiple servers', function () { expect(baseVideo.licence).to.equal(video.licence) expect(baseVideo.category).to.equal(video.category) expect(baseVideo.nsfw).to.equal(video.nsfw) - expect(baseVideo.author).to.equal(video.account) + expect(baseVideo.account).to.equal(video.account) expect(baseVideo.tags).to.deep.equal(video.tags) } }) diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index 255cdd43c..3d035d7d7 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts @@ -3,10 +3,10 @@ import { ActivityPubSignature } from './activitypub-signature' import { VideoAbuseObject } from './objects/video-abuse-object' export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | - ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce + ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce | + ActivityUndo -// Flag -> report abuse -export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' +export type ActivityType = 'Create' | 'Add' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Announce' | 'Undo' export interface BaseActivity { '@context'?: any[] @@ -51,3 +51,8 @@ export interface ActivityAnnounce extends BaseActivity { type: 'Announce' object: ActivityCreate | ActivityAdd } + +export interface ActivityUndo extends BaseActivity { + type: 'Undo', + object: ActivityFollow +}