From 7a7724e66e4533523083e7336cd0d0c747c4a33b Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 13 Nov 2017 17:39:41 +0100 Subject: [PATCH] Handle follow/accept --- client/src/standalone/videos/embed.html | 2 +- server/controllers/activitypub/client.ts | 8 +- server/controllers/activitypub/inbox.ts | 29 +++- server/controllers/activitypub/index.ts | 2 +- server/controllers/activitypub/videos.ts | 61 -------- server/controllers/api/pods.ts | 33 ++++- .../custom-validators/video-accounts.ts | 8 + server/helpers/utils.ts | 11 ++ server/initializers/constants.ts | 42 ++---- server/lib/activitypub/process-accept.ts | 27 ++++ server/lib/activitypub/process-delete.ts | 105 +++++++++++++ server/lib/activitypub/process-follow.ts | 32 ++++ server/lib/activitypub/send-request.ts | 26 ++-- server/middlewares/sort.ts | 16 +- server/middlewares/validators/account.ts | 25 ++-- server/middlewares/validators/sort.ts | 11 +- .../account/account-follow-interface.ts | 8 +- server/models/account/account-follow.ts | 30 +++- server/models/account/account-interface.ts | 24 ++- server/models/account/account.ts | 138 +++++++++++++++--- server/models/video/video-channel.ts | 7 +- server/models/video/video-interface.ts | 17 +-- server/models/video/video.ts | 9 +- server/tests/real-world/real-world.ts | 3 +- shared/models/accounts/account.model.ts | 5 + shared/models/accounts/follow.model.ts | 1 + shared/models/accounts/index.ts | 2 + shared/models/activitypub/activity.ts | 18 ++- shared/models/index.ts | 1 + 29 files changed, 493 insertions(+), 208 deletions(-) create mode 100644 server/lib/activitypub/process-accept.ts create mode 100644 server/lib/activitypub/process-delete.ts create mode 100644 server/lib/activitypub/process-follow.ts create mode 100644 shared/models/accounts/account.model.ts create mode 100644 shared/models/accounts/follow.model.ts create mode 100644 shared/models/accounts/index.ts diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index 627acb5af..0a35bc362 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html @@ -15,4 +15,4 @@ - diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 28d08b3f4..5cfbc2f1d 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts @@ -16,12 +16,12 @@ activityPubClientRouter.get('/account/:name', executeIfActivityPub(asyncMiddleware(accountController)) ) -activityPubClientRouter.get('/account/:name/followers', +activityPubClientRouter.get('/account/:nameWithHost/followers', executeIfActivityPub(localAccountValidator), executeIfActivityPub(asyncMiddleware(accountFollowersController)) ) -activityPubClientRouter.get('/account/:name/following', +activityPubClientRouter.get('/account/:nameWithHost/following', executeIfActivityPub(localAccountValidator), executeIfActivityPub(asyncMiddleware(accountFollowingController)) ) @@ -46,7 +46,7 @@ async function accountFollowersController (req: express.Request, res: express.Re const page = req.params.page || 1 const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) - const result = await db.Account.listFollowerUrlsForApi(account.name, start, count) + const result = await db.Account.listFollowerUrlsForApi(account.id, start, count) const activityPubResult = activityPubCollectionPagination(req.url, page, result) return res.json(activityPubResult) @@ -58,7 +58,7 @@ async function accountFollowingController (req: express.Request, res: express.Re const page = req.params.page || 1 const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) - const result = await db.Account.listFollowingUrlsForApi(account.name, start, count) + const result = await db.Account.listFollowingUrlsForApi(account.id, start, count) const activityPubResult = activityPubCollectionPagination(req.url, page, result) return res.json(activityPubResult) diff --git a/server/controllers/activitypub/inbox.ts b/server/controllers/activitypub/inbox.ts index eee217650..eedb518b9 100644 --- a/server/controllers/activitypub/inbox.ts +++ b/server/controllers/activitypub/inbox.ts @@ -3,26 +3,41 @@ import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, Activity import { logger } from '../../helpers' import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' import { processCreateActivity, processFlagActivity, processUpdateActivity } from '../../lib' +import { processAcceptActivity } from '../../lib/activitypub/process-accept' import { processAddActivity } from '../../lib/activitypub/process-add' -import { asyncMiddleware, checkSignature, signatureValidator } from '../../middlewares' +import { processDeleteActivity } from '../../lib/activitypub/process-delete' +import { processFollowActivity } from '../../lib/activitypub/process-follow' +import { asyncMiddleware, checkSignature, localAccountValidator, signatureValidator } from '../../middlewares' import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' +import { AccountInstance } from '../../models/account/account-interface' -const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise } = { +const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccount?: AccountInstance) => Promise } = { Create: processCreateActivity, Add: processAddActivity, Update: processUpdateActivity, - Flag: processFlagActivity + Flag: processFlagActivity, + Delete: processDeleteActivity, + Follow: processFollowActivity, + Accept: processAcceptActivity } const inboxRouter = express.Router() -inboxRouter.post('/', +inboxRouter.post('/inbox', signatureValidator, asyncMiddleware(checkSignature), activityPubValidator, asyncMiddleware(inboxController) ) +inboxRouter.post('/:nameWithHost/inbox', + signatureValidator, + asyncMiddleware(checkSignature), + localAccountValidator, + activityPubValidator, + asyncMiddleware(inboxController) +) + // --------------------------------------------------------------------------- export { @@ -46,12 +61,12 @@ async function inboxController (req: express.Request, res: express.Response, nex // Only keep activities we are able to process activities = activities.filter(a => isActivityValid(a)) - await processActivities(activities) + await processActivities(activities, res.locals.account) res.status(204).end() } -async function processActivities (activities: Activity[]) { +async function processActivities (activities: Activity[], inboxAccount?: AccountInstance) { for (const activity of activities) { const activityProcessor = processActivity[activity.type] if (activityProcessor === undefined) { @@ -59,6 +74,6 @@ async function processActivities (activities: Activity[]) { continue } - await activityProcessor(activity) + await activityProcessor(activity, inboxAccount) } } diff --git a/server/controllers/activitypub/index.ts b/server/controllers/activitypub/index.ts index 2b0e2a489..6c7bafc6e 100644 --- a/server/controllers/activitypub/index.ts +++ b/server/controllers/activitypub/index.ts @@ -6,7 +6,7 @@ import { activityPubClientRouter } from './client' const remoteRouter = express.Router() -remoteRouter.use('/inbox', inboxRouter) +remoteRouter.use('/', inboxRouter) remoteRouter.use('/', activityPubClientRouter) remoteRouter.use('/*', badRequest) diff --git a/server/controllers/activitypub/videos.ts b/server/controllers/activitypub/videos.ts index a9b31bf75..98894379f 100644 --- a/server/controllers/activitypub/videos.ts +++ b/server/controllers/activitypub/videos.ts @@ -224,67 +224,6 @@ // logger.info('Remote video with uuid %s quick and dirty updated', videoUUID) // } // -// async function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { -// const options = { -// arguments: [ videoToRemoveData, fromPod ], -// errorMessage: 'Cannot remove the remote video channel with many retries.' -// } -// -// await retryTransactionWrapper(removeRemoteVideo, options) -// } -// -// async function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { -// logger.debug('Removing remote video "%s".', videoToRemoveData.uuid) -// -// await db.sequelize.transaction(async t => { -// // We need the instance because we have to remove some other stuffs (thumbnail etc) -// const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t) -// await videoInstance.destroy({ transaction: t }) -// }) -// -// logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid) -// } -// -// async function removeRemoteVideoAccountRetryWrapper (accountAttributesToRemove: RemoteVideoAccountRemoveData, fromPod: PodInstance) { -// const options = { -// arguments: [ accountAttributesToRemove, fromPod ], -// errorMessage: 'Cannot remove the remote video account with many retries.' -// } -// -// await retryTransactionWrapper(removeRemoteVideoAccount, options) -// } -// -// async function removeRemoteVideoAccount (accountAttributesToRemove: RemoteVideoAccountRemoveData, fromPod: PodInstance) { -// logger.debug('Removing remote video account "%s".', accountAttributesToRemove.uuid) -// -// await db.sequelize.transaction(async t => { -// const videoAccount = await db.Account.loadAccountByPodAndUUID(accountAttributesToRemove.uuid, fromPod.id, t) -// await videoAccount.destroy({ transaction: t }) -// }) -// -// logger.info('Remote video account with uuid %s removed.', accountAttributesToRemove.uuid) -// } -// -// async function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) { -// const options = { -// arguments: [ videoChannelAttributesToRemove, fromPod ], -// errorMessage: 'Cannot remove the remote video channel with many retries.' -// } -// -// await retryTransactionWrapper(removeRemoteVideoChannel, options) -// } -// -// async function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) { -// logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid) -// -// await db.sequelize.transaction(async t => { -// const videoChannel = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t) -// await videoChannel.destroy({ transaction: t }) -// }) -// -// logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid) -// } -// // async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { // const options = { // arguments: [ reportData, fromPod ], diff --git a/server/controllers/api/pods.ts b/server/controllers/api/pods.ts index 43df3f66f..aa07b17f6 100644 --- a/server/controllers/api/pods.ts +++ b/server/controllers/api/pods.ts @@ -1,16 +1,27 @@ import * as express from 'express' import { getFormattedObjects } from '../../helpers' +import { getApplicationAccount } from '../../helpers/utils' import { database as db } from '../../initializers/database' -import { asyncMiddleware, paginationValidator, podsSortValidator, setPagination, setPodsSort } from '../../middlewares' +import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares' +import { setFollowingSort } from '../../middlewares/sort' +import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort' const podsRouter = express.Router() -podsRouter.get('/', +podsRouter.get('/following', paginationValidator, - podsSortValidator, - setPodsSort, + followingSortValidator, + setFollowingSort, setPagination, - asyncMiddleware(listPods) + asyncMiddleware(listFollowing) +) + +podsRouter.get('/followers', + paginationValidator, + followersSortValidator, + setFollowersSort, + setPagination, + asyncMiddleware(listFollowers) ) // --------------------------------------------------------------------------- @@ -21,8 +32,16 @@ export { // --------------------------------------------------------------------------- -async function listPods (req: express.Request, res: express.Response, next: express.NextFunction) { - const resultList = await db.Pod.listForApi(req.query.start, req.query.count, req.query.sort) +async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) { + const applicationAccount = await getApplicationAccount() + const resultList = await db.Account.listFollowingForApi(applicationAccount.id, req.query.start, req.query.count, req.query.sort) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + +async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) { + const applicationAccount = await getApplicationAccount() + const resultList = await db.Account.listFollowersForApi(applicationAccount.id, req.query.start, req.query.count, req.query.sort) return res.json(getFormattedObjects(resultList.data, resultList.total)) } diff --git a/server/helpers/custom-validators/video-accounts.ts b/server/helpers/custom-validators/video-accounts.ts index 3f3e9edd1..31808ae1e 100644 --- a/server/helpers/custom-validators/video-accounts.ts +++ b/server/helpers/custom-validators/video-accounts.ts @@ -8,11 +8,18 @@ import { AccountInstance } from '../../models' import { logger } from '../logger' import { isUserUsernameValid } from './users' +import { isHostValid } from './pods' function isVideoAccountNameValid (value: string) { return isUserUsernameValid(value) } +function isAccountNameWithHostValid (value: string) { + const [ name, host ] = value.split('@') + + return isVideoAccountNameValid(name) && isHostValid(host) +} + function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) { let promise: Promise if (validator.isInt(id)) { @@ -41,5 +48,6 @@ function checkVideoAccountExists (id: string, res: express.Response, callback: ( export { checkVideoAccountExists, + isAccountNameWithHostValid, isVideoAccountNameValid } diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 0ebbf48a8..39957c90f 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts @@ -5,6 +5,7 @@ import { pseudoRandomBytesPromise } from './core-utils' import { CONFIG, database as db } from '../initializers' import { ResultList } from '../../shared' import { VideoResolution } from '../../shared/models/videos/video-resolution.enum' +import { AccountInstance } from '../models/account/account-interface' function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) { return res.type('json').status(400).end() @@ -78,6 +79,15 @@ function resetSequelizeInstance (instance: Sequelize.Instance, savedFields: }) } +let applicationAccount: AccountInstance +async function getApplicationAccount () { + if (applicationAccount === undefined) { + applicationAccount = await db.Account.loadApplication() + } + + return Promise.resolve(applicationAccount) +} + type SortType = { sortModel: any, sortValue: string } // --------------------------------------------------------------------------- @@ -89,5 +99,6 @@ export { isSignupAllowed, computeResolutionsToTranscode, resetSequelizeInstance, + getApplicationAccount, SortType } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 2d61094bd..5d0d39395 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -14,6 +14,7 @@ import { JobCategory } from '../../shared/models' import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum' +import { FollowState } from '../../shared/models/accounts/follow.model' // --------------------------------------------------------------------------- @@ -34,12 +35,13 @@ const SEARCHABLE_COLUMNS = { // Sortable columns per schema const SORTABLE_COLUMNS = { - PODS: [ 'id', 'host', 'score', 'createdAt' ], USERS: [ 'id', 'username', 'createdAt' ], VIDEO_ABUSES: [ 'id', 'createdAt' ], VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ], - BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ] + BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], + FOLLOWERS: [ 'createdAt' ], + FOLLOWING: [ 'createdAt' ] } const OAUTH_LIFETIME = { @@ -236,27 +238,10 @@ const PODS_SCORE = { BONUS: 10 } -// Time to wait between requests to the friends (10 min) -let REQUESTS_INTERVAL = 600000 - -// Number of requests in parallel we can make -const REQUESTS_IN_PARALLEL = 10 - -// To how many pods we send requests -const REQUESTS_LIMIT_PODS = 10 -// How many requests we send to a pod per interval -const REQUESTS_LIMIT_PER_POD = 5 - -const REQUESTS_VIDEO_QADU_LIMIT_PODS = 10 -// The QADU requests are not big -const REQUESTS_VIDEO_QADU_LIMIT_PER_POD = 50 - -const REQUESTS_VIDEO_EVENT_LIMIT_PODS = 10 -// The EVENTS requests are not big -const REQUESTS_VIDEO_EVENT_LIMIT_PER_POD = 50 - -// Number of requests to retry for replay requests module -const RETRY_REQUESTS = 5 +const FOLLOW_STATES: { [ id: string ]: FollowState } = { + PENDING: 'pending', + ACCEPTED: 'accepted' +} const REMOTE_SCHEME = { HTTP: 'https', @@ -333,7 +318,6 @@ const OPENGRAPH_AND_OEMBED_COMMENT = '' if (isTestInstance() === true) { CONSTRAINTS_FIELDS.VIDEOS.DURATION.max = 14 FRIEND_SCORE.BASE = 20 - REQUESTS_INTERVAL = 10000 JOBS_FETCHING_INTERVAL = 10000 REMOTE_SCHEME.HTTP = 'http' REMOTE_SCHEME.WS = 'ws' @@ -361,15 +345,7 @@ export { PODS_SCORE, PREVIEWS_SIZE, REMOTE_SCHEME, - REQUESTS_IN_PARALLEL, - REQUESTS_INTERVAL, - REQUESTS_LIMIT_PER_POD, - REQUESTS_LIMIT_PODS, - REQUESTS_VIDEO_EVENT_LIMIT_PER_POD, - REQUESTS_VIDEO_EVENT_LIMIT_PODS, - REQUESTS_VIDEO_QADU_LIMIT_PER_POD, - REQUESTS_VIDEO_QADU_LIMIT_PODS, - RETRY_REQUESTS, + FOLLOW_STATES, SEARCHABLE_COLUMNS, PRIVATE_RSA_KEY_SIZE, SORTABLE_COLUMNS, diff --git a/server/lib/activitypub/process-accept.ts b/server/lib/activitypub/process-accept.ts new file mode 100644 index 000000000..37e42bd3a --- /dev/null +++ b/server/lib/activitypub/process-accept.ts @@ -0,0 +1,27 @@ +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.') + + const targetAccount = await db.Account.loadByUrl(activity.actor) + + return processFollow(inboxAccount, targetAccount) +} + +// --------------------------------------------------------------------------- + +export { + processAcceptActivity +} + +// --------------------------------------------------------------------------- + +async function processFollow (account: AccountInstance, targetAccount: AccountInstance) { + const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id) + if (!follow) throw new Error('Cannot find associated follow.') + + follow.set('state', 'accepted') + return follow.save() +} diff --git a/server/lib/activitypub/process-delete.ts b/server/lib/activitypub/process-delete.ts new file mode 100644 index 000000000..377df432d --- /dev/null +++ b/server/lib/activitypub/process-delete.ts @@ -0,0 +1,105 @@ +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) + + if (account.url === activity.id) { + return processDeleteAccount(account) + } + + { + let videoObject = await db.Video.loadByUrl(activity.id) + if (videoObject !== undefined) { + return processDeleteVideo(account, videoObject) + } + } + + { + let videoChannelObject = await db.VideoChannel.loadByUrl(activity.id) + if (videoChannelObject !== undefined) { + return processDeleteVideoChannel(account, videoChannelObject) + } + } + + return undefined +} + +// --------------------------------------------------------------------------- + +export { + processDeleteActivity +} + +// --------------------------------------------------------------------------- + +async function processDeleteVideo (account: AccountInstance, videoToDelete: VideoInstance) { + const options = { + arguments: [ account, videoToDelete ], + errorMessage: 'Cannot remove the remote video with many retries.' + } + + await retryTransactionWrapper(deleteRemoteVideo, options) +} + +async function deleteRemoteVideo (account: AccountInstance, videoToDelete: VideoInstance) { + logger.debug('Removing remote video "%s".', videoToDelete.uuid) + + await db.sequelize.transaction(async t => { + if (videoToDelete.VideoChannel.Account.id !== account.id) { + throw new Error('Account ' + account.url + ' does not own video channel ' + videoToDelete.VideoChannel.url) + } + + await videoToDelete.destroy({ transaction: t }) + }) + + logger.info('Remote video with uuid %s removed.', videoToDelete.uuid) +} + +async function processDeleteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) { + const options = { + arguments: [ account, videoChannelToRemove ], + errorMessage: 'Cannot remove the remote video channel with many retries.' + } + + await retryTransactionWrapper(deleteRemoteVideoChannel, options) +} + +async function deleteRemoteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) { + logger.debug('Removing remote video channel "%s".', videoChannelToRemove.uuid) + + await db.sequelize.transaction(async t => { + if (videoChannelToRemove.Account.id !== account.id) { + throw new Error('Account ' + account.url + ' does not own video channel ' + videoChannelToRemove.url) + } + + await videoChannelToRemove.destroy({ transaction: t }) + }) + + logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.uuid) +} + +async function processDeleteAccount (accountToRemove: AccountInstance) { + const options = { + arguments: [ accountToRemove ], + errorMessage: 'Cannot remove the remote account with many retries.' + } + + await retryTransactionWrapper(deleteRemoteAccount, options) +} + +async function deleteRemoteAccount (accountToRemove: AccountInstance) { + logger.debug('Removing remote account "%s".', accountToRemove.uuid) + + await db.sequelize.transaction(async t => { + await accountToRemove.destroy({ transaction: t }) + }) + + logger.info('Remote account with uuid %s removed.', accountToRemove.uuid) +} diff --git a/server/lib/activitypub/process-follow.ts b/server/lib/activitypub/process-follow.ts new file mode 100644 index 000000000..a04fc7994 --- /dev/null +++ b/server/lib/activitypub/process-follow.ts @@ -0,0 +1,32 @@ +import { ActivityFollow } from '../../../shared/models/activitypub/activity' +import { getOrCreateAccount } from '../../helpers' +import { database as db } from '../../initializers' +import { AccountInstance } from '../../models/account/account-interface' + +async function processFollowActivity (activity: ActivityFollow) { + const activityObject = activity.object + const account = await getOrCreateAccount(activity.actor) + + return processFollow(account, activityObject) +} + +// --------------------------------------------------------------------------- + +export { + processFollowActivity +} + +// --------------------------------------------------------------------------- + +async function processFollow (account: AccountInstance, targetAccountURL: string) { + const targetAccount = await db.Account.loadByUrl(targetAccountURL) + + if (targetAccount === undefined) throw new Error('Unknown account') + if (targetAccount.isOwned() === false) throw new Error('This is not a local account.') + + return db.AccountFollow.create({ + accountId: account.id, + targetAccountId: targetAccount.id, + state: 'accepted' + }) +} diff --git a/server/lib/activitypub/send-request.ts b/server/lib/activitypub/send-request.ts index 91101f5ad..ce9a96f14 100644 --- a/server/lib/activitypub/send-request.ts +++ b/server/lib/activitypub/send-request.ts @@ -25,8 +25,7 @@ function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Sequeliz } function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) { - const videoChannelObject = videoChannel.toActivityPubObject() - const data = deleteActivityData(videoChannel.url, videoChannel.Account, videoChannelObject) + const data = deleteActivityData(videoChannel.url, videoChannel.Account) return broadcastToFollowers(data, videoChannel.Account, t) } @@ -46,12 +45,17 @@ function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) { } function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) { - const videoObject = video.toActivityPubObject() - const data = deleteActivityData(video.url, video.VideoChannel.Account, videoObject) + const data = deleteActivityData(video.url, video.VideoChannel.Account) return broadcastToFollowers(data, video.VideoChannel.Account, t) } +function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) { + const data = deleteActivityData(account.url, account) + + return broadcastToFollowers(data, account, t) +} + // --------------------------------------------------------------------------- export { @@ -60,13 +64,14 @@ export { sendDeleteVideoChannel, sendAddVideo, sendUpdateVideo, - sendDeleteVideo + sendDeleteVideo, + sendDeleteAccount } // --------------------------------------------------------------------------- async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t: Sequelize.Transaction) { - const result = await db.Account.listFollowerUrlsForApi(fromAccount.name, 0) + const result = await db.Account.listFollowerUrlsForApi(fromAccount.id, 0) const jobPayload = { uris: result.data, @@ -114,14 +119,11 @@ async function updateActivityData (url: string, byAccount: AccountInstance, obje return buildSignedActivity(byAccount, base) } -async function deleteActivityData (url: string, byAccount: AccountInstance, object: any) { - const to = await getPublicActivityTo(byAccount) +async function deleteActivityData (url: string, byAccount: AccountInstance) { const base = { - type: 'Update', + type: 'Delete', id: url, - actor: byAccount.url, - to, - object + actor: byAccount.url } return buildSignedActivity(byAccount, base) diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts index 91aa3e5b6..20ae341f0 100644 --- a/server/middlewares/sort.ts +++ b/server/middlewares/sort.ts @@ -34,6 +34,18 @@ function setVideosSort (req: express.Request, res: express.Response, next: expre return next() } +function setFollowersSort (req: express.Request, res: express.Response, next: express.NextFunction) { + if (!req.query.sort) req.query.sort = '-createdAt' + + return next() +} + +function setFollowingSort (req: express.Request, res: express.Response, next: express.NextFunction) { + if (!req.query.sort) req.query.sort = '-createdAt' + + return next() +} + function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) { let newSort: SortType = { sortModel: undefined, sortValue: undefined } @@ -63,5 +75,7 @@ export { setVideoAbusesSort, setVideoChannelsSort, setVideosSort, - setBlacklistSort + setBlacklistSort, + setFollowersSort, + setFollowingSort } diff --git a/server/middlewares/validators/account.ts b/server/middlewares/validators/account.ts index 5abe942d6..3ccf2ea21 100644 --- a/server/middlewares/validators/account.ts +++ b/server/middlewares/validators/account.ts @@ -1,21 +1,20 @@ -import { param } from 'express-validator/check' import * as express from 'express' - -import { database as db } from '../../initializers/database' -import { checkErrors } from './utils' +import { param } from 'express-validator/check' import { - logger, - isUserUsernameValid, - isUserPasswordValid, - isUserVideoQuotaValid, isUserDisplayNSFWValid, + isUserPasswordValid, isUserRoleValid, - isAccountNameValid + isUserUsernameValid, + isUserVideoQuotaValid, + logger } from '../../helpers' +import { isAccountNameWithHostValid } from '../../helpers/custom-validators/video-accounts' +import { database as db } from '../../initializers/database' import { AccountInstance } from '../../models' +import { checkErrors } from './utils' const localAccountValidator = [ - param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'), + param('nameWithHost').custom(isAccountNameWithHostValid).withMessage('Should have a valid account with domain name (myuser@domain.tld)'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking localAccountValidator parameters', { parameters: req.params }) @@ -34,8 +33,10 @@ export { // --------------------------------------------------------------------------- -function checkLocalAccountExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) { - db.Account.loadLocalAccountByName(name) +function checkLocalAccountExists (nameWithHost: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) { + const [ name, host ] = nameWithHost.split('@') + + db.Account.loadLocalAccountByNameAndPod(name, host) .then(account => { if (!account) { return res.status(404) diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index d23a95537..6fea41bb8 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts @@ -6,29 +6,32 @@ import { logger } from '../../helpers' import { SORTABLE_COLUMNS } from '../../initializers' // Initialize constants here for better performances -const SORTABLE_PODS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PODS) const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) +const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS) +const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING) -const podsSortValidator = checkSort(SORTABLE_PODS_COLUMNS) const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) +const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS) +const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS) // --------------------------------------------------------------------------- export { - podsSortValidator, usersSortValidator, videoAbusesSortValidator, videoChannelsSortValidator, videosSortValidator, - blacklistSortValidator + blacklistSortValidator, + followersSortValidator, + followingSortValidator } // --------------------------------------------------------------------------- diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts index 3be383649..efdff915e 100644 --- a/server/models/account/account-follow-interface.ts +++ b/server/models/account/account-follow-interface.ts @@ -1,17 +1,19 @@ import * as Sequelize from 'sequelize' -import * as Promise from 'bluebird' - -import { VideoRateType } from '../../../shared/models/videos/video-rate.type' +import * as Bluebird from 'bluebird' +import { FollowState } from '../../../shared/models/accounts/follow.model' export namespace AccountFollowMethods { + export type LoadByAccountAndTarget = (accountId: number, targetAccountId: number) => Bluebird } export interface AccountFollowClass { + loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget } export interface AccountFollowAttributes { accountId: number targetAccountId: number + state: FollowState } export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance { diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts index 9bf03b253..e6abc893a 100644 --- a/server/models/account/account-follow.ts +++ b/server/models/account/account-follow.ts @@ -1,18 +1,21 @@ +import { values } from 'lodash' import * as Sequelize from 'sequelize' import { addMethodsToModel } from '../utils' -import { - AccountFollowInstance, - AccountFollowAttributes, - - AccountFollowMethods -} from './account-follow-interface' +import { AccountFollowAttributes, AccountFollowInstance, AccountFollowMethods } from './account-follow-interface' +import { FOLLOW_STATES } from '../../initializers/constants' let AccountFollow: Sequelize.Model +let loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { AccountFollow = sequelize.define('AccountFollow', - { }, + { + state: { + type: DataTypes.ENUM(values(FOLLOW_STATES)), + allowNull: false + } + }, { indexes: [ { @@ -43,6 +46,7 @@ function associate (models) { name: 'accountId', allowNull: false }, + as: 'followers', onDelete: 'CASCADE' }) @@ -51,6 +55,18 @@ function associate (models) { name: 'targetAccountId', allowNull: false }, + as: 'following', onDelete: 'CASCADE' }) } + +loadByAccountAndTarget = function (accountId: number, targetAccountId: number) { + const query = { + where: { + accountId, + targetAccountId + } + } + + return AccountFollow.findOne(query) +} diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts index a662eb992..d49dfbe17 100644 --- a/server/models/account/account-interface.ts +++ b/server/models/account/account-interface.ts @@ -1,22 +1,26 @@ -import * as Sequelize from 'sequelize' import * as Bluebird from 'bluebird' - +import * as Sequelize from 'sequelize' +import { Account as FormattedAccount, ActivityPubActor } from '../../../shared' +import { ResultList } from '../../../shared/models/result-list.model' import { PodInstance } from '../pod/pod-interface' import { VideoChannelInstance } from '../video/video-channel-interface' -import { ActivityPubActor } from '../../../shared' -import { ResultList } from '../../../shared/models/result-list.model' export namespace AccountMethods { + export type LoadApplication = () => Bluebird + export type Load = (id: number) => Bluebird export type LoadByUUID = (uuid: string) => Bluebird export type LoadByUrl = (url: string) => Bluebird export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird - export type LoadLocalAccountByName = (name: string) => Bluebird + export type LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird export type ListOwned = () => Bluebird - export type ListFollowerUrlsForApi = (name: string, start: number, count?: number) => Promise< ResultList > - export type ListFollowingUrlsForApi = (name: string, start: number, count?: number) => Promise< ResultList > + export type ListFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList > + export type ListFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList > + export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList > + export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList > export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor + export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount export type IsOwned = (this: AccountInstance) => boolean export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird export type GetFollowingUrl = (this: AccountInstance) => string @@ -25,14 +29,17 @@ export namespace AccountMethods { } export interface AccountClass { + loadApplication: AccountMethods.LoadApplication loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID load: AccountMethods.Load loadByUUID: AccountMethods.LoadByUUID loadByUrl: AccountMethods.LoadByUrl - loadLocalAccountByName: AccountMethods.LoadLocalAccountByName + loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod listOwned: AccountMethods.ListOwned listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi + listFollowingForApi: AccountMethods.ListFollowingForApi + listFollowersForApi: AccountMethods.ListFollowersForApi } export interface AccountAttributes { @@ -58,6 +65,7 @@ export interface AccountAttributes { export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance { isOwned: AccountMethods.IsOwned toActivityPubObject: AccountMethods.ToActivityPubObject + toFormattedJSON: AccountMethods.ToFormattedJSON getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls getFollowingUrl: AccountMethods.GetFollowingUrl getFollowersUrl: AccountMethods.GetFollowersUrl diff --git a/server/models/account/account.ts b/server/models/account/account.ts index a79e13880..daf8f4703 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -15,25 +15,31 @@ import { activityPubContextify } from '../../helpers' -import { addMethodsToModel } from '../utils' +import { addMethodsToModel, getSort } from '../utils' import { AccountInstance, AccountAttributes, AccountMethods } from './account-interface' +import LoadApplication = AccountMethods.LoadApplication +import { sendDeleteAccount } from '../../lib/activitypub/send-request' let Account: Sequelize.Model let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID let load: AccountMethods.Load +let loadApplication: AccountMethods.LoadApplication let loadByUUID: AccountMethods.LoadByUUID let loadByUrl: AccountMethods.LoadByUrl -let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName +let loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod let listOwned: AccountMethods.ListOwned let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi +let listFollowingForApi: AccountMethods.ListFollowingForApi +let listFollowersForApi: AccountMethods.ListFollowersForApi let isOwned: AccountMethods.IsOwned let toActivityPubObject: AccountMethods.ToActivityPubObject +let toFormattedJSON: AccountMethods.ToFormattedJSON let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls let getFollowingUrl: AccountMethods.GetFollowingUrl let getFollowersUrl: AccountMethods.GetFollowersUrl @@ -189,16 +195,20 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes const classMethods = [ associate, loadAccountByPodAndUUID, + loadApplication, load, loadByUUID, - loadLocalAccountByName, + loadLocalAccountByNameAndPod, listOwned, listFollowerUrlsForApi, - listFollowingUrlsForApi + listFollowingUrlsForApi, + listFollowingForApi, + listFollowersForApi ] const instanceMethods = [ isOwned, toActivityPubObject, + toFormattedJSON, getFollowerSharedInboxUrls, getFollowingUrl, getFollowersUrl, @@ -250,6 +260,7 @@ function associate (models) { name: 'accountId', allowNull: false }, + as: 'following', onDelete: 'cascade' }) @@ -258,23 +269,29 @@ function associate (models) { name: 'targetAccountId', allowNull: false }, + as: 'followers', onDelete: 'cascade' }) } function afterDestroy (account: AccountInstance) { if (account.isOwned()) { - const removeVideoAccountToFriendsParams = { - uuid: account.uuid - } - - // FIXME: remove account in followers - // return removeVideoAccountToFriends(removeVideoAccountToFriendsParams) + return sendDeleteAccount(account, undefined) } return undefined } +toFormattedJSON = function (this: AccountInstance) { + const json = { + id: this.id, + host: this.Pod.host, + name: this.name + } + + return json +} + toActivityPubObject = function (this: AccountInstance) { const type = this.podId ? 'Application' as 'Application' : 'Person' as 'Person' @@ -347,12 +364,85 @@ listOwned = function () { return Account.findAll(query) } -listFollowerUrlsForApi = function (name: string, start: number, count?: number) { - return createListFollowForApiQuery('followers', name, start, count) +listFollowerUrlsForApi = function (id: number, start: number, count?: number) { + return createListFollowForApiQuery('followers', id, start, count) +} + +listFollowingUrlsForApi = function (id: number, start: number, count?: number) { + return createListFollowForApiQuery('following', id, start, count) +} + +listFollowingForApi = function (id: number, start: number, count: number, sort: string) { + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ + { + model: Account['sequelize'].models.AccountFollow, + required: true, + as: 'following', + include: [ + { + model: Account['sequelize'].models.Account, + as: 'following', + required: true, + include: [ Account['sequelize'].models.Pod ] + } + ] + } + ] + } + + return Account.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) +} + +listFollowersForApi = function (id: number, start: number, count: number, sort: string) { + const query = { + distinct: true, + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ + { + model: Account['sequelize'].models.AccountFollow, + required: true, + as: 'followers', + include: [ + { + model: Account['sequelize'].models.Account, + as: 'followers', + required: true, + include: [ Account['sequelize'].models.Pod ] + } + ] + } + ] + } + + return Account.findAndCountAll(query).then(({ rows, count }) => { + return { + data: rows, + total: count + } + }) } -listFollowingUrlsForApi = function (name: string, start: number, count?: number) { - return createListFollowForApiQuery('following', name, start, count) +loadApplication = function () { + return Account.findOne({ + include: [ + { + model: Account['sequelize'].model.Application, + required: true + } + ] + }) } load = function (id: number) { @@ -369,14 +459,22 @@ loadByUUID = function (uuid: string) { return Account.findOne(query) } -loadLocalAccountByName = function (name: string) { +loadLocalAccountByNameAndPod = function (name: string, host: string) { const query: Sequelize.FindOptions = { where: { name, userId: { [Sequelize.Op.ne]: null } - } + }, + include: [ + { + model: Account['sequelize'].models.Pod, + where: { + host + } + } + ] } return Account.findOne(query) @@ -406,7 +504,7 @@ loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Se // ------------------------------ UTILS ------------------------------ -async function createListFollowForApiQuery (type: 'followers' | 'following', name: string, start: number, count?: number) { +async function createListFollowForApiQuery (type: 'followers' | 'following', id: number, start: number, count?: number) { let firstJoin: string let secondJoin: string @@ -424,14 +522,14 @@ async function createListFollowForApiQuery (type: 'followers' | 'following', nam for (const selection of selections) { let query = 'SELECT ' + selection + ' FROM "Account" ' + 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' + - 'INNER JOIN "Account" AS "Followers" ON "Followers"."id" = "AccountFollower"."' + secondJoin + '" ' + - 'WHERE "Account"."name" = \'$name\' ' + + 'INNER JOIN "Account" AS "Follows" ON "Followers"."id" = "Follows"."' + secondJoin + '" ' + + 'WHERE "Account"."id" = $id ' + 'LIMIT ' + start if (count !== undefined) query += ', ' + count const options = { - bind: { name }, + bind: { id }, type: Sequelize.QueryTypes.SELECT } tasks.push(Account['sequelize'].query(query, options)) diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 183ff3436..919ec916d 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -9,6 +9,7 @@ import { VideoChannelMethods } from './video-channel-interface' +import { sendDeleteVideoChannel } from '../../lib/activitypub/send-request' let VideoChannel: Sequelize.Model let toFormattedJSON: VideoChannelMethods.ToFormattedJSON @@ -176,11 +177,7 @@ function associate (models) { function afterDestroy (videoChannel: VideoChannelInstance) { if (videoChannel.isOwned()) { - const removeVideoChannelToFriendsParams = { - uuid: videoChannel.uuid - } - - // FIXME: send remove event to followers + return sendDeleteVideoChannel(videoChannel, undefined) } return undefined diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index a0ac43e1e..7243756d2 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts @@ -1,19 +1,12 @@ -import * as Sequelize from 'sequelize' import * as Bluebird from 'bluebird' +import * as Sequelize from 'sequelize' +import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' +import { ResultList } from '../../../shared/models/result-list.model' +import { Video as FormattedVideo, VideoDetails as FormattedDetailsVideo } from '../../../shared/models/videos/video.model' import { TagAttributes, TagInstance } from './tag-interface' -import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' - -// Don't use barrel, import just what we need -import { - Video as FormattedVideo, - VideoDetails as FormattedDetailsVideo -} from '../../../shared/models/videos/video.model' -import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model' -import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model' -import { ResultList } from '../../../shared/models/result-list.model' import { VideoChannelInstance } from './video-channel-interface' -import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' +import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' export namespace VideoMethods { export type GetThumbnailName = (this: VideoInstance) => string diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 10ae5097c..ca71da375 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -45,6 +45,7 @@ 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/activitypub/send-request' const Buffer = safeBuffer.Buffer @@ -363,13 +364,9 @@ function afterDestroy (video: VideoInstance) { ) if (video.isOwned()) { - const removeVideoToFriendsParams = { - uuid: video.uuid - } - tasks.push( - video.removePreview() - // FIXME: remove video for followers + video.removePreview(), + sendDeleteVideo(video, undefined) ) // Remove physical files and torrents diff --git a/server/tests/real-world/real-world.ts b/server/tests/real-world/real-world.ts index da5696f8c..c79ad38ff 100644 --- a/server/tests/real-world/real-world.ts +++ b/server/tests/real-world/real-world.ts @@ -3,7 +3,6 @@ import * as program from 'commander' // /!\ Before imports /!\ process.env.NODE_ENV = 'test' -import { REQUESTS_INTERVAL } from '../../initializers/constants' import { Video, VideoRateType, VideoFile } from '../../../shared' import { ServerInfo as DefaultServerInfo, @@ -137,7 +136,7 @@ async function start () { initializeRequestsPerServer(servers) checking = false clearInterval(waitingInterval) - }, REQUESTS_INTERVAL) + }, 10000) }, integrityInterval) } diff --git a/shared/models/accounts/account.model.ts b/shared/models/accounts/account.model.ts new file mode 100644 index 000000000..338426dc7 --- /dev/null +++ b/shared/models/accounts/account.model.ts @@ -0,0 +1,5 @@ +export interface Account { + id: number + name: string + host: string +} diff --git a/shared/models/accounts/follow.model.ts b/shared/models/accounts/follow.model.ts new file mode 100644 index 000000000..80cfe07e4 --- /dev/null +++ b/shared/models/accounts/follow.model.ts @@ -0,0 +1 @@ +export type FollowState = 'pending' | 'accepted' diff --git a/shared/models/accounts/index.ts b/shared/models/accounts/index.ts new file mode 100644 index 000000000..8fe437b81 --- /dev/null +++ b/shared/models/accounts/index.ts @@ -0,0 +1,2 @@ +export * from './account.model' +export * from './follow.model' diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index a2494da25..254daf118 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts @@ -4,10 +4,11 @@ import { } from './objects' import { ActivityPubSignature } from './activitypub-signature' -export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | ActivityFlag +export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | ActivityFlag | + ActivityDelete | ActivityFollow | ActivityAccept // Flag -> report abuse -export type ActivityType = 'Create' | 'Add' | 'Update' | 'Flag' +export type ActivityType = 'Create' | 'Add' | 'Update' | 'Flag' | 'Delete' | 'Follow' | 'Accept' export interface BaseActivity { '@context'?: any[] @@ -37,3 +38,16 @@ export interface ActivityFlag extends BaseActivity { type: 'Flag' object: string } + +export interface ActivityDelete extends BaseActivity { + type: 'Delete' +} + +export interface ActivityFollow extends BaseActivity { + type: 'Follow' + object: string +} + +export interface ActivityAccept extends BaseActivity { + type: 'Accept' +} diff --git a/shared/models/index.ts b/shared/models/index.ts index 0ccb84d24..28decac03 100644 --- a/shared/models/index.ts +++ b/shared/models/index.ts @@ -1,3 +1,4 @@ +export * from './accounts' export * from './activitypub' export * from './pods' export * from './users' -- 2.41.0