From cefe22cf7c5286af1eb0e7a19937e741e2c2f58a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 5 Jun 2023 15:51:16 +0200 Subject: [PATCH] Fetch remote AP objects if only id is specified --- server/controllers/activitypub/client.ts | 3 +- server/controllers/activitypub/outbox.ts | 1 + server/lib/activitypub/activity.ts | 14 +- server/lib/activitypub/playlists/get.ts | 4 +- .../lib/activitypub/process/process-create.ts | 58 +- .../activitypub/process/process-dislike.ts | 11 +- .../lib/activitypub/process/process-flag.ts | 8 +- .../lib/activitypub/process/process-undo.ts | 43 +- .../lib/activitypub/process/process-update.ts | 36 +- server/lib/activitypub/send/send-create.ts | 23 +- server/lib/activitypub/send/send-undo.ts | 19 +- server/lib/activitypub/send/send-update.ts | 9 +- server/lib/activitypub/videos/get.ts | 8 +- server/tests/api/server/follows.ts | 1101 +++++++++-------- shared/models/activitypub/activity.ts | 56 +- .../activitypub/objects/activitypub-object.ts | 17 + .../activitypub/objects/dislike-object.ts | 6 - shared/models/activitypub/objects/index.ts | 3 +- .../activitypub/objects/object.model.ts | 1 - 19 files changed, 760 insertions(+), 661 deletions(-) create mode 100644 shared/models/activitypub/objects/activitypub-object.ts delete mode 100644 shared/models/activitypub/objects/dislike-object.ts delete mode 100644 shared/models/activitypub/objects/object.model.ts diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 166fc2a22..c47c61f52 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -4,6 +4,7 @@ import { activityPubCollectionPagination } from '@server/lib/activitypub/collect import { activityPubContextify } from '@server/lib/activitypub/context' import { getServerActor } from '@server/models/application/application' import { MAccountId, MActorId, MChannelId, MVideoId } from '@server/types/models' +import { VideoCommentObject } from '@shared/models' import { VideoPrivacy, VideoRateType } from '../../../shared/models/videos' import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../../initializers/constants' @@ -353,7 +354,7 @@ async function videoCommentController (req: express.Request, res: express.Respon videoCommentObject = audiencify(videoCommentObject, audience) if (req.path.endsWith('/activity')) { - const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject, audience) + const data = buildCreateActivity(videoComment.url, videoComment.Account.Actor, videoCommentObject as VideoCommentObject, audience) return activityPubResponse(activityPubContextify(data, 'Comment'), res) } } diff --git a/server/controllers/activitypub/outbox.ts b/server/controllers/activitypub/outbox.ts index 681a5660c..4175cf276 100644 --- a/server/controllers/activitypub/outbox.ts +++ b/server/controllers/activitypub/outbox.ts @@ -63,6 +63,7 @@ async function buildActivities (actor: MActorLight, start: number, count: number activities.push(announceActivity) } else { + // FIXME: only use the video URL to reduce load. Breaks compat with PeerTube < 6.0.0 const videoObject = await video.toActivityPubObject() const createActivity = buildCreateActivity(video.url, byActor, videoObject, createActivityAudience) diff --git a/server/lib/activitypub/activity.ts b/server/lib/activitypub/activity.ts index 1f6ec221e..0fed3e8fd 100644 --- a/server/lib/activitypub/activity.ts +++ b/server/lib/activitypub/activity.ts @@ -1,4 +1,5 @@ -import { ActivityType } from '@shared/models' +import { doJSONRequest } from '@server/helpers/requests' +import { APObjectId, ActivityObject, ActivityPubActor, ActivityType } from '@shared/models' function getAPId (object: string | { id: string }) { if (typeof object === 'string') return object @@ -32,8 +33,19 @@ function buildAvailableActivities (): ActivityType[] { ] } +async function fetchAPObject (object: APObjectId) { + if (typeof object === 'string') { + const { body } = await doJSONRequest>(object, { activityPub: true }) + + return body + } + + return object as Exclude +} + export { getAPId, + fetchAPObject, getActivityStreamDuration, buildAvailableActivities, getDurationFromActivityStream diff --git a/server/lib/activitypub/playlists/get.ts b/server/lib/activitypub/playlists/get.ts index bfaf52cc9..c34554d69 100644 --- a/server/lib/activitypub/playlists/get.ts +++ b/server/lib/activitypub/playlists/get.ts @@ -1,12 +1,12 @@ import { VideoPlaylistModel } from '@server/models/video/video-playlist' import { MVideoPlaylistFullSummary } from '@server/types/models' -import { APObject } from '@shared/models' +import { APObjectId } from '@shared/models' import { getAPId } from '../activity' import { createOrUpdateVideoPlaylist } from './create-update' import { scheduleRefreshIfNeeded } from './refresh' import { fetchRemoteVideoPlaylist } from './shared' -async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObject): Promise { +async function getOrCreateAPVideoPlaylist (playlistObjectArg: APObjectId): Promise { const playlistUrl = getAPId(playlistObjectArg) const playlistFromDatabase = await VideoPlaylistModel.loadByUrlWithAccountAndChannelSummary(playlistUrl) diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 1e6e8956c..e89d1ab45 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -1,13 +1,24 @@ import { isBlockedByServerOrAccount } from '@server/lib/blocklist' import { isRedundancyAccepted } from '@server/lib/redundancy' import { VideoModel } from '@server/models/video/video' -import { ActivityCreate, CacheFileObject, PlaylistObject, VideoCommentObject, VideoObject, WatchActionObject } from '@shared/models' +import { + AbuseObject, + ActivityCreate, + ActivityCreateObject, + ActivityObject, + CacheFileObject, + PlaylistObject, + VideoCommentObject, + VideoObject, + WatchActionObject +} from '@shared/models' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' import { sequelizeTypescript } from '../../../initializers/database' import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' import { Notifier } from '../../notifier' +import { fetchAPObject } from '../activity' import { createOrUpdateCacheFile } from '../cache-file' import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' import { createOrUpdateVideoPlaylist } from '../playlists' @@ -15,35 +26,35 @@ import { forwardVideoRelatedActivity } from '../send/shared/send-utils' import { resolveThread } from '../video-comments' import { getOrCreateAPVideo } from '../videos' -async function processCreateActivity (options: APProcessorOptions) { +async function processCreateActivity (options: APProcessorOptions>) { const { activity, byActor } = options // Only notify if it is not from a fetcher job const notify = options.fromFetch !== true - const activityObject = activity.object + const activityObject = await fetchAPObject>(activity.object) const activityType = activityObject.type if (activityType === 'Video') { - return processCreateVideo(activity, notify) + return processCreateVideo(activityObject, notify) } if (activityType === 'Note') { // Comments will be fetched from videos if (options.fromFetch) return - return retryTransactionWrapper(processCreateVideoComment, activity, byActor, notify) + return retryTransactionWrapper(processCreateVideoComment, activity, activityObject, byActor, notify) } if (activityType === 'WatchAction') { - return retryTransactionWrapper(processCreateWatchAction, activity) + return retryTransactionWrapper(processCreateWatchAction, activityObject) } if (activityType === 'CacheFile') { - return retryTransactionWrapper(processCreateCacheFile, activity, byActor) + return retryTransactionWrapper(processCreateCacheFile, activity, activityObject, byActor) } if (activityType === 'Playlist') { - return retryTransactionWrapper(processCreatePlaylist, activity, byActor) + return retryTransactionWrapper(processCreatePlaylist, activity, activityObject, byActor) } logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) @@ -58,9 +69,7 @@ export { // --------------------------------------------------------------------------- -async function processCreateVideo (activity: ActivityCreate, notify: boolean) { - const videoToCreateData = activity.object as VideoObject - +async function processCreateVideo (videoToCreateData: VideoObject, notify: boolean) { const syncParam = { rates: false, shares: false, comments: false, thumbnail: true, refreshVideo: false } const { video, created } = await getOrCreateAPVideo({ videoObject: videoToCreateData, syncParam }) @@ -69,11 +78,13 @@ async function processCreateVideo (activity: ActivityCreate, notify: boolean) { return video } -async function processCreateCacheFile (activity: ActivityCreate, byActor: MActorSignature) { +async function processCreateCacheFile ( + activity: ActivityCreate, + cacheFile: CacheFileObject, + byActor: MActorSignature +) { if (await isRedundancyAccepted(activity, byActor) !== true) return - const cacheFile = activity.object as CacheFileObject - const { video } = await getOrCreateAPVideo({ videoObject: cacheFile.object }) await sequelizeTypescript.transaction(async t => { @@ -87,9 +98,7 @@ async function processCreateCacheFile (activity: ActivityCreate, byActor: MActor } } -async function processCreateWatchAction (activity: ActivityCreate) { - const watchAction = activity.object as WatchActionObject - +async function processCreateWatchAction (watchAction: WatchActionObject) { if (watchAction.actionStatus !== 'CompletedActionStatus') return const video = await VideoModel.loadByUrl(watchAction.object) @@ -100,8 +109,12 @@ async function processCreateWatchAction (activity: ActivityCreate) { }) } -async function processCreateVideoComment (activity: ActivityCreate, byActor: MActorSignature, notify: boolean) { - const commentObject = activity.object as VideoCommentObject +async function processCreateVideoComment ( + activity: ActivityCreate, + commentObject: VideoCommentObject, + byActor: MActorSignature, + notify: boolean +) { const byAccount = byActor.Account if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) @@ -144,8 +157,11 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: MAc if (created && notify) Notifier.Instance.notifyOnNewComment(comment) } -async function processCreatePlaylist (activity: ActivityCreate, byActor: MActorSignature) { - const playlistObject = activity.object as PlaylistObject +async function processCreatePlaylist ( + activity: ActivityCreate, + playlistObject: PlaylistObject, + byActor: MActorSignature +) { const byAccount = byActor.Account if (!byAccount) throw new Error('Cannot create video playlist with the non account actor ' + byActor.url) diff --git a/server/lib/activitypub/process/process-dislike.ts b/server/lib/activitypub/process/process-dislike.ts index 44e349b22..4e270f917 100644 --- a/server/lib/activitypub/process/process-dislike.ts +++ b/server/lib/activitypub/process/process-dislike.ts @@ -1,5 +1,5 @@ import { VideoModel } from '@server/models/video/video' -import { ActivityCreate, ActivityDislike, DislikeObject } from '@shared/models' +import { ActivityDislike } from '@shared/models' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { sequelizeTypescript } from '../../../initializers/database' import { AccountVideoRateModel } from '../../../models/account/account-video-rate' @@ -7,7 +7,7 @@ import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { MActorSignature } from '../../../types/models' import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' -async function processDislikeActivity (options: APProcessorOptions) { +async function processDislikeActivity (options: APProcessorOptions) { const { activity, byActor } = options return retryTransactionWrapper(processDislike, activity, byActor) } @@ -20,11 +20,8 @@ export { // --------------------------------------------------------------------------- -async function processDislike (activity: ActivityCreate | ActivityDislike, byActor: MActorSignature) { - const dislikeObject = activity.type === 'Dislike' - ? activity.object - : (activity.object as DislikeObject).object - +async function processDislike (activity: ActivityDislike, byActor: MActorSignature) { + const dislikeObject = activity.object const byAccount = byActor.Account if (!byAccount) throw new Error('Cannot create dislike with the non account actor ' + byActor.url) diff --git a/server/lib/activitypub/process/process-flag.ts b/server/lib/activitypub/process/process-flag.ts index 10f58ef27..bea285670 100644 --- a/server/lib/activitypub/process/process-flag.ts +++ b/server/lib/activitypub/process/process-flag.ts @@ -3,7 +3,7 @@ import { AccountModel } from '@server/models/account/account' import { VideoModel } from '@server/models/video/video' import { VideoCommentModel } from '@server/models/video/video-comment' import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' -import { AbuseObject, AbuseState, ActivityCreate, ActivityFlag } from '@shared/models' +import { AbuseState, ActivityFlag } from '@shared/models' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' import { sequelizeTypescript } from '../../../initializers/database' @@ -11,7 +11,7 @@ import { getAPId } from '../../../lib/activitypub/activity' import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models' -async function processFlagActivity (options: APProcessorOptions) { +async function processFlagActivity (options: APProcessorOptions) { const { activity, byActor } = options return retryTransactionWrapper(processCreateAbuse, activity, byActor) @@ -25,9 +25,7 @@ export { // --------------------------------------------------------------------------- -async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) { - const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject) - +async function processCreateAbuse (flag: ActivityFlag, byActor: MActorSignature) { const account = byActor.Account if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url) diff --git a/server/lib/activitypub/process/process-undo.ts b/server/lib/activitypub/process/process-undo.ts index 99423a72b..25f68724d 100644 --- a/server/lib/activitypub/process/process-undo.ts +++ b/server/lib/activitypub/process/process-undo.ts @@ -1,6 +1,14 @@ import { VideoModel } from '@server/models/video/video' -import { ActivityAnnounce, ActivityFollow, ActivityLike, ActivityUndo, CacheFileObject } from '../../../../shared/models/activitypub' -import { DislikeObject } from '../../../../shared/models/activitypub/objects' +import { + ActivityAnnounce, + ActivityCreate, + ActivityDislike, + ActivityFollow, + ActivityLike, + ActivityUndo, + ActivityUndoObject, + CacheFileObject +} from '../../../../shared/models/activitypub' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { logger } from '../../../helpers/logger' import { sequelizeTypescript } from '../../../initializers/database' @@ -11,10 +19,11 @@ import { VideoRedundancyModel } from '../../../models/redundancy/video-redundanc import { VideoShareModel } from '../../../models/video/video-share' import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { MActorSignature } from '../../../types/models' +import { fetchAPObject } from '../activity' import { forwardVideoRelatedActivity } from '../send/shared/send-utils' import { federateVideoIfNeeded, getOrCreateAPVideo } from '../videos' -async function processUndoActivity (options: APProcessorOptions) { +async function processUndoActivity (options: APProcessorOptions>) { const { activity, byActor } = options const activityToUndo = activity.object @@ -23,8 +32,10 @@ async function processUndoActivity (options: APProcessorOptions) { } if (activityToUndo.type === 'Create') { - if (activityToUndo.object.type === 'CacheFile') { - return retryTransactionWrapper(processUndoCacheFile, byActor, activity) + const objectToUndo = await fetchAPObject(activityToUndo.object) + + if (objectToUndo.type === 'CacheFile') { + return retryTransactionWrapper(processUndoCacheFile, byActor, activity, objectToUndo) } } @@ -53,8 +64,8 @@ export { // --------------------------------------------------------------------------- -async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { - const likeActivity = activity.object as ActivityLike +async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo) { + const likeActivity = activity.object const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: likeActivity.object }) // We don't care about likes of remote videos @@ -78,12 +89,10 @@ async function processUndoLike (byActor: MActorSignature, activity: ActivityUndo }) } -async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo) { - const dislike = activity.object.type === 'Dislike' - ? activity.object - : activity.object.object as DislikeObject +async function processUndoDislike (byActor: MActorSignature, activity: ActivityUndo) { + const dislikeActivity = activity.object - const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislike.object }) + const { video: onlyVideo } = await getOrCreateAPVideo({ videoObject: dislikeActivity.object }) // We don't care about likes of remote videos if (!onlyVideo.isOwned()) return @@ -91,7 +100,7 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU if (!byActor.Account) throw new Error('Unknown account ' + byActor.url) const video = await VideoModel.loadFull(onlyVideo.id, t) - const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislike.id, t) + const rate = await AccountVideoRateModel.loadByAccountAndVideoOrUrl(byActor.Account.id, video.id, dislikeActivity.id, t) if (!rate || rate.type !== 'dislike') { logger.warn(`Unknown dislike by account %d for video %d.`, byActor.Account.id, video.id) return @@ -107,9 +116,11 @@ async function processUndoDislike (byActor: MActorSignature, activity: ActivityU // --------------------------------------------------------------------------- -async function processUndoCacheFile (byActor: MActorSignature, activity: ActivityUndo) { - const cacheFileObject = activity.object.object as CacheFileObject - +async function processUndoCacheFile ( + byActor: MActorSignature, + activity: ActivityUndo>, + cacheFileObject: CacheFileObject +) { const { video } = await getOrCreateAPVideo({ videoObject: cacheFileObject.object }) return sequelizeTypescript.transaction(async t => { diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 4afdbd430..9caa74e04 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts @@ -1,5 +1,5 @@ import { isRedundancyAccepted } from '@server/lib/redundancy' -import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' +import { ActivityUpdate, ActivityUpdateObject, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub' import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor' import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object' import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file' @@ -10,16 +10,18 @@ import { sequelizeTypescript } from '../../../initializers/database' import { ActorModel } from '../../../models/actor/actor' import { APProcessorOptions } from '../../../types/activitypub-processor.model' import { MActorFull, MActorSignature } from '../../../types/models' +import { fetchAPObject } from '../activity' import { APActorUpdater } from '../actors/updater' import { createOrUpdateCacheFile } from '../cache-file' import { createOrUpdateVideoPlaylist } from '../playlists' import { forwardVideoRelatedActivity } from '../send/shared/send-utils' import { APVideoUpdater, getOrCreateAPVideo } from '../videos' -async function processUpdateActivity (options: APProcessorOptions) { +async function processUpdateActivity (options: APProcessorOptions>) { const { activity, byActor } = options - const objectType = activity.object.type + const object = await fetchAPObject(activity.object) + const objectType = object.type if (objectType === 'Video') { return retryTransactionWrapper(processUpdateVideo, activity) @@ -28,17 +30,17 @@ async function processUpdateActivity (options: APProcessorOptions) { const videoObject = activity.object as VideoObject if (sanitizeAndCheckVideoTorrentObject(videoObject) === false) { @@ -72,11 +74,13 @@ async function processUpdateVideo (activity: ActivityUpdate) { return updater.update(activity.to) } -async function processUpdateCacheFile (byActor: MActorSignature, activity: ActivityUpdate) { +async function processUpdateCacheFile ( + byActor: MActorSignature, + activity: ActivityUpdate, + cacheFileObject: CacheFileObject +) { if (await isRedundancyAccepted(activity, byActor) !== true) return - const cacheFileObject = activity.object as CacheFileObject - if (!isCacheFileObjectValid(cacheFileObject)) { logger.debug('Cache file object sent by update is not valid.', { cacheFileObject }) return undefined @@ -96,19 +100,19 @@ async function processUpdateCacheFile (byActor: MActorSignature, activity: Activ } } -async function processUpdateActor (actor: MActorFull, activity: ActivityUpdate) { - const actorObject = activity.object as ActivityPubActor - +async function processUpdateActor (actor: MActorFull, actorObject: ActivityPubActor) { logger.debug('Updating remote account "%s".', actorObject.url) const updater = new APActorUpdater(actorObject, actor) return updater.update() } -async function processUpdatePlaylist (byActor: MActorSignature, activity: ActivityUpdate) { - const playlistObject = activity.object as PlaylistObject +async function processUpdatePlaylist ( + byActor: MActorSignature, + activity: ActivityUpdate, + playlistObject: PlaylistObject +) { const byAccount = byActor.Account - if (!byAccount) throw new Error('Cannot update video playlist with the non account actor ' + byActor.url) await createOrUpdateVideoPlaylist(playlistObject, activity.to) diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 0e996ab80..2cd4db14d 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts @@ -1,6 +1,14 @@ import { Transaction } from 'sequelize' import { getServerActor } from '@server/models/application/application' -import { ActivityAudience, ActivityCreate, ContextType, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' +import { + ActivityAudience, + ActivityCreate, + ActivityCreateObject, + ContextType, + VideoCommentObject, + VideoPlaylistPrivacy, + VideoPrivacy +} from '@shared/models' import { logger, loggerTagsFactory } from '../../../helpers/logger' import { VideoCommentModel } from '../../../models/video/video-comment' import { @@ -107,7 +115,7 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: const byActor = comment.Account.Actor const threadParentComments = await VideoCommentModel.listThreadParentComments(comment, transaction) - const commentObject = comment.toActivityPubObject(threadParentComments) + const commentObject = comment.toActivityPubObject(threadParentComments) as VideoCommentObject const actorsInvolvedInComment = await getActorsInvolvedInVideo(comment.Video, transaction) // Add the actor that commented too @@ -168,7 +176,12 @@ async function sendCreateVideoComment (comment: MCommentOwnerVideo, transaction: }) } -function buildCreateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityCreate { +function buildCreateActivity ( + url: string, + byActor: MActorLight, + object: T, + audience?: ActivityAudience +): ActivityCreate { if (!audience) audience = getAudience(byActor) return audiencify( @@ -176,7 +189,9 @@ function buildCreateActivity (url: string, byActor: MActorLight, object: any, au type: 'Create' as 'Create', id: url + '/activity', actor: byActor.url, - object: audiencify(object, audience) + object: typeof object === 'string' + ? object + : audiencify(object, audience) }, audience ) diff --git a/server/lib/activitypub/send/send-undo.ts b/server/lib/activitypub/send/send-undo.ts index b8eb47ff6..b0b48c9c4 100644 --- a/server/lib/activitypub/send/send-undo.ts +++ b/server/lib/activitypub/send/send-undo.ts @@ -1,14 +1,5 @@ import { Transaction } from 'sequelize' -import { - ActivityAnnounce, - ActivityAudience, - ActivityCreate, - ActivityDislike, - ActivityFollow, - ActivityLike, - ActivityUndo, - ContextType -} from '@shared/models' +import { ActivityAudience, ActivityDislike, ActivityLike, ActivityUndo, ActivityUndoObject, ContextType } from '@shared/models' import { logger } from '../../../helpers/logger' import { VideoModel } from '../../../models/video/video' import { @@ -128,12 +119,12 @@ export { // --------------------------------------------------------------------------- -function undoActivityData ( +function undoActivityData ( url: string, byActor: MActorAudience, - object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce, + object: T, audience?: ActivityAudience -): ActivityUndo { +): ActivityUndo { if (!audience) audience = getAudience(byActor) return audiencify( @@ -151,7 +142,7 @@ async function sendUndoVideoRelatedActivity (options: { byActor: MActor video: MVideoAccountLight url: string - activity: ActivityFollow | ActivityCreate | ActivityAnnounce + activity: ActivityUndoObject contextType: ContextType transaction: Transaction }) { diff --git a/server/lib/activitypub/send/send-update.ts b/server/lib/activitypub/send/send-update.ts index 3d2b437e4..f3fb741c6 100644 --- a/server/lib/activitypub/send/send-update.ts +++ b/server/lib/activitypub/send/send-update.ts @@ -1,6 +1,6 @@ import { Transaction } from 'sequelize' import { getServerActor } from '@server/models/application/application' -import { ActivityAudience, ActivityUpdate, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' +import { ActivityAudience, ActivityUpdate, ActivityUpdateObject, VideoPlaylistPrivacy, VideoPrivacy } from '@shared/models' import { logger } from '../../../helpers/logger' import { AccountModel } from '../../../models/account/account' import { VideoModel } from '../../../models/video/video' @@ -137,7 +137,12 @@ export { // --------------------------------------------------------------------------- -function buildUpdateActivity (url: string, byActor: MActorLight, object: any, audience?: ActivityAudience): ActivityUpdate { +function buildUpdateActivity ( + url: string, + byActor: MActorLight, + object: ActivityUpdateObject, + audience?: ActivityAudience +): ActivityUpdate { if (!audience) audience = getAudience(byActor) return audiencify( diff --git a/server/lib/activitypub/videos/get.ts b/server/lib/activitypub/videos/get.ts index 14ba55034..92387c5d4 100644 --- a/server/lib/activitypub/videos/get.ts +++ b/server/lib/activitypub/videos/get.ts @@ -3,7 +3,7 @@ import { logger } from '@server/helpers/logger' import { JobQueue } from '@server/lib/job-queue' import { loadVideoByUrl, VideoLoadByUrlType } from '@server/lib/model-loaders' import { MVideoAccountLightBlacklistAllFiles, MVideoImmutable, MVideoThumbnail } from '@server/types/models' -import { APObject } from '@shared/models' +import { APObjectId } from '@shared/models' import { getAPId } from '../activity' import { refreshVideoIfNeeded } from './refresh' import { APVideoCreator, fetchRemoteVideo, SyncParam, syncVideoExternalAttributes } from './shared' @@ -15,21 +15,21 @@ type GetVideoResult = Promise<{ }> type GetVideoParamAll = { - videoObject: APObject + videoObject: APObjectId syncParam?: SyncParam fetchType?: 'all' allowRefresh?: boolean } type GetVideoParamImmutable = { - videoObject: APObject + videoObject: APObjectId syncParam?: SyncParam fetchType: 'only-immutable-attributes' allowRefresh: false } type GetVideoParamOther = { - videoObject: APObject + videoObject: APObjectId syncParam?: SyncParam fetchType?: 'all' | 'only-video' allowRefresh?: boolean diff --git a/server/tests/api/server/follows.ts b/server/tests/api/server/follows.ts index 2a5fff82b..e3e4605ee 100644 --- a/server/tests/api/server/follows.ts +++ b/server/tests/api/server/follows.ts @@ -6,611 +6,636 @@ import { Video, VideoPrivacy } from '@shared/models' import { cleanupTests, createMultipleServers, PeerTubeServer, setAccessTokensToServers, waitJobs } from '@shared/server-commands' describe('Test follows', function () { - let servers: PeerTubeServer[] = [] - before(async function () { - this.timeout(120000) + describe('Complex follow', function () { + let servers: PeerTubeServer[] = [] - servers = await createMultipleServers(3) + before(async function () { + this.timeout(120000) - // Get the access tokens - await setAccessTokensToServers(servers) - }) + servers = await createMultipleServers(3) - describe('Data propagation after follow', function () { + // Get the access tokens + await setAccessTokensToServers(servers) + }) - it('Should not have followers/followings', async function () { - for (const server of servers) { - const bodies = await Promise.all([ - server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }), - server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) - ]) + describe('Data propagation after follow', function () { - for (const body of bodies) { + it('Should not have followers/followings', async function () { + for (const server of servers) { + const bodies = await Promise.all([ + server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }), + server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) + ]) + + for (const body of bodies) { + expect(body.total).to.equal(0) + + const follows = body.data + expect(follows).to.be.an('array') + expect(follows).to.have.lengthOf(0) + } + } + }) + + it('Should have server 1 following root account of server 2 and server 3', async function () { + this.timeout(30000) + + await servers[0].follows.follow({ + hosts: [ servers[2].url ], + handles: [ 'root@' + servers[1].host ] + }) + + await waitJobs(servers) + }) + + it('Should have 2 followings on server 1', async function () { + const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' }) + expect(body.total).to.equal(2) + + let follows = body.data + expect(follows).to.be.an('array') + expect(follows).to.have.lengthOf(1) + + const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' }) + follows = follows.concat(body2.data) + + const server2Follow = follows.find(f => f.following.host === servers[1].host) + const server3Follow = follows.find(f => f.following.host === servers[2].host) + + expect(server2Follow).to.not.be.undefined + expect(server2Follow.following.name).to.equal('root') + expect(server2Follow.state).to.equal('accepted') + + expect(server3Follow).to.not.be.undefined + expect(server3Follow.following.name).to.equal('peertube') + expect(server3Follow.state).to.equal('accepted') + }) + + it('Should have 0 followings on server 2 and 3', async function () { + for (const server of [ servers[1], servers[2] ]) { + const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) expect(body.total).to.equal(0) const follows = body.data expect(follows).to.be.an('array') expect(follows).to.have.lengthOf(0) } - } - }) - - it('Should have server 1 following root account of server 2 and server 3', async function () { - this.timeout(30000) - - await servers[0].follows.follow({ - hosts: [ servers[2].url ], - handles: [ 'root@' + servers[1].host ] }) - await waitJobs(servers) - }) - - it('Should have 2 followings on server 1', async function () { - const body = await servers[0].follows.getFollowings({ start: 0, count: 1, sort: 'createdAt' }) - expect(body.total).to.equal(2) - - let follows = body.data - expect(follows).to.be.an('array') - expect(follows).to.have.lengthOf(1) - - const body2 = await servers[0].follows.getFollowings({ start: 1, count: 1, sort: 'createdAt' }) - follows = follows.concat(body2.data) - - const server2Follow = follows.find(f => f.following.host === servers[1].host) - const server3Follow = follows.find(f => f.following.host === servers[2].host) - - expect(server2Follow).to.not.be.undefined - expect(server2Follow.following.name).to.equal('root') - expect(server2Follow.state).to.equal('accepted') - - expect(server3Follow).to.not.be.undefined - expect(server3Follow.following.name).to.equal('peertube') - expect(server3Follow.state).to.equal('accepted') - }) - - it('Should have 0 followings on server 2 and 3', async function () { - for (const server of [ servers[1], servers[2] ]) { - const body = await server.follows.getFollowings({ start: 0, count: 5, sort: 'createdAt' }) - expect(body.total).to.equal(0) + it('Should have 1 followers on server 3', async function () { + const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) + expect(body.total).to.equal(1) const follows = body.data expect(follows).to.be.an('array') - expect(follows).to.have.lengthOf(0) - } - }) + expect(follows).to.have.lengthOf(1) + expect(follows[0].follower.host).to.equal(servers[0].host) + }) - it('Should have 1 followers on server 3', async function () { - const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) - expect(body.total).to.equal(1) - - const follows = body.data - expect(follows).to.be.an('array') - expect(follows).to.have.lengthOf(1) - expect(follows[0].follower.host).to.equal(servers[0].host) - }) - - it('Should have 0 followers on server 1 and 2', async function () { - for (const server of [ servers[0], servers[1] ]) { - const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }) - expect(body.total).to.equal(0) - - const follows = body.data - expect(follows).to.be.an('array') - expect(follows).to.have.lengthOf(0) - } - }) - - it('Should search/filter followings on server 1', async function () { - const sort = 'createdAt' - const start = 0 - const count = 1 - - { - const search = ':' + servers[1].port - - { - const body = await servers[0].follows.getFollowings({ start, count, sort, search }) - expect(body.total).to.equal(1) + it('Should have 0 followers on server 1 and 2', async function () { + for (const server of [ servers[0], servers[1] ]) { + const body = await server.follows.getFollowers({ start: 0, count: 5, sort: 'createdAt' }) + expect(body.total).to.equal(0) const follows = body.data - expect(follows).to.have.lengthOf(1) - expect(follows[0].following.host).to.equal(servers[1].host) + expect(follows).to.be.an('array') + expect(follows).to.have.lengthOf(0) + } + }) + + it('Should search/filter followings on server 1', async function () { + const sort = 'createdAt' + const start = 0 + const count = 1 + + { + const search = ':' + servers[1].port + + { + const body = await servers[0].follows.getFollowings({ start, count, sort, search }) + expect(body.total).to.equal(1) + + const follows = body.data + expect(follows).to.have.lengthOf(1) + expect(follows[0].following.host).to.equal(servers[1].host) + } + + { + const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' }) + expect(body.total).to.equal(1) + expect(body.data).to.have.lengthOf(1) + } + + { + const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) + expect(body.total).to.equal(1) + expect(body.data).to.have.lengthOf(1) + } + + { + const body = await servers[0].follows.getFollowings({ + start, + count, + sort, + search, + state: 'accepted', + actorType: 'Application' + }) + expect(body.total).to.equal(0) + expect(body.data).to.have.lengthOf(0) + } + + { + const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' }) + expect(body.total).to.equal(0) + expect(body.data).to.have.lengthOf(0) + } } { - const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted' }) + const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' }) expect(body.total).to.equal(1) expect(body.data).to.have.lengthOf(1) } { - const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) - expect(body.total).to.equal(1) - expect(body.data).to.have.lengthOf(1) + const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' }) + expect(body.total).to.equal(0) + + expect(body.data).to.have.lengthOf(0) + } + }) + + it('Should search/filter followers on server 2', async function () { + const start = 0 + const count = 5 + const sort = 'createdAt' + + { + const search = servers[0].port + '' + + { + const body = await servers[2].follows.getFollowers({ start, count, sort, search }) + expect(body.total).to.equal(1) + + const follows = body.data + expect(follows).to.have.lengthOf(1) + expect(follows[0].following.host).to.equal(servers[2].host) + } + + { + const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' }) + expect(body.total).to.equal(1) + expect(body.data).to.have.lengthOf(1) + } + + { + const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) + expect(body.total).to.equal(0) + expect(body.data).to.have.lengthOf(0) + } + + { + const body = await servers[2].follows.getFollowers({ + start, + count, + sort, + search, + state: 'accepted', + actorType: 'Application' + }) + expect(body.total).to.equal(1) + expect(body.data).to.have.lengthOf(1) + } + + { + const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' }) + expect(body.total).to.equal(0) + expect(body.data).to.have.lengthOf(0) + } } { - const body = await servers[0].follows.getFollowings({ - start, - count, - sort, - search, - state: 'accepted', - actorType: 'Application' - }) + const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' }) expect(body.total).to.equal(0) - expect(body.data).to.have.lengthOf(0) + + const follows = body.data + expect(follows).to.have.lengthOf(0) + } + }) + + it('Should have the correct follows counts', async function () { + await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) + await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) + await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) + + // Server 2 and 3 does not know server 1 follow another server (there was not a refresh) + await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) + await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) + await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) + + await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) + await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) + }) + + it('Should unfollow server 3 on server 1', async function () { + this.timeout(15000) + + await servers[0].follows.unfollow({ target: servers[2] }) + + await waitJobs(servers) + }) + + it('Should not follow server 3 on server 1 anymore', async function () { + const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' }) + expect(body.total).to.equal(1) + + const follows = body.data + expect(follows).to.be.an('array') + expect(follows).to.have.lengthOf(1) + + expect(follows[0].following.host).to.equal(servers[1].host) + }) + + it('Should not have server 1 as follower on server 3 anymore', async function () { + const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) + expect(body.total).to.equal(0) + + const follows = body.data + expect(follows).to.be.an('array') + expect(follows).to.have.lengthOf(0) + }) + + it('Should have the correct follows counts after the unfollow', async function () { + await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) + await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) + await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) + + await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) + await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) + await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) + + await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 }) + await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) + }) + + it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () { + this.timeout(160000) + + await servers[1].videos.upload({ attributes: { name: 'server2' } }) + await servers[2].videos.upload({ attributes: { name: 'server3' } }) + + await waitJobs(servers) + + { + const { total, data } = await servers[0].videos.list() + expect(total).to.equal(1) + expect(data[0].name).to.equal('server2') } { - const body = await servers[0].follows.getFollowings({ start, count, sort, search, state: 'pending' }) - expect(body.total).to.equal(0) - expect(body.data).to.have.lengthOf(0) + const { total, data } = await servers[1].videos.list() + expect(total).to.equal(1) + expect(data[0].name).to.equal('server2') } - } - { - const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'root' }) + { + const { total, data } = await servers[2].videos.list() + expect(total).to.equal(1) + expect(data[0].name).to.equal('server3') + } + }) + + it('Should remove account follow', async function () { + this.timeout(15000) + + await servers[0].follows.unfollow({ target: 'root@' + servers[1].host }) + + await waitJobs(servers) + }) + + it('Should have removed the account follow', async function () { + await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) + await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) + + { + const { total, data } = await servers[0].follows.getFollowings() + expect(total).to.equal(0) + expect(data).to.have.lengthOf(0) + } + + { + const { total, data } = await servers[0].videos.list() + expect(total).to.equal(0) + expect(data).to.have.lengthOf(0) + } + }) + + it('Should follow a channel', async function () { + this.timeout(15000) + + await servers[0].follows.follow({ + handles: [ 'root_channel@' + servers[1].host ] + }) + + await waitJobs(servers) + + await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) + await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) + + { + const { total, data } = await servers[0].follows.getFollowings() + expect(total).to.equal(1) + expect(data).to.have.lengthOf(1) + } + + { + const { total, data } = await servers[0].videos.list() + expect(total).to.equal(1) + expect(data).to.have.lengthOf(1) + } + }) + }) + + describe('Should propagate data on a new server follow', function () { + let video4: Video + + before(async function () { + this.timeout(240000) + + const video4Attributes = { + name: 'server3-4', + category: 2, + nsfw: true, + licence: 6, + tags: [ 'tag1', 'tag2', 'tag3' ] + } + + await servers[2].videos.upload({ attributes: { name: 'server3-2' } }) + await servers[2].videos.upload({ attributes: { name: 'server3-3' } }) + + const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes }) + + await servers[2].videos.upload({ attributes: { name: 'server3-5' } }) + await servers[2].videos.upload({ attributes: { name: 'server3-6' } }) + + { + const userAccessToken = await servers[2].users.generateUserAndToken('captain') + + await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' }) + await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' }) + } + + { + await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' }) + + await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' }) + await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' }) + await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' }) + } + + { + const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' }) + await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' }) + + const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' }) + + await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' }) + + await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId }) + await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId }) + } + + await servers[2].captions.add({ + language: 'ar', + videoId: video4CreateResult.id, + fixture: 'subtitle-good2.vtt' + }) + + await waitJobs(servers) + + // Server 1 follows server 3 + await servers[0].follows.follow({ hosts: [ servers[2].url ] }) + + await waitJobs(servers) + }) + + it('Should have the correct follows counts', async function () { + await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) + await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) + await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) + await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) + + await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) + await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) + await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) + await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) + + await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) + await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) + }) + + it('Should have propagated videos', async function () { + const { total, data } = await servers[0].videos.list() + expect(total).to.equal(7) + + const video2 = data.find(v => v.name === 'server3-2') + video4 = data.find(v => v.name === 'server3-4') + const video6 = data.find(v => v.name === 'server3-6') + + expect(video2).to.not.be.undefined + expect(video4).to.not.be.undefined + expect(video6).to.not.be.undefined + + const isLocal = false + const checkAttributes = { + name: 'server3-4', + category: 2, + licence: 6, + language: 'zh', + nsfw: true, + description: 'my super description', + support: 'my super support text', + account: { + name: 'root', + host: servers[2].host + }, + isLocal, + commentsEnabled: true, + downloadEnabled: true, + duration: 5, + tags: [ 'tag1', 'tag2', 'tag3' ], + privacy: VideoPrivacy.PUBLIC, + likes: 1, + dislikes: 1, + channel: { + displayName: 'Main root channel', + name: 'root_channel', + description: '', + isLocal + }, + fixture: 'video_short.webm', + files: [ + { + resolution: 720, + size: 218910 + } + ] + } + await completeVideoCheck({ + server: servers[0], + originServer: servers[2], + videoUUID: video4.uuid, + attributes: checkAttributes + }) + }) + + it('Should have propagated comments', async function () { + const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' }) + + expect(total).to.equal(2) + expect(data).to.be.an('array') + expect(data).to.have.lengthOf(2) + + { + const comment = data[0] + expect(comment.inReplyToCommentId).to.be.null + expect(comment.text).equal('my super first comment') + expect(comment.videoId).to.equal(video4.id) + expect(comment.id).to.equal(comment.threadId) + expect(comment.account.name).to.equal('root') + expect(comment.account.host).to.equal(servers[2].host) + expect(comment.totalReplies).to.equal(3) + expect(dateIsValid(comment.createdAt as string)).to.be.true + expect(dateIsValid(comment.updatedAt as string)).to.be.true + + const threadId = comment.threadId + + const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId }) + expect(tree.comment.text).equal('my super first comment') + expect(tree.children).to.have.lengthOf(2) + + const firstChild = tree.children[0] + expect(firstChild.comment.text).to.equal('my super answer to thread 1') + expect(firstChild.children).to.have.lengthOf(1) + + const childOfFirstChild = firstChild.children[0] + expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') + expect(childOfFirstChild.children).to.have.lengthOf(0) + + const secondChild = tree.children[1] + expect(secondChild.comment.text).to.equal('my second answer to thread 1') + expect(secondChild.children).to.have.lengthOf(0) + } + + { + const deletedComment = data[1] + expect(deletedComment).to.not.be.undefined + expect(deletedComment.isDeleted).to.be.true + expect(deletedComment.deletedAt).to.not.be.null + expect(deletedComment.text).to.equal('') + expect(deletedComment.inReplyToCommentId).to.be.null + expect(deletedComment.account).to.be.null + expect(deletedComment.totalReplies).to.equal(2) + expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true + + const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId }) + const [ commentRoot, deletedChildRoot ] = tree.children + + expect(deletedChildRoot).to.not.be.undefined + expect(deletedChildRoot.comment.isDeleted).to.be.true + expect(deletedChildRoot.comment.deletedAt).to.not.be.null + expect(deletedChildRoot.comment.text).to.equal('') + expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) + expect(deletedChildRoot.comment.account).to.be.null + expect(deletedChildRoot.children).to.have.lengthOf(1) + + const answerToDeletedChild = deletedChildRoot.children[0] + expect(answerToDeletedChild.comment).to.not.be.undefined + expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id) + expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted') + expect(answerToDeletedChild.comment.account.name).to.equal('root') + + expect(commentRoot.comment).to.not.be.undefined + expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) + expect(commentRoot.comment.text).to.equal('answer to deleted') + expect(commentRoot.comment.account.name).to.equal('root') + } + }) + + it('Should have propagated captions', async function () { + const body = await servers[0].captions.list({ videoId: video4.id }) expect(body.total).to.equal(1) expect(body.data).to.have.lengthOf(1) - } - { - const body = await servers[0].follows.getFollowings({ start, count, sort, search: 'bla' }) - expect(body.total).to.equal(0) - - expect(body.data).to.have.lengthOf(0) - } - }) - - it('Should search/filter followers on server 2', async function () { - const start = 0 - const count = 5 - const sort = 'createdAt' - - { - const search = servers[0].port + '' - - { - const body = await servers[2].follows.getFollowers({ start, count, sort, search }) - expect(body.total).to.equal(1) - - const follows = body.data - expect(follows).to.have.lengthOf(1) - expect(follows[0].following.host).to.equal(servers[2].host) - } - - { - const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted' }) - expect(body.total).to.equal(1) - expect(body.data).to.have.lengthOf(1) - } - - { - const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'accepted', actorType: 'Person' }) - expect(body.total).to.equal(0) - expect(body.data).to.have.lengthOf(0) - } - - { - const body = await servers[2].follows.getFollowers({ - start, - count, - sort, - search, - state: 'accepted', - actorType: 'Application' - }) - expect(body.total).to.equal(1) - expect(body.data).to.have.lengthOf(1) - } - - { - const body = await servers[2].follows.getFollowers({ start, count, sort, search, state: 'pending' }) - expect(body.total).to.equal(0) - expect(body.data).to.have.lengthOf(0) - } - } - - { - const body = await servers[2].follows.getFollowers({ start, count, sort, search: 'bla' }) - expect(body.total).to.equal(0) - - const follows = body.data - expect(follows).to.have.lengthOf(0) - } - }) - - it('Should have the correct follows counts', async function () { - await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) - await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) - await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) - - // Server 2 and 3 does not know server 1 follow another server (there was not a refresh) - await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) - await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) - await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) - - await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) - await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) - }) - - it('Should unfollow server 3 on server 1', async function () { - this.timeout(15000) - - await servers[0].follows.unfollow({ target: servers[2] }) - - await waitJobs(servers) - }) - - it('Should not follow server 3 on server 1 anymore', async function () { - const body = await servers[0].follows.getFollowings({ start: 0, count: 2, sort: 'createdAt' }) - expect(body.total).to.equal(1) - - const follows = body.data - expect(follows).to.be.an('array') - expect(follows).to.have.lengthOf(1) - - expect(follows[0].following.host).to.equal(servers[1].host) - }) - - it('Should not have server 1 as follower on server 3 anymore', async function () { - const body = await servers[2].follows.getFollowers({ start: 0, count: 1, sort: 'createdAt' }) - expect(body.total).to.equal(0) - - const follows = body.data - expect(follows).to.be.an('array') - expect(follows).to.have.lengthOf(0) - }) - - it('Should have the correct follows counts after the unfollow', async function () { - await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) - await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) - await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) - - await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) - await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 1, following: 0 }) - await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) - - await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 0 }) - await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 0, following: 0 }) - }) - - it('Should upload a video on server 2 and 3 and propagate only the video of server 2', async function () { - this.timeout(160000) - - await servers[1].videos.upload({ attributes: { name: 'server2' } }) - await servers[2].videos.upload({ attributes: { name: 'server3' } }) - - await waitJobs(servers) - - { - const { total, data } = await servers[0].videos.list() - expect(total).to.equal(1) - expect(data[0].name).to.equal('server2') - } - - { - const { total, data } = await servers[1].videos.list() - expect(total).to.equal(1) - expect(data[0].name).to.equal('server2') - } - - { - const { total, data } = await servers[2].videos.list() - expect(total).to.equal(1) - expect(data[0].name).to.equal('server3') - } - }) - - it('Should remove account follow', async function () { - this.timeout(15000) - - await servers[0].follows.unfollow({ target: 'root@' + servers[1].host }) - - await waitJobs(servers) - }) - - it('Should have removed the account follow', async function () { - await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) - await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) - - { - const { total, data } = await servers[0].follows.getFollowings() - expect(total).to.equal(0) - expect(data).to.have.lengthOf(0) - } - - { - const { total, data } = await servers[0].videos.list() - expect(total).to.equal(0) - expect(data).to.have.lengthOf(0) - } - }) - - it('Should follow a channel', async function () { - this.timeout(15000) - - await servers[0].follows.follow({ - handles: [ 'root_channel@' + servers[1].host ] + const caption1 = body.data[0] + expect(caption1.language.id).to.equal('ar') + expect(caption1.language.label).to.equal('Arabic') + expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$')) + await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.') }) - await waitJobs(servers) + it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () { + this.timeout(5000) - await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) - await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) + await servers[0].follows.unfollow({ target: servers[2] }) - { - const { total, data } = await servers[0].follows.getFollowings() + await waitJobs(servers) + + const { total } = await servers[0].videos.list() expect(total).to.equal(1) - expect(data).to.have.lengthOf(1) - } + }) + }) - { - const { total, data } = await servers[0].videos.list() - expect(total).to.equal(1) - expect(data).to.have.lengthOf(1) - } + after(async function () { + await cleanupTests(servers) }) }) - describe('Should propagate data on a new server follow', function () { - let video4: Video + describe('Simple data propagation propagate data on a new channel follow', function () { + let servers: PeerTubeServer[] = [] before(async function () { this.timeout(120000) - const video4Attributes = { - name: 'server3-4', - category: 2, - nsfw: true, - licence: 6, - tags: [ 'tag1', 'tag2', 'tag3' ] - } + servers = await createMultipleServers(3) + await setAccessTokensToServers(servers) - await servers[2].videos.upload({ attributes: { name: 'server3-2' } }) - await servers[2].videos.upload({ attributes: { name: 'server3-3' } }) - const video4CreateResult = await servers[2].videos.upload({ attributes: video4Attributes }) - await servers[2].videos.upload({ attributes: { name: 'server3-5' } }) - await servers[2].videos.upload({ attributes: { name: 'server3-6' } }) - - { - const userAccessToken = await servers[2].users.generateUserAndToken('captain') - - await servers[2].videos.rate({ id: video4CreateResult.id, rating: 'like' }) - await servers[2].videos.rate({ token: userAccessToken, id: video4CreateResult.id, rating: 'dislike' }) - } - - { - await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'my super first comment' }) - - await servers[2].comments.addReplyToLastThread({ text: 'my super answer to thread 1' }) - await servers[2].comments.addReplyToLastReply({ text: 'my super answer to answer of thread 1' }) - await servers[2].comments.addReplyToLastThread({ text: 'my second answer to thread 1' }) - } - - { - const { id: threadId } = await servers[2].comments.createThread({ videoId: video4CreateResult.id, text: 'will be deleted' }) - await servers[2].comments.addReplyToLastThread({ text: 'answer to deleted' }) - - const { id: replyId } = await servers[2].comments.addReplyToLastThread({ text: 'will also be deleted' }) - - await servers[2].comments.addReplyToLastReply({ text: 'my second answer to deleted' }) - - await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: threadId }) - await servers[2].comments.delete({ videoId: video4CreateResult.id, commentId: replyId }) - } - - await servers[2].captions.add({ - language: 'ar', - videoId: video4CreateResult.id, - fixture: 'subtitle-good2.vtt' - }) + await servers[0].videos.upload({ attributes: { name: 'video to add' } }) await waitJobs(servers) - // Server 1 follows server 3 - await servers[0].follows.follow({ hosts: [ servers[2].url ] }) - - await waitJobs(servers) - }) - - it('Should have the correct follows counts', async function () { - await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[0].host, followers: 0, following: 2 }) - await expectAccountFollows({ server: servers[0], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) - await expectChannelsFollows({ server: servers[0], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) - await expectAccountFollows({ server: servers[0], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) - - await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) - await expectAccountFollows({ server: servers[1], handle: 'peertube@' + servers[1].host, followers: 0, following: 0 }) - await expectAccountFollows({ server: servers[1], handle: 'root@' + servers[1].host, followers: 0, following: 0 }) - await expectChannelsFollows({ server: servers[1], handle: 'root_channel@' + servers[1].host, followers: 1, following: 0 }) - - await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[0].host, followers: 0, following: 1 }) - await expectAccountFollows({ server: servers[2], handle: 'peertube@' + servers[2].host, followers: 1, following: 0 }) - }) - - it('Should have propagated videos', async function () { - const { total, data } = await servers[0].videos.list() - expect(total).to.equal(7) - - const video2 = data.find(v => v.name === 'server3-2') - video4 = data.find(v => v.name === 'server3-4') - const video6 = data.find(v => v.name === 'server3-6') - - expect(video2).to.not.be.undefined - expect(video4).to.not.be.undefined - expect(video6).to.not.be.undefined - - const isLocal = false - const checkAttributes = { - name: 'server3-4', - category: 2, - licence: 6, - language: 'zh', - nsfw: true, - description: 'my super description', - support: 'my super support text', - account: { - name: 'root', - host: servers[2].host - }, - isLocal, - commentsEnabled: true, - downloadEnabled: true, - duration: 5, - tags: [ 'tag1', 'tag2', 'tag3' ], - privacy: VideoPrivacy.PUBLIC, - likes: 1, - dislikes: 1, - channel: { - displayName: 'Main root channel', - name: 'root_channel', - description: '', - isLocal - }, - fixture: 'video_short.webm', - files: [ - { - resolution: 720, - size: 218910 - } - ] - } - await completeVideoCheck({ - server: servers[0], - originServer: servers[2], - videoUUID: video4.uuid, - attributes: checkAttributes - }) - }) - - it('Should have propagated comments', async function () { - const { total, data } = await servers[0].comments.listThreads({ videoId: video4.id, sort: 'createdAt' }) - - expect(total).to.equal(2) - expect(data).to.be.an('array') - expect(data).to.have.lengthOf(2) - - { - const comment = data[0] - expect(comment.inReplyToCommentId).to.be.null - expect(comment.text).equal('my super first comment') - expect(comment.videoId).to.equal(video4.id) - expect(comment.id).to.equal(comment.threadId) - expect(comment.account.name).to.equal('root') - expect(comment.account.host).to.equal(servers[2].host) - expect(comment.totalReplies).to.equal(3) - expect(dateIsValid(comment.createdAt as string)).to.be.true - expect(dateIsValid(comment.updatedAt as string)).to.be.true - - const threadId = comment.threadId - - const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId }) - expect(tree.comment.text).equal('my super first comment') - expect(tree.children).to.have.lengthOf(2) - - const firstChild = tree.children[0] - expect(firstChild.comment.text).to.equal('my super answer to thread 1') - expect(firstChild.children).to.have.lengthOf(1) - - const childOfFirstChild = firstChild.children[0] - expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1') - expect(childOfFirstChild.children).to.have.lengthOf(0) - - const secondChild = tree.children[1] - expect(secondChild.comment.text).to.equal('my second answer to thread 1') - expect(secondChild.children).to.have.lengthOf(0) - } - - { - const deletedComment = data[1] - expect(deletedComment).to.not.be.undefined - expect(deletedComment.isDeleted).to.be.true - expect(deletedComment.deletedAt).to.not.be.null - expect(deletedComment.text).to.equal('') - expect(deletedComment.inReplyToCommentId).to.be.null - expect(deletedComment.account).to.be.null - expect(deletedComment.totalReplies).to.equal(2) - expect(dateIsValid(deletedComment.deletedAt as string)).to.be.true - - const tree = await servers[0].comments.getThread({ videoId: video4.id, threadId: deletedComment.threadId }) - const [ commentRoot, deletedChildRoot ] = tree.children - - expect(deletedChildRoot).to.not.be.undefined - expect(deletedChildRoot.comment.isDeleted).to.be.true - expect(deletedChildRoot.comment.deletedAt).to.not.be.null - expect(deletedChildRoot.comment.text).to.equal('') - expect(deletedChildRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) - expect(deletedChildRoot.comment.account).to.be.null - expect(deletedChildRoot.children).to.have.lengthOf(1) - - const answerToDeletedChild = deletedChildRoot.children[0] - expect(answerToDeletedChild.comment).to.not.be.undefined - expect(answerToDeletedChild.comment.inReplyToCommentId).to.equal(deletedChildRoot.comment.id) - expect(answerToDeletedChild.comment.text).to.equal('my second answer to deleted') - expect(answerToDeletedChild.comment.account.name).to.equal('root') - - expect(commentRoot.comment).to.not.be.undefined - expect(commentRoot.comment.inReplyToCommentId).to.equal(deletedComment.id) - expect(commentRoot.comment.text).to.equal('answer to deleted') - expect(commentRoot.comment.account.name).to.equal('root') + for (const server of [ servers[1], servers[2] ]) { + const video = await server.videos.find({ name: 'video to add' }) + expect(video).to.not.exist } }) - it('Should have propagated captions', async function () { - const body = await servers[0].captions.list({ videoId: video4.id }) - expect(body.total).to.equal(1) - expect(body.data).to.have.lengthOf(1) - - const caption1 = body.data[0] - expect(caption1.language.id).to.equal('ar') - expect(caption1.language.label).to.equal('Arabic') - expect(caption1.captionPath).to.match(new RegExp('^/lazy-static/video-captions/.+-ar.vtt$')) - await testCaptionFile(servers[0].url, caption1.captionPath, 'Subtitle good 2.') - }) - - it('Should unfollow server 3 on server 1 and does not list server 3 videos', async function () { - this.timeout(5000) - - await servers[0].follows.unfollow({ target: servers[2] }) - - await waitJobs(servers) - - const { total } = await servers[0].videos.list() - expect(total).to.equal(1) - }) - }) - - describe('Should propagate data on a new channel follow', function () { - - before(async function () { + it('Should have propagated video after new channel follow', async function () { this.timeout(60000) - await servers[2].videos.upload({ attributes: { name: 'server3-7' } }) + await servers[1].follows.follow({ handles: [ 'root_channel@' + servers[0].host ] }) await waitJobs(servers) - const video = await servers[0].videos.find({ name: 'server3-7' }) - expect(video).to.not.exist - }) - - it('Should have propagated channel video', async function () { - this.timeout(60000) - - await servers[0].follows.follow({ handles: [ 'root_channel@' + servers[2].host ] }) - - await waitJobs(servers) - - const video = await servers[0].videos.find({ name: 'server3-7' }) - + const video = await servers[1].videos.find({ name: 'video to add' }) expect(video).to.exist }) - }) - after(async function () { - await cleanupTests(servers) + it('Should have propagated video after new account follow', async function () { + this.timeout(60000) + + await servers[2].follows.follow({ handles: [ 'root@' + servers[0].host ] }) + + await waitJobs(servers) + + const video = await servers[2].videos.find({ name: 'video to add' }) + expect(video).to.exist + }) + + after(async function () { + await cleanupTests(servers) + }) }) }) diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index fd5d38316..10cf53ead 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts @@ -1,20 +1,34 @@ import { ActivityPubActor } from './activitypub-actor' import { ActivityPubSignature } from './activitypub-signature' -import { ActivityFlagReasonObject, CacheFileObject, VideoObject, WatchActionObject } from './objects' -import { AbuseObject } from './objects/abuse-object' -import { DislikeObject } from './objects/dislike-object' -import { APObject } from './objects/object.model' -import { PlaylistObject } from './objects/playlist-object' -import { VideoCommentObject } from './objects/video-comment-object' +import { + ActivityFlagReasonObject, + ActivityObject, + APObjectId, + CacheFileObject, + PlaylistObject, + VideoCommentObject, + VideoObject, + WatchActionObject +} from './objects' + +export type ActivityUpdateObject = + Extract | ActivityPubActor + +// Cannot Extract from Activity because of circular reference +export type ActivityUndoObject = + ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce + +export type ActivityCreateObject = + Extract export type Activity = - ActivityCreate | - ActivityUpdate | + ActivityCreate | + ActivityUpdate | ActivityDelete | ActivityFollow | ActivityAccept | ActivityAnnounce | - ActivityUndo | + ActivityUndo | ActivityLike | ActivityReject | ActivityView | @@ -50,19 +64,19 @@ export interface BaseActivity { signature?: ActivityPubSignature } -export interface ActivityCreate extends BaseActivity { +export interface ActivityCreate extends BaseActivity { type: 'Create' - object: VideoObject | AbuseObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject | WatchActionObject + object: T } -export interface ActivityUpdate extends BaseActivity { +export interface ActivityUpdate extends BaseActivity { type: 'Update' - object: VideoObject | ActivityPubActor | CacheFileObject | PlaylistObject + object: T } export interface ActivityDelete extends BaseActivity { type: 'Delete' - object: string | { id: string } + object: APObjectId } export interface ActivityFollow extends BaseActivity { @@ -82,23 +96,23 @@ export interface ActivityReject extends BaseActivity { export interface ActivityAnnounce extends BaseActivity { type: 'Announce' - object: APObject + object: APObjectId } -export interface ActivityUndo extends BaseActivity { +export interface ActivityUndo extends BaseActivity { type: 'Undo' - object: ActivityFollow | ActivityLike | ActivityDislike | ActivityCreate | ActivityAnnounce + object: T } export interface ActivityLike extends BaseActivity { type: 'Like' - object: APObject + object: APObjectId } export interface ActivityView extends BaseActivity { type: 'View' actor: string - object: APObject + object: APObjectId // If sending a "viewer" event expires?: string @@ -108,13 +122,13 @@ export interface ActivityDislike extends BaseActivity { id: string type: 'Dislike' actor: string - object: APObject + object: APObjectId } export interface ActivityFlag extends BaseActivity { type: 'Flag' content: string - object: APObject | APObject[] + object: APObjectId | APObjectId[] tag?: ActivityFlagReasonObject[] startAt?: number endAt?: number diff --git a/shared/models/activitypub/objects/activitypub-object.ts b/shared/models/activitypub/objects/activitypub-object.ts new file mode 100644 index 000000000..faeac2618 --- /dev/null +++ b/shared/models/activitypub/objects/activitypub-object.ts @@ -0,0 +1,17 @@ +import { AbuseObject } from './abuse-object' +import { CacheFileObject } from './cache-file-object' +import { PlaylistObject } from './playlist-object' +import { VideoCommentObject } from './video-comment-object' +import { VideoObject } from './video-object' +import { WatchActionObject } from './watch-action-object' + +export type ActivityObject = + VideoObject | + AbuseObject | + VideoCommentObject | + CacheFileObject | + PlaylistObject | + WatchActionObject | + string + +export type APObjectId = string | { id: string } diff --git a/shared/models/activitypub/objects/dislike-object.ts b/shared/models/activitypub/objects/dislike-object.ts deleted file mode 100644 index 7218fb784..000000000 --- a/shared/models/activitypub/objects/dislike-object.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface DislikeObject { - id: string - type: 'Dislike' - actor: string - object: string -} diff --git a/shared/models/activitypub/objects/index.ts b/shared/models/activitypub/objects/index.ts index a2e040b32..753e02003 100644 --- a/shared/models/activitypub/objects/index.ts +++ b/shared/models/activitypub/objects/index.ts @@ -1,8 +1,7 @@ export * from './abuse-object' +export * from './activitypub-object' export * from './cache-file-object' export * from './common-objects' -export * from './dislike-object' -export * from './object.model' export * from './playlist-element-object' export * from './playlist-object' export * from './video-comment-object' diff --git a/shared/models/activitypub/objects/object.model.ts b/shared/models/activitypub/objects/object.model.ts deleted file mode 100644 index 3fd33800a..000000000 --- a/shared/models/activitypub/objects/object.model.ts +++ /dev/null @@ -1 +0,0 @@ -export type APObject = string | { id: string }