Browse Source

Handle follow/accept

Chocobozzz 6 years ago
parent
commit
7a7724e66e

+ 1 - 1
client/src/standalone/videos/embed.html

@@ -15,4 +15,4 @@
     </video>
 
   </body>
-</html
+</html>

+ 4 - 4
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)

+ 22 - 7
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<any> } = {
+const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccount?: AccountInstance) => Promise<any> } = {
   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)
   }
 }

+ 1 - 1
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)
 

+ 0 - 61
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 ],

+ 26 - 7
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))
 }

+ 8 - 0
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<AccountInstance>
   if (validator.isInt(id)) {
@@ -41,5 +48,6 @@ function checkVideoAccountExists (id: string, res: express.Response, callback: (
 
 export {
   checkVideoAccountExists,
+  isAccountNameWithHostValid,
   isVideoAccountNameValid
 }

+ 11 - 0
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<any>, 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
 }

+ 9 - 33
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 = '<!-- open graph and oembed tags -->'
 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,

+ 27 - 0
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()
+}

+ 105 - 0
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)
+}

+ 32 - 0
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'
+  })
+}

+ 14 - 12
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)

+ 15 - 1
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
 }

+ 13 - 12
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)

+ 7 - 4
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
 }
 
 // ---------------------------------------------------------------------------

+ 5 - 3
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<AccountFollowInstance>
 }
 
 export interface AccountFollowClass {
+  loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget
 }
 
 export interface AccountFollowAttributes {
   accountId: number
   targetAccountId: number
+  state: FollowState
 }
 
 export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> {

+ 23 - 7
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<AccountFollowInstance, AccountFollowAttributes>
+let loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget
 
 export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
   AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('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)
+}

+ 16 - 8
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<AccountInstance>
+
   export type Load = (id: number) => Bluebird<AccountInstance>
   export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
   export type LoadByUrl = (url: string) => Bluebird<AccountInstance>
   export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance>
-  export type LoadLocalAccountByName = (name: string) => Bluebird<AccountInstance>
+  export type LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird<AccountInstance>
   export type ListOwned = () => Bluebird<AccountInstance[]>
-  export type ListFollowerUrlsForApi = (name: string, start: number, count?: number) => Promise< ResultList<string> >
-  export type ListFollowingUrlsForApi = (name: string, start: number, count?: number) => Promise< ResultList<string> >
+  export type ListFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
+  export type ListFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
+  export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> >
+  export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> >
 
   export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor
+  export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount
   export type IsOwned = (this: AccountInstance) => boolean
   export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird<string[]>
   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<AccountAttributes> {
   isOwned: AccountMethods.IsOwned
   toActivityPubObject: AccountMethods.ToActivityPubObject
+  toFormattedJSON: AccountMethods.ToFormattedJSON
   getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
   getFollowingUrl: AccountMethods.GetFollowingUrl
   getFollowersUrl: AccountMethods.GetFollowersUrl

+ 118 - 20
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<AccountInstance, AccountAttributes>
 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<AccountAttributes> = {
     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))

+ 2 - 5
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<VideoChannelInstance, VideoChannelAttributes>
 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

+ 5 - 12
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

+ 3 - 6
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

+ 1 - 2
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)
 }
 

+ 5 - 0
shared/models/accounts/account.model.ts

@@ -0,0 +1,5 @@
+export interface Account {
+  id: number
+  name: string
+  host: string
+}

+ 1 - 0
shared/models/accounts/follow.model.ts

@@ -0,0 +1 @@
+export type FollowState = 'pending' | 'accepted'

+ 2 - 0
shared/models/accounts/index.ts

@@ -0,0 +1,2 @@
+export * from './account.model'
+export * from './follow.model'

+ 16 - 2
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'
+}

+ 1 - 0
shared/models/index.ts

@@ -1,3 +1,4 @@
+export * from './accounts'
 export * from './activitypub'
 export * from './pods'
 export * from './users'