aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-11-13 17:39:41 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-11-27 19:40:51 +0100
commit7a7724e66e4533523083e7336cd0d0c747c4a33b (patch)
tree805299eb9c6829158cd17e5a823a84a3a54d8209
parent571389d43b8fc8aaf27e77c06f19b320b08dbbc9 (diff)
downloadPeerTube-7a7724e66e4533523083e7336cd0d0c747c4a33b.tar.gz
PeerTube-7a7724e66e4533523083e7336cd0d0c747c4a33b.tar.zst
PeerTube-7a7724e66e4533523083e7336cd0d0c747c4a33b.zip
Handle follow/accept
-rw-r--r--client/src/standalone/videos/embed.html2
-rw-r--r--server/controllers/activitypub/client.ts8
-rw-r--r--server/controllers/activitypub/inbox.ts29
-rw-r--r--server/controllers/activitypub/index.ts2
-rw-r--r--server/controllers/activitypub/videos.ts61
-rw-r--r--server/controllers/api/pods.ts33
-rw-r--r--server/helpers/custom-validators/video-accounts.ts8
-rw-r--r--server/helpers/utils.ts11
-rw-r--r--server/initializers/constants.ts42
-rw-r--r--server/lib/activitypub/process-accept.ts27
-rw-r--r--server/lib/activitypub/process-delete.ts105
-rw-r--r--server/lib/activitypub/process-follow.ts32
-rw-r--r--server/lib/activitypub/send-request.ts26
-rw-r--r--server/middlewares/sort.ts16
-rw-r--r--server/middlewares/validators/account.ts25
-rw-r--r--server/middlewares/validators/sort.ts11
-rw-r--r--server/models/account/account-follow-interface.ts8
-rw-r--r--server/models/account/account-follow.ts30
-rw-r--r--server/models/account/account-interface.ts24
-rw-r--r--server/models/account/account.ts138
-rw-r--r--server/models/video/video-channel.ts7
-rw-r--r--server/models/video/video-interface.ts17
-rw-r--r--server/models/video/video.ts9
-rw-r--r--server/tests/real-world/real-world.ts3
-rw-r--r--shared/models/accounts/account.model.ts5
-rw-r--r--shared/models/accounts/follow.model.ts1
-rw-r--r--shared/models/accounts/index.ts2
-rw-r--r--shared/models/activitypub/activity.ts18
-rw-r--r--shared/models/index.ts1
29 files changed, 493 insertions, 208 deletions
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 @@
15 </video> 15 </video>
16 16
17 </body> 17 </body>
18</html 18</html>
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',
16 executeIfActivityPub(asyncMiddleware(accountController)) 16 executeIfActivityPub(asyncMiddleware(accountController))
17) 17)
18 18
19activityPubClientRouter.get('/account/:name/followers', 19activityPubClientRouter.get('/account/:nameWithHost/followers',
20 executeIfActivityPub(localAccountValidator), 20 executeIfActivityPub(localAccountValidator),
21 executeIfActivityPub(asyncMiddleware(accountFollowersController)) 21 executeIfActivityPub(asyncMiddleware(accountFollowersController))
22) 22)
23 23
24activityPubClientRouter.get('/account/:name/following', 24activityPubClientRouter.get('/account/:nameWithHost/following',
25 executeIfActivityPub(localAccountValidator), 25 executeIfActivityPub(localAccountValidator),
26 executeIfActivityPub(asyncMiddleware(accountFollowingController)) 26 executeIfActivityPub(asyncMiddleware(accountFollowingController))
27) 27)
@@ -46,7 +46,7 @@ async function accountFollowersController (req: express.Request, res: express.Re
46 const page = req.params.page || 1 46 const page = req.params.page || 1
47 const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) 47 const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
48 48
49 const result = await db.Account.listFollowerUrlsForApi(account.name, start, count) 49 const result = await db.Account.listFollowerUrlsForApi(account.id, start, count)
50 const activityPubResult = activityPubCollectionPagination(req.url, page, result) 50 const activityPubResult = activityPubCollectionPagination(req.url, page, result)
51 51
52 return res.json(activityPubResult) 52 return res.json(activityPubResult)
@@ -58,7 +58,7 @@ async function accountFollowingController (req: express.Request, res: express.Re
58 const page = req.params.page || 1 58 const page = req.params.page || 1
59 const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE) 59 const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
60 60
61 const result = await db.Account.listFollowingUrlsForApi(account.name, start, count) 61 const result = await db.Account.listFollowingUrlsForApi(account.id, start, count)
62 const activityPubResult = activityPubCollectionPagination(req.url, page, result) 62 const activityPubResult = activityPubCollectionPagination(req.url, page, result)
63 63
64 return res.json(activityPubResult) 64 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
3import { logger } from '../../helpers' 3import { logger } from '../../helpers'
4import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' 4import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
5import { processCreateActivity, processFlagActivity, processUpdateActivity } from '../../lib' 5import { processCreateActivity, processFlagActivity, processUpdateActivity } from '../../lib'
6import { processAcceptActivity } from '../../lib/activitypub/process-accept'
6import { processAddActivity } from '../../lib/activitypub/process-add' 7import { processAddActivity } from '../../lib/activitypub/process-add'
7import { asyncMiddleware, checkSignature, signatureValidator } from '../../middlewares' 8import { processDeleteActivity } from '../../lib/activitypub/process-delete'
9import { processFollowActivity } from '../../lib/activitypub/process-follow'
10import { asyncMiddleware, checkSignature, localAccountValidator, signatureValidator } from '../../middlewares'
8import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' 11import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
12import { AccountInstance } from '../../models/account/account-interface'
9 13
10const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = { 14const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccount?: AccountInstance) => Promise<any> } = {
11 Create: processCreateActivity, 15 Create: processCreateActivity,
12 Add: processAddActivity, 16 Add: processAddActivity,
13 Update: processUpdateActivity, 17 Update: processUpdateActivity,
14 Flag: processFlagActivity 18 Flag: processFlagActivity,
19 Delete: processDeleteActivity,
20 Follow: processFollowActivity,
21 Accept: processAcceptActivity
15} 22}
16 23
17const inboxRouter = express.Router() 24const inboxRouter = express.Router()
18 25
19inboxRouter.post('/', 26inboxRouter.post('/inbox',
20 signatureValidator, 27 signatureValidator,
21 asyncMiddleware(checkSignature), 28 asyncMiddleware(checkSignature),
22 activityPubValidator, 29 activityPubValidator,
23 asyncMiddleware(inboxController) 30 asyncMiddleware(inboxController)
24) 31)
25 32
33inboxRouter.post('/:nameWithHost/inbox',
34 signatureValidator,
35 asyncMiddleware(checkSignature),
36 localAccountValidator,
37 activityPubValidator,
38 asyncMiddleware(inboxController)
39)
40
26// --------------------------------------------------------------------------- 41// ---------------------------------------------------------------------------
27 42
28export { 43export {
@@ -46,12 +61,12 @@ async function inboxController (req: express.Request, res: express.Response, nex
46 // Only keep activities we are able to process 61 // Only keep activities we are able to process
47 activities = activities.filter(a => isActivityValid(a)) 62 activities = activities.filter(a => isActivityValid(a))
48 63
49 await processActivities(activities) 64 await processActivities(activities, res.locals.account)
50 65
51 res.status(204).end() 66 res.status(204).end()
52} 67}
53 68
54async function processActivities (activities: Activity[]) { 69async function processActivities (activities: Activity[], inboxAccount?: AccountInstance) {
55 for (const activity of activities) { 70 for (const activity of activities) {
56 const activityProcessor = processActivity[activity.type] 71 const activityProcessor = processActivity[activity.type]
57 if (activityProcessor === undefined) { 72 if (activityProcessor === undefined) {
@@ -59,6 +74,6 @@ async function processActivities (activities: Activity[]) {
59 continue 74 continue
60 } 75 }
61 76
62 await activityProcessor(activity) 77 await activityProcessor(activity, inboxAccount)
63 } 78 }
64} 79}
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'
6 6
7const remoteRouter = express.Router() 7const remoteRouter = express.Router()
8 8
9remoteRouter.use('/inbox', inboxRouter) 9remoteRouter.use('/', inboxRouter)
10remoteRouter.use('/', activityPubClientRouter) 10remoteRouter.use('/', activityPubClientRouter)
11remoteRouter.use('/*', badRequest) 11remoteRouter.use('/*', badRequest)
12 12
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 @@
224// logger.info('Remote video with uuid %s quick and dirty updated', videoUUID) 224// logger.info('Remote video with uuid %s quick and dirty updated', videoUUID)
225// } 225// }
226// 226//
227// async function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
228// const options = {
229// arguments: [ videoToRemoveData, fromPod ],
230// errorMessage: 'Cannot remove the remote video channel with many retries.'
231// }
232//
233// await retryTransactionWrapper(removeRemoteVideo, options)
234// }
235//
236// async function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
237// logger.debug('Removing remote video "%s".', videoToRemoveData.uuid)
238//
239// await db.sequelize.transaction(async t => {
240// // We need the instance because we have to remove some other stuffs (thumbnail etc)
241// const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t)
242// await videoInstance.destroy({ transaction: t })
243// })
244//
245// logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid)
246// }
247//
248// async function removeRemoteVideoAccountRetryWrapper (accountAttributesToRemove: RemoteVideoAccountRemoveData, fromPod: PodInstance) {
249// const options = {
250// arguments: [ accountAttributesToRemove, fromPod ],
251// errorMessage: 'Cannot remove the remote video account with many retries.'
252// }
253//
254// await retryTransactionWrapper(removeRemoteVideoAccount, options)
255// }
256//
257// async function removeRemoteVideoAccount (accountAttributesToRemove: RemoteVideoAccountRemoveData, fromPod: PodInstance) {
258// logger.debug('Removing remote video account "%s".', accountAttributesToRemove.uuid)
259//
260// await db.sequelize.transaction(async t => {
261// const videoAccount = await db.Account.loadAccountByPodAndUUID(accountAttributesToRemove.uuid, fromPod.id, t)
262// await videoAccount.destroy({ transaction: t })
263// })
264//
265// logger.info('Remote video account with uuid %s removed.', accountAttributesToRemove.uuid)
266// }
267//
268// async function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
269// const options = {
270// arguments: [ videoChannelAttributesToRemove, fromPod ],
271// errorMessage: 'Cannot remove the remote video channel with many retries.'
272// }
273//
274// await retryTransactionWrapper(removeRemoteVideoChannel, options)
275// }
276//
277// async function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
278// logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid)
279//
280// await db.sequelize.transaction(async t => {
281// const videoChannel = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t)
282// await videoChannel.destroy({ transaction: t })
283// })
284//
285// logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid)
286// }
287//
288// async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { 227// async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
289// const options = { 228// const options = {
290// arguments: [ reportData, fromPod ], 229// 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 @@
1import * as express from 'express' 1import * as express from 'express'
2import { getFormattedObjects } from '../../helpers' 2import { getFormattedObjects } from '../../helpers'
3import { getApplicationAccount } from '../../helpers/utils'
3import { database as db } from '../../initializers/database' 4import { database as db } from '../../initializers/database'
4import { asyncMiddleware, paginationValidator, podsSortValidator, setPagination, setPodsSort } from '../../middlewares' 5import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares'
6import { setFollowingSort } from '../../middlewares/sort'
7import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort'
5 8
6const podsRouter = express.Router() 9const podsRouter = express.Router()
7 10
8podsRouter.get('/', 11podsRouter.get('/following',
9 paginationValidator, 12 paginationValidator,
10 podsSortValidator, 13 followingSortValidator,
11 setPodsSort, 14 setFollowingSort,
12 setPagination, 15 setPagination,
13 asyncMiddleware(listPods) 16 asyncMiddleware(listFollowing)
17)
18
19podsRouter.get('/followers',
20 paginationValidator,
21 followersSortValidator,
22 setFollowersSort,
23 setPagination,
24 asyncMiddleware(listFollowers)
14) 25)
15 26
16// --------------------------------------------------------------------------- 27// ---------------------------------------------------------------------------
@@ -21,8 +32,16 @@ export {
21 32
22// --------------------------------------------------------------------------- 33// ---------------------------------------------------------------------------
23 34
24async function listPods (req: express.Request, res: express.Response, next: express.NextFunction) { 35async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) {
25 const resultList = await db.Pod.listForApi(req.query.start, req.query.count, req.query.sort) 36 const applicationAccount = await getApplicationAccount()
37 const resultList = await db.Account.listFollowingForApi(applicationAccount.id, req.query.start, req.query.count, req.query.sort)
38
39 return res.json(getFormattedObjects(resultList.data, resultList.total))
40}
41
42async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) {
43 const applicationAccount = await getApplicationAccount()
44 const resultList = await db.Account.listFollowersForApi(applicationAccount.id, req.query.start, req.query.count, req.query.sort)
26 45
27 return res.json(getFormattedObjects(resultList.data, resultList.total)) 46 return res.json(getFormattedObjects(resultList.data, resultList.total))
28} 47}
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'
8import { logger } from '../logger' 8import { logger } from '../logger'
9 9
10import { isUserUsernameValid } from './users' 10import { isUserUsernameValid } from './users'
11import { isHostValid } from './pods'
11 12
12function isVideoAccountNameValid (value: string) { 13function isVideoAccountNameValid (value: string) {
13 return isUserUsernameValid(value) 14 return isUserUsernameValid(value)
14} 15}
15 16
17function isAccountNameWithHostValid (value: string) {
18 const [ name, host ] = value.split('@')
19
20 return isVideoAccountNameValid(name) && isHostValid(host)
21}
22
16function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) { 23function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) {
17 let promise: Promise<AccountInstance> 24 let promise: Promise<AccountInstance>
18 if (validator.isInt(id)) { 25 if (validator.isInt(id)) {
@@ -41,5 +48,6 @@ function checkVideoAccountExists (id: string, res: express.Response, callback: (
41 48
42export { 49export {
43 checkVideoAccountExists, 50 checkVideoAccountExists,
51 isAccountNameWithHostValid,
44 isVideoAccountNameValid 52 isVideoAccountNameValid
45} 53}
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'
5import { CONFIG, database as db } from '../initializers' 5import { CONFIG, database as db } from '../initializers'
6import { ResultList } from '../../shared' 6import { ResultList } from '../../shared'
7import { VideoResolution } from '../../shared/models/videos/video-resolution.enum' 7import { VideoResolution } from '../../shared/models/videos/video-resolution.enum'
8import { AccountInstance } from '../models/account/account-interface'
8 9
9function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) { 10function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) {
10 return res.type('json').status(400).end() 11 return res.type('json').status(400).end()
@@ -78,6 +79,15 @@ function resetSequelizeInstance (instance: Sequelize.Instance<any>, savedFields:
78 }) 79 })
79} 80}
80 81
82let applicationAccount: AccountInstance
83async function getApplicationAccount () {
84 if (applicationAccount === undefined) {
85 applicationAccount = await db.Account.loadApplication()
86 }
87
88 return Promise.resolve(applicationAccount)
89}
90
81type SortType = { sortModel: any, sortValue: string } 91type SortType = { sortModel: any, sortValue: string }
82 92
83// --------------------------------------------------------------------------- 93// ---------------------------------------------------------------------------
@@ -89,5 +99,6 @@ export {
89 isSignupAllowed, 99 isSignupAllowed,
90 computeResolutionsToTranscode, 100 computeResolutionsToTranscode,
91 resetSequelizeInstance, 101 resetSequelizeInstance,
102 getApplicationAccount,
92 SortType 103 SortType
93} 104}
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 {
14 JobCategory 14 JobCategory
15} from '../../shared/models' 15} from '../../shared/models'
16import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum' 16import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum'
17import { FollowState } from '../../shared/models/accounts/follow.model'
17 18
18// --------------------------------------------------------------------------- 19// ---------------------------------------------------------------------------
19 20
@@ -34,12 +35,13 @@ const SEARCHABLE_COLUMNS = {
34 35
35// Sortable columns per schema 36// Sortable columns per schema
36const SORTABLE_COLUMNS = { 37const SORTABLE_COLUMNS = {
37 PODS: [ 'id', 'host', 'score', 'createdAt' ],
38 USERS: [ 'id', 'username', 'createdAt' ], 38 USERS: [ 'id', 'username', 'createdAt' ],
39 VIDEO_ABUSES: [ 'id', 'createdAt' ], 39 VIDEO_ABUSES: [ 'id', 'createdAt' ],
40 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], 40 VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
41 VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ], 41 VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ],
42 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ] 42 BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
43 FOLLOWERS: [ 'createdAt' ],
44 FOLLOWING: [ 'createdAt' ]
43} 45}
44 46
45const OAUTH_LIFETIME = { 47const OAUTH_LIFETIME = {
@@ -236,27 +238,10 @@ const PODS_SCORE = {
236 BONUS: 10 238 BONUS: 10
237} 239}
238 240
239// Time to wait between requests to the friends (10 min) 241const FOLLOW_STATES: { [ id: string ]: FollowState } = {
240let REQUESTS_INTERVAL = 600000 242 PENDING: 'pending',
241 243 ACCEPTED: 'accepted'
242// Number of requests in parallel we can make 244}
243const REQUESTS_IN_PARALLEL = 10
244
245// To how many pods we send requests
246const REQUESTS_LIMIT_PODS = 10
247// How many requests we send to a pod per interval
248const REQUESTS_LIMIT_PER_POD = 5
249
250const REQUESTS_VIDEO_QADU_LIMIT_PODS = 10
251// The QADU requests are not big
252const REQUESTS_VIDEO_QADU_LIMIT_PER_POD = 50
253
254const REQUESTS_VIDEO_EVENT_LIMIT_PODS = 10
255// The EVENTS requests are not big
256const REQUESTS_VIDEO_EVENT_LIMIT_PER_POD = 50
257
258// Number of requests to retry for replay requests module
259const RETRY_REQUESTS = 5
260 245
261const REMOTE_SCHEME = { 246const REMOTE_SCHEME = {
262 HTTP: 'https', 247 HTTP: 'https',
@@ -333,7 +318,6 @@ const OPENGRAPH_AND_OEMBED_COMMENT = '<!-- open graph and oembed tags -->'
333if (isTestInstance() === true) { 318if (isTestInstance() === true) {
334 CONSTRAINTS_FIELDS.VIDEOS.DURATION.max = 14 319 CONSTRAINTS_FIELDS.VIDEOS.DURATION.max = 14
335 FRIEND_SCORE.BASE = 20 320 FRIEND_SCORE.BASE = 20
336 REQUESTS_INTERVAL = 10000
337 JOBS_FETCHING_INTERVAL = 10000 321 JOBS_FETCHING_INTERVAL = 10000
338 REMOTE_SCHEME.HTTP = 'http' 322 REMOTE_SCHEME.HTTP = 'http'
339 REMOTE_SCHEME.WS = 'ws' 323 REMOTE_SCHEME.WS = 'ws'
@@ -361,15 +345,7 @@ export {
361 PODS_SCORE, 345 PODS_SCORE,
362 PREVIEWS_SIZE, 346 PREVIEWS_SIZE,
363 REMOTE_SCHEME, 347 REMOTE_SCHEME,
364 REQUESTS_IN_PARALLEL, 348 FOLLOW_STATES,
365 REQUESTS_INTERVAL,
366 REQUESTS_LIMIT_PER_POD,
367 REQUESTS_LIMIT_PODS,
368 REQUESTS_VIDEO_EVENT_LIMIT_PER_POD,
369 REQUESTS_VIDEO_EVENT_LIMIT_PODS,
370 REQUESTS_VIDEO_QADU_LIMIT_PER_POD,
371 REQUESTS_VIDEO_QADU_LIMIT_PODS,
372 RETRY_REQUESTS,
373 SEARCHABLE_COLUMNS, 349 SEARCHABLE_COLUMNS,
374 PRIVATE_RSA_KEY_SIZE, 350 PRIVATE_RSA_KEY_SIZE,
375 SORTABLE_COLUMNS, 351 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 @@
1import { ActivityAccept } from '../../../shared/models/activitypub/activity'
2import { database as db } from '../../initializers'
3import { AccountInstance } from '../../models/account/account-interface'
4
5async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: AccountInstance) {
6 if (inboxAccount === undefined) throw new Error('Need to accept on explicit inbox.')
7
8 const targetAccount = await db.Account.loadByUrl(activity.actor)
9
10 return processFollow(inboxAccount, targetAccount)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 processAcceptActivity
17}
18
19// ---------------------------------------------------------------------------
20
21async function processFollow (account: AccountInstance, targetAccount: AccountInstance) {
22 const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id)
23 if (!follow) throw new Error('Cannot find associated follow.')
24
25 follow.set('state', 'accepted')
26 return follow.save()
27}
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 @@
1import { ActivityDelete } from '../../../shared/models/activitypub/activity'
2import { getOrCreateAccount } from '../../helpers/activitypub'
3import { retryTransactionWrapper } from '../../helpers/database-utils'
4import { logger } from '../../helpers/logger'
5import { database as db } from '../../initializers'
6import { AccountInstance } from '../../models/account/account-interface'
7import { VideoChannelInstance } from '../../models/video/video-channel-interface'
8import { VideoInstance } from '../../models/video/video-interface'
9
10async function processDeleteActivity (activity: ActivityDelete) {
11 const account = await getOrCreateAccount(activity.actor)
12
13 if (account.url === activity.id) {
14 return processDeleteAccount(account)
15 }
16
17 {
18 let videoObject = await db.Video.loadByUrl(activity.id)
19 if (videoObject !== undefined) {
20 return processDeleteVideo(account, videoObject)
21 }
22 }
23
24 {
25 let videoChannelObject = await db.VideoChannel.loadByUrl(activity.id)
26 if (videoChannelObject !== undefined) {
27 return processDeleteVideoChannel(account, videoChannelObject)
28 }
29 }
30
31 return undefined
32}
33
34// ---------------------------------------------------------------------------
35
36export {
37 processDeleteActivity
38}
39
40// ---------------------------------------------------------------------------
41
42async function processDeleteVideo (account: AccountInstance, videoToDelete: VideoInstance) {
43 const options = {
44 arguments: [ account, videoToDelete ],
45 errorMessage: 'Cannot remove the remote video with many retries.'
46 }
47
48 await retryTransactionWrapper(deleteRemoteVideo, options)
49}
50
51async function deleteRemoteVideo (account: AccountInstance, videoToDelete: VideoInstance) {
52 logger.debug('Removing remote video "%s".', videoToDelete.uuid)
53
54 await db.sequelize.transaction(async t => {
55 if (videoToDelete.VideoChannel.Account.id !== account.id) {
56 throw new Error('Account ' + account.url + ' does not own video channel ' + videoToDelete.VideoChannel.url)
57 }
58
59 await videoToDelete.destroy({ transaction: t })
60 })
61
62 logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
63}
64
65async function processDeleteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) {
66 const options = {
67 arguments: [ account, videoChannelToRemove ],
68 errorMessage: 'Cannot remove the remote video channel with many retries.'
69 }
70
71 await retryTransactionWrapper(deleteRemoteVideoChannel, options)
72}
73
74async function deleteRemoteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) {
75 logger.debug('Removing remote video channel "%s".', videoChannelToRemove.uuid)
76
77 await db.sequelize.transaction(async t => {
78 if (videoChannelToRemove.Account.id !== account.id) {
79 throw new Error('Account ' + account.url + ' does not own video channel ' + videoChannelToRemove.url)
80 }
81
82 await videoChannelToRemove.destroy({ transaction: t })
83 })
84
85 logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.uuid)
86}
87
88async function processDeleteAccount (accountToRemove: AccountInstance) {
89 const options = {
90 arguments: [ accountToRemove ],
91 errorMessage: 'Cannot remove the remote account with many retries.'
92 }
93
94 await retryTransactionWrapper(deleteRemoteAccount, options)
95}
96
97async function deleteRemoteAccount (accountToRemove: AccountInstance) {
98 logger.debug('Removing remote account "%s".', accountToRemove.uuid)
99
100 await db.sequelize.transaction(async t => {
101 await accountToRemove.destroy({ transaction: t })
102 })
103
104 logger.info('Remote account with uuid %s removed.', accountToRemove.uuid)
105}
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 @@
1import { ActivityFollow } from '../../../shared/models/activitypub/activity'
2import { getOrCreateAccount } from '../../helpers'
3import { database as db } from '../../initializers'
4import { AccountInstance } from '../../models/account/account-interface'
5
6async function processFollowActivity (activity: ActivityFollow) {
7 const activityObject = activity.object
8 const account = await getOrCreateAccount(activity.actor)
9
10 return processFollow(account, activityObject)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 processFollowActivity
17}
18
19// ---------------------------------------------------------------------------
20
21async function processFollow (account: AccountInstance, targetAccountURL: string) {
22 const targetAccount = await db.Account.loadByUrl(targetAccountURL)
23
24 if (targetAccount === undefined) throw new Error('Unknown account')
25 if (targetAccount.isOwned() === false) throw new Error('This is not a local account.')
26
27 return db.AccountFollow.create({
28 accountId: account.id,
29 targetAccountId: targetAccount.id,
30 state: 'accepted'
31 })
32}
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
25} 25}
26 26
27function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) { 27function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
28 const videoChannelObject = videoChannel.toActivityPubObject() 28 const data = deleteActivityData(videoChannel.url, videoChannel.Account)
29 const data = deleteActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
30 29
31 return broadcastToFollowers(data, videoChannel.Account, t) 30 return broadcastToFollowers(data, videoChannel.Account, t)
32} 31}
@@ -46,12 +45,17 @@ function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) {
46} 45}
47 46
48function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) { 47function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) {
49 const videoObject = video.toActivityPubObject() 48 const data = deleteActivityData(video.url, video.VideoChannel.Account)
50 const data = deleteActivityData(video.url, video.VideoChannel.Account, videoObject)
51 49
52 return broadcastToFollowers(data, video.VideoChannel.Account, t) 50 return broadcastToFollowers(data, video.VideoChannel.Account, t)
53} 51}
54 52
53function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) {
54 const data = deleteActivityData(account.url, account)
55
56 return broadcastToFollowers(data, account, t)
57}
58
55// --------------------------------------------------------------------------- 59// ---------------------------------------------------------------------------
56 60
57export { 61export {
@@ -60,13 +64,14 @@ export {
60 sendDeleteVideoChannel, 64 sendDeleteVideoChannel,
61 sendAddVideo, 65 sendAddVideo,
62 sendUpdateVideo, 66 sendUpdateVideo,
63 sendDeleteVideo 67 sendDeleteVideo,
68 sendDeleteAccount
64} 69}
65 70
66// --------------------------------------------------------------------------- 71// ---------------------------------------------------------------------------
67 72
68async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t: Sequelize.Transaction) { 73async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t: Sequelize.Transaction) {
69 const result = await db.Account.listFollowerUrlsForApi(fromAccount.name, 0) 74 const result = await db.Account.listFollowerUrlsForApi(fromAccount.id, 0)
70 75
71 const jobPayload = { 76 const jobPayload = {
72 uris: result.data, 77 uris: result.data,
@@ -114,14 +119,11 @@ async function updateActivityData (url: string, byAccount: AccountInstance, obje
114 return buildSignedActivity(byAccount, base) 119 return buildSignedActivity(byAccount, base)
115} 120}
116 121
117async function deleteActivityData (url: string, byAccount: AccountInstance, object: any) { 122async function deleteActivityData (url: string, byAccount: AccountInstance) {
118 const to = await getPublicActivityTo(byAccount)
119 const base = { 123 const base = {
120 type: 'Update', 124 type: 'Delete',
121 id: url, 125 id: url,
122 actor: byAccount.url, 126 actor: byAccount.url
123 to,
124 object
125 } 127 }
126 128
127 return buildSignedActivity(byAccount, base) 129 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
34 return next() 34 return next()
35} 35}
36 36
37function setFollowersSort (req: express.Request, res: express.Response, next: express.NextFunction) {
38 if (!req.query.sort) req.query.sort = '-createdAt'
39
40 return next()
41}
42
43function setFollowingSort (req: express.Request, res: express.Response, next: express.NextFunction) {
44 if (!req.query.sort) req.query.sort = '-createdAt'
45
46 return next()
47}
48
37function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) { 49function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) {
38 let newSort: SortType = { sortModel: undefined, sortValue: undefined } 50 let newSort: SortType = { sortModel: undefined, sortValue: undefined }
39 51
@@ -63,5 +75,7 @@ export {
63 setVideoAbusesSort, 75 setVideoAbusesSort,
64 setVideoChannelsSort, 76 setVideoChannelsSort,
65 setVideosSort, 77 setVideosSort,
66 setBlacklistSort 78 setBlacklistSort,
79 setFollowersSort,
80 setFollowingSort
67} 81}
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 @@
1import { param } from 'express-validator/check'
2import * as express from 'express' 1import * as express from 'express'
3 2import { param } from 'express-validator/check'
4import { database as db } from '../../initializers/database'
5import { checkErrors } from './utils'
6import { 3import {
7 logger,
8 isUserUsernameValid,
9 isUserPasswordValid,
10 isUserVideoQuotaValid,
11 isUserDisplayNSFWValid, 4 isUserDisplayNSFWValid,
5 isUserPasswordValid,
12 isUserRoleValid, 6 isUserRoleValid,
13 isAccountNameValid 7 isUserUsernameValid,
8 isUserVideoQuotaValid,
9 logger
14} from '../../helpers' 10} from '../../helpers'
11import { isAccountNameWithHostValid } from '../../helpers/custom-validators/video-accounts'
12import { database as db } from '../../initializers/database'
15import { AccountInstance } from '../../models' 13import { AccountInstance } from '../../models'
14import { checkErrors } from './utils'
16 15
17const localAccountValidator = [ 16const localAccountValidator = [
18 param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'), 17 param('nameWithHost').custom(isAccountNameWithHostValid).withMessage('Should have a valid account with domain name (myuser@domain.tld)'),
19 18
20 (req: express.Request, res: express.Response, next: express.NextFunction) => { 19 (req: express.Request, res: express.Response, next: express.NextFunction) => {
21 logger.debug('Checking localAccountValidator parameters', { parameters: req.params }) 20 logger.debug('Checking localAccountValidator parameters', { parameters: req.params })
@@ -34,8 +33,10 @@ export {
34 33
35// --------------------------------------------------------------------------- 34// ---------------------------------------------------------------------------
36 35
37function checkLocalAccountExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) { 36function checkLocalAccountExists (nameWithHost: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) {
38 db.Account.loadLocalAccountByName(name) 37 const [ name, host ] = nameWithHost.split('@')
38
39 db.Account.loadLocalAccountByNameAndPod(name, host)
39 .then(account => { 40 .then(account => {
40 if (!account) { 41 if (!account) {
41 return res.status(404) 42 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'
6import { SORTABLE_COLUMNS } from '../../initializers' 6import { SORTABLE_COLUMNS } from '../../initializers'
7 7
8// Initialize constants here for better performances 8// Initialize constants here for better performances
9const SORTABLE_PODS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PODS)
10const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) 9const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
11const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) 10const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
12const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) 11const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
13const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) 12const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
14const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) 13const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
14const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
15const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
15 16
16const podsSortValidator = checkSort(SORTABLE_PODS_COLUMNS)
17const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) 17const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
18const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) 18const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
19const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) 19const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
20const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) 20const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
21const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) 21const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
22const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
23const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
22 24
23// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
24 26
25export { 27export {
26 podsSortValidator,
27 usersSortValidator, 28 usersSortValidator,
28 videoAbusesSortValidator, 29 videoAbusesSortValidator,
29 videoChannelsSortValidator, 30 videoChannelsSortValidator,
30 videosSortValidator, 31 videosSortValidator,
31 blacklistSortValidator 32 blacklistSortValidator,
33 followersSortValidator,
34 followingSortValidator
32} 35}
33 36
34// --------------------------------------------------------------------------- 37// ---------------------------------------------------------------------------
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 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird' 2import * as Bluebird from 'bluebird'
3 3import { FollowState } from '../../../shared/models/accounts/follow.model'
4import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
5 4
6export namespace AccountFollowMethods { 5export namespace AccountFollowMethods {
6 export type LoadByAccountAndTarget = (accountId: number, targetAccountId: number) => Bluebird<AccountFollowInstance>
7} 7}
8 8
9export interface AccountFollowClass { 9export interface AccountFollowClass {
10 loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget
10} 11}
11 12
12export interface AccountFollowAttributes { 13export interface AccountFollowAttributes {
13 accountId: number 14 accountId: number
14 targetAccountId: number 15 targetAccountId: number
16 state: FollowState
15} 17}
16 18
17export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> { 19export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> {
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 @@
1import { values } from 'lodash'
1import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
2 3
3import { addMethodsToModel } from '../utils' 4import { addMethodsToModel } from '../utils'
4import { 5import { AccountFollowAttributes, AccountFollowInstance, AccountFollowMethods } from './account-follow-interface'
5 AccountFollowInstance, 6import { FOLLOW_STATES } from '../../initializers/constants'
6 AccountFollowAttributes,
7
8 AccountFollowMethods
9} from './account-follow-interface'
10 7
11let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> 8let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes>
9let loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget
12 10
13export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { 11export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
14 AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow', 12 AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow',
15 { }, 13 {
14 state: {
15 type: DataTypes.ENUM(values(FOLLOW_STATES)),
16 allowNull: false
17 }
18 },
16 { 19 {
17 indexes: [ 20 indexes: [
18 { 21 {
@@ -43,6 +46,7 @@ function associate (models) {
43 name: 'accountId', 46 name: 'accountId',
44 allowNull: false 47 allowNull: false
45 }, 48 },
49 as: 'followers',
46 onDelete: 'CASCADE' 50 onDelete: 'CASCADE'
47 }) 51 })
48 52
@@ -51,6 +55,18 @@ function associate (models) {
51 name: 'targetAccountId', 55 name: 'targetAccountId',
52 allowNull: false 56 allowNull: false
53 }, 57 },
58 as: 'following',
54 onDelete: 'CASCADE' 59 onDelete: 'CASCADE'
55 }) 60 })
56} 61}
62
63loadByAccountAndTarget = function (accountId: number, targetAccountId: number) {
64 const query = {
65 where: {
66 accountId,
67 targetAccountId
68 }
69 }
70
71 return AccountFollow.findOne(query)
72}
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 @@
1import * as Sequelize from 'sequelize'
2import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
3 2import * as Sequelize from 'sequelize'
3import { Account as FormattedAccount, ActivityPubActor } from '../../../shared'
4import { ResultList } from '../../../shared/models/result-list.model'
4import { PodInstance } from '../pod/pod-interface' 5import { PodInstance } from '../pod/pod-interface'
5import { VideoChannelInstance } from '../video/video-channel-interface' 6import { VideoChannelInstance } from '../video/video-channel-interface'
6import { ActivityPubActor } from '../../../shared'
7import { ResultList } from '../../../shared/models/result-list.model'
8 7
9export namespace AccountMethods { 8export namespace AccountMethods {
9 export type LoadApplication = () => Bluebird<AccountInstance>
10
10 export type Load = (id: number) => Bluebird<AccountInstance> 11 export type Load = (id: number) => Bluebird<AccountInstance>
11 export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance> 12 export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
12 export type LoadByUrl = (url: string) => Bluebird<AccountInstance> 13 export type LoadByUrl = (url: string) => Bluebird<AccountInstance>
13 export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance> 14 export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance>
14 export type LoadLocalAccountByName = (name: string) => Bluebird<AccountInstance> 15 export type LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird<AccountInstance>
15 export type ListOwned = () => Bluebird<AccountInstance[]> 16 export type ListOwned = () => Bluebird<AccountInstance[]>
16 export type ListFollowerUrlsForApi = (name: string, start: number, count?: number) => Promise< ResultList<string> > 17 export type ListFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
17 export type ListFollowingUrlsForApi = (name: string, start: number, count?: number) => Promise< ResultList<string> > 18 export type ListFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
19 export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> >
20 export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> >
18 21
19 export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor 22 export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor
23 export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount
20 export type IsOwned = (this: AccountInstance) => boolean 24 export type IsOwned = (this: AccountInstance) => boolean
21 export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird<string[]> 25 export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird<string[]>
22 export type GetFollowingUrl = (this: AccountInstance) => string 26 export type GetFollowingUrl = (this: AccountInstance) => string
@@ -25,14 +29,17 @@ export namespace AccountMethods {
25} 29}
26 30
27export interface AccountClass { 31export interface AccountClass {
32 loadApplication: AccountMethods.LoadApplication
28 loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID 33 loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
29 load: AccountMethods.Load 34 load: AccountMethods.Load
30 loadByUUID: AccountMethods.LoadByUUID 35 loadByUUID: AccountMethods.LoadByUUID
31 loadByUrl: AccountMethods.LoadByUrl 36 loadByUrl: AccountMethods.LoadByUrl
32 loadLocalAccountByName: AccountMethods.LoadLocalAccountByName 37 loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod
33 listOwned: AccountMethods.ListOwned 38 listOwned: AccountMethods.ListOwned
34 listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi 39 listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
35 listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi 40 listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
41 listFollowingForApi: AccountMethods.ListFollowingForApi
42 listFollowersForApi: AccountMethods.ListFollowersForApi
36} 43}
37 44
38export interface AccountAttributes { 45export interface AccountAttributes {
@@ -58,6 +65,7 @@ export interface AccountAttributes {
58export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> { 65export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
59 isOwned: AccountMethods.IsOwned 66 isOwned: AccountMethods.IsOwned
60 toActivityPubObject: AccountMethods.ToActivityPubObject 67 toActivityPubObject: AccountMethods.ToActivityPubObject
68 toFormattedJSON: AccountMethods.ToFormattedJSON
61 getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls 69 getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
62 getFollowingUrl: AccountMethods.GetFollowingUrl 70 getFollowingUrl: AccountMethods.GetFollowingUrl
63 getFollowersUrl: AccountMethods.GetFollowersUrl 71 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 {
15 activityPubContextify 15 activityPubContextify
16} from '../../helpers' 16} from '../../helpers'
17 17
18import { addMethodsToModel } from '../utils' 18import { addMethodsToModel, getSort } from '../utils'
19import { 19import {
20 AccountInstance, 20 AccountInstance,
21 AccountAttributes, 21 AccountAttributes,
22 22
23 AccountMethods 23 AccountMethods
24} from './account-interface' 24} from './account-interface'
25import LoadApplication = AccountMethods.LoadApplication
26import { sendDeleteAccount } from '../../lib/activitypub/send-request'
25 27
26let Account: Sequelize.Model<AccountInstance, AccountAttributes> 28let Account: Sequelize.Model<AccountInstance, AccountAttributes>
27let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID 29let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
28let load: AccountMethods.Load 30let load: AccountMethods.Load
31let loadApplication: AccountMethods.LoadApplication
29let loadByUUID: AccountMethods.LoadByUUID 32let loadByUUID: AccountMethods.LoadByUUID
30let loadByUrl: AccountMethods.LoadByUrl 33let loadByUrl: AccountMethods.LoadByUrl
31let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName 34let loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod
32let listOwned: AccountMethods.ListOwned 35let listOwned: AccountMethods.ListOwned
33let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi 36let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
34let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi 37let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
38let listFollowingForApi: AccountMethods.ListFollowingForApi
39let listFollowersForApi: AccountMethods.ListFollowersForApi
35let isOwned: AccountMethods.IsOwned 40let isOwned: AccountMethods.IsOwned
36let toActivityPubObject: AccountMethods.ToActivityPubObject 41let toActivityPubObject: AccountMethods.ToActivityPubObject
42let toFormattedJSON: AccountMethods.ToFormattedJSON
37let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls 43let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
38let getFollowingUrl: AccountMethods.GetFollowingUrl 44let getFollowingUrl: AccountMethods.GetFollowingUrl
39let getFollowersUrl: AccountMethods.GetFollowersUrl 45let getFollowersUrl: AccountMethods.GetFollowersUrl
@@ -189,16 +195,20 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
189 const classMethods = [ 195 const classMethods = [
190 associate, 196 associate,
191 loadAccountByPodAndUUID, 197 loadAccountByPodAndUUID,
198 loadApplication,
192 load, 199 load,
193 loadByUUID, 200 loadByUUID,
194 loadLocalAccountByName, 201 loadLocalAccountByNameAndPod,
195 listOwned, 202 listOwned,
196 listFollowerUrlsForApi, 203 listFollowerUrlsForApi,
197 listFollowingUrlsForApi 204 listFollowingUrlsForApi,
205 listFollowingForApi,
206 listFollowersForApi
198 ] 207 ]
199 const instanceMethods = [ 208 const instanceMethods = [
200 isOwned, 209 isOwned,
201 toActivityPubObject, 210 toActivityPubObject,
211 toFormattedJSON,
202 getFollowerSharedInboxUrls, 212 getFollowerSharedInboxUrls,
203 getFollowingUrl, 213 getFollowingUrl,
204 getFollowersUrl, 214 getFollowersUrl,
@@ -250,6 +260,7 @@ function associate (models) {
250 name: 'accountId', 260 name: 'accountId',
251 allowNull: false 261 allowNull: false
252 }, 262 },
263 as: 'following',
253 onDelete: 'cascade' 264 onDelete: 'cascade'
254 }) 265 })
255 266
@@ -258,23 +269,29 @@ function associate (models) {
258 name: 'targetAccountId', 269 name: 'targetAccountId',
259 allowNull: false 270 allowNull: false
260 }, 271 },
272 as: 'followers',
261 onDelete: 'cascade' 273 onDelete: 'cascade'
262 }) 274 })
263} 275}
264 276
265function afterDestroy (account: AccountInstance) { 277function afterDestroy (account: AccountInstance) {
266 if (account.isOwned()) { 278 if (account.isOwned()) {
267 const removeVideoAccountToFriendsParams = { 279 return sendDeleteAccount(account, undefined)
268 uuid: account.uuid
269 }
270
271 // FIXME: remove account in followers
272 // return removeVideoAccountToFriends(removeVideoAccountToFriendsParams)
273 } 280 }
274 281
275 return undefined 282 return undefined
276} 283}
277 284
285toFormattedJSON = function (this: AccountInstance) {
286 const json = {
287 id: this.id,
288 host: this.Pod.host,
289 name: this.name
290 }
291
292 return json
293}
294
278toActivityPubObject = function (this: AccountInstance) { 295toActivityPubObject = function (this: AccountInstance) {
279 const type = this.podId ? 'Application' as 'Application' : 'Person' as 'Person' 296 const type = this.podId ? 'Application' as 'Application' : 'Person' as 'Person'
280 297
@@ -347,12 +364,85 @@ listOwned = function () {
347 return Account.findAll(query) 364 return Account.findAll(query)
348} 365}
349 366
350listFollowerUrlsForApi = function (name: string, start: number, count?: number) { 367listFollowerUrlsForApi = function (id: number, start: number, count?: number) {
351 return createListFollowForApiQuery('followers', name, start, count) 368 return createListFollowForApiQuery('followers', id, start, count)
369}
370
371listFollowingUrlsForApi = function (id: number, start: number, count?: number) {
372 return createListFollowForApiQuery('following', id, start, count)
373}
374
375listFollowingForApi = function (id: number, start: number, count: number, sort: string) {
376 const query = {
377 distinct: true,
378 offset: start,
379 limit: count,
380 order: [ getSort(sort) ],
381 include: [
382 {
383 model: Account['sequelize'].models.AccountFollow,
384 required: true,
385 as: 'following',
386 include: [
387 {
388 model: Account['sequelize'].models.Account,
389 as: 'following',
390 required: true,
391 include: [ Account['sequelize'].models.Pod ]
392 }
393 ]
394 }
395 ]
396 }
397
398 return Account.findAndCountAll(query).then(({ rows, count }) => {
399 return {
400 data: rows,
401 total: count
402 }
403 })
404}
405
406listFollowersForApi = function (id: number, start: number, count: number, sort: string) {
407 const query = {
408 distinct: true,
409 offset: start,
410 limit: count,
411 order: [ getSort(sort) ],
412 include: [
413 {
414 model: Account['sequelize'].models.AccountFollow,
415 required: true,
416 as: 'followers',
417 include: [
418 {
419 model: Account['sequelize'].models.Account,
420 as: 'followers',
421 required: true,
422 include: [ Account['sequelize'].models.Pod ]
423 }
424 ]
425 }
426 ]
427 }
428
429 return Account.findAndCountAll(query).then(({ rows, count }) => {
430 return {
431 data: rows,
432 total: count
433 }
434 })
352} 435}
353 436
354listFollowingUrlsForApi = function (name: string, start: number, count?: number) { 437loadApplication = function () {
355 return createListFollowForApiQuery('following', name, start, count) 438 return Account.findOne({
439 include: [
440 {
441 model: Account['sequelize'].model.Application,
442 required: true
443 }
444 ]
445 })
356} 446}
357 447
358load = function (id: number) { 448load = function (id: number) {
@@ -369,14 +459,22 @@ loadByUUID = function (uuid: string) {
369 return Account.findOne(query) 459 return Account.findOne(query)
370} 460}
371 461
372loadLocalAccountByName = function (name: string) { 462loadLocalAccountByNameAndPod = function (name: string, host: string) {
373 const query: Sequelize.FindOptions<AccountAttributes> = { 463 const query: Sequelize.FindOptions<AccountAttributes> = {
374 where: { 464 where: {
375 name, 465 name,
376 userId: { 466 userId: {
377 [Sequelize.Op.ne]: null 467 [Sequelize.Op.ne]: null
378 } 468 }
379 } 469 },
470 include: [
471 {
472 model: Account['sequelize'].models.Pod,
473 where: {
474 host
475 }
476 }
477 ]
380 } 478 }
381 479
382 return Account.findOne(query) 480 return Account.findOne(query)
@@ -406,7 +504,7 @@ loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Se
406 504
407// ------------------------------ UTILS ------------------------------ 505// ------------------------------ UTILS ------------------------------
408 506
409async function createListFollowForApiQuery (type: 'followers' | 'following', name: string, start: number, count?: number) { 507async function createListFollowForApiQuery (type: 'followers' | 'following', id: number, start: number, count?: number) {
410 let firstJoin: string 508 let firstJoin: string
411 let secondJoin: string 509 let secondJoin: string
412 510
@@ -424,14 +522,14 @@ async function createListFollowForApiQuery (type: 'followers' | 'following', nam
424 for (const selection of selections) { 522 for (const selection of selections) {
425 let query = 'SELECT ' + selection + ' FROM "Account" ' + 523 let query = 'SELECT ' + selection + ' FROM "Account" ' +
426 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' + 524 'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' +
427 'INNER JOIN "Account" AS "Followers" ON "Followers"."id" = "AccountFollower"."' + secondJoin + '" ' + 525 'INNER JOIN "Account" AS "Follows" ON "Followers"."id" = "Follows"."' + secondJoin + '" ' +
428 'WHERE "Account"."name" = \'$name\' ' + 526 'WHERE "Account"."id" = $id ' +
429 'LIMIT ' + start 527 'LIMIT ' + start
430 528
431 if (count !== undefined) query += ', ' + count 529 if (count !== undefined) query += ', ' + count
432 530
433 const options = { 531 const options = {
434 bind: { name }, 532 bind: { id },
435 type: Sequelize.QueryTypes.SELECT 533 type: Sequelize.QueryTypes.SELECT
436 } 534 }
437 tasks.push(Account['sequelize'].query(query, options)) 535 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 {
9 9
10 VideoChannelMethods 10 VideoChannelMethods
11} from './video-channel-interface' 11} from './video-channel-interface'
12import { sendDeleteVideoChannel } from '../../lib/activitypub/send-request'
12 13
13let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes> 14let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
14let toFormattedJSON: VideoChannelMethods.ToFormattedJSON 15let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
@@ -176,11 +177,7 @@ function associate (models) {
176 177
177function afterDestroy (videoChannel: VideoChannelInstance) { 178function afterDestroy (videoChannel: VideoChannelInstance) {
178 if (videoChannel.isOwned()) { 179 if (videoChannel.isOwned()) {
179 const removeVideoChannelToFriendsParams = { 180 return sendDeleteVideoChannel(videoChannel, undefined)
180 uuid: videoChannel.uuid
181 }
182
183 // FIXME: send remove event to followers
184 } 181 }
185 182
186 return undefined 183 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 @@
1import * as Sequelize from 'sequelize'
2import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
4import { ResultList } from '../../../shared/models/result-list.model'
5import { Video as FormattedVideo, VideoDetails as FormattedDetailsVideo } from '../../../shared/models/videos/video.model'
3 6
4import { TagAttributes, TagInstance } from './tag-interface' 7import { TagAttributes, TagInstance } from './tag-interface'
5import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
6
7// Don't use barrel, import just what we need
8import {
9 Video as FormattedVideo,
10 VideoDetails as FormattedDetailsVideo
11} from '../../../shared/models/videos/video.model'
12import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model'
13import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model'
14import { ResultList } from '../../../shared/models/result-list.model'
15import { VideoChannelInstance } from './video-channel-interface' 8import { VideoChannelInstance } from './video-channel-interface'
16import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object' 9import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
17 10
18export namespace VideoMethods { 11export namespace VideoMethods {
19 export type GetThumbnailName = (this: VideoInstance) => string 12 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'
45import { TagInstance } from './tag-interface' 45import { TagInstance } from './tag-interface'
46import { VideoFileInstance, VideoFileModel } from './video-file-interface' 46import { VideoFileInstance, VideoFileModel } from './video-file-interface'
47import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface' 47import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface'
48import { sendDeleteVideo } from '../../lib/activitypub/send-request'
48 49
49const Buffer = safeBuffer.Buffer 50const Buffer = safeBuffer.Buffer
50 51
@@ -363,13 +364,9 @@ function afterDestroy (video: VideoInstance) {
363 ) 364 )
364 365
365 if (video.isOwned()) { 366 if (video.isOwned()) {
366 const removeVideoToFriendsParams = {
367 uuid: video.uuid
368 }
369
370 tasks.push( 367 tasks.push(
371 video.removePreview() 368 video.removePreview(),
372 // FIXME: remove video for followers 369 sendDeleteVideo(video, undefined)
373 ) 370 )
374 371
375 // Remove physical files and torrents 372 // 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'
3// /!\ Before imports /!\ 3// /!\ Before imports /!\
4process.env.NODE_ENV = 'test' 4process.env.NODE_ENV = 'test'
5 5
6import { REQUESTS_INTERVAL } from '../../initializers/constants'
7import { Video, VideoRateType, VideoFile } from '../../../shared' 6import { Video, VideoRateType, VideoFile } from '../../../shared'
8import { 7import {
9 ServerInfo as DefaultServerInfo, 8 ServerInfo as DefaultServerInfo,
@@ -137,7 +136,7 @@ async function start () {
137 initializeRequestsPerServer(servers) 136 initializeRequestsPerServer(servers)
138 checking = false 137 checking = false
139 clearInterval(waitingInterval) 138 clearInterval(waitingInterval)
140 }, REQUESTS_INTERVAL) 139 }, 10000)
141 }, integrityInterval) 140 }, integrityInterval)
142} 141}
143 142
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 @@
1export interface Account {
2 id: number
3 name: string
4 host: string
5}
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 @@
1export * from './account.model'
2export * 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 {
4} from './objects' 4} from './objects'
5import { ActivityPubSignature } from './activitypub-signature' 5import { ActivityPubSignature } from './activitypub-signature'
6 6
7export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | ActivityFlag 7export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | ActivityFlag |
8 ActivityDelete | ActivityFollow | ActivityAccept
8 9
9// Flag -> report abuse 10// Flag -> report abuse
10export type ActivityType = 'Create' | 'Add' | 'Update' | 'Flag' 11export type ActivityType = 'Create' | 'Add' | 'Update' | 'Flag' | 'Delete' | 'Follow' | 'Accept'
11 12
12export interface BaseActivity { 13export interface BaseActivity {
13 '@context'?: any[] 14 '@context'?: any[]
@@ -37,3 +38,16 @@ export interface ActivityFlag extends BaseActivity {
37 type: 'Flag' 38 type: 'Flag'
38 object: string 39 object: string
39} 40}
41
42export interface ActivityDelete extends BaseActivity {
43 type: 'Delete'
44}
45
46export interface ActivityFollow extends BaseActivity {
47 type: 'Follow'
48 object: string
49}
50
51export interface ActivityAccept extends BaseActivity {
52 type: 'Accept'
53}
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 @@
1export * from './accounts'
1export * from './activitypub' 2export * from './activitypub'
2export * from './pods' 3export * from './pods'
3export * from './users' 4export * from './users'