</video>
</body>
-</html
+</html>
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))
)
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)
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)
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 {
// 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) {
continue
}
- await activityProcessor(activity)
+ await activityProcessor(activity, inboxAccount)
}
}
const remoteRouter = express.Router()
-remoteRouter.use('/inbox', inboxRouter)
+remoteRouter.use('/', inboxRouter)
remoteRouter.use('/', activityPubClientRouter)
remoteRouter.use('/*', badRequest)
// 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 ],
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)
)
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-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))
}
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)) {
export {
checkVideoAccountExists,
+ isAccountNameWithHostValid,
isVideoAccountNameValid
}
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()
})
}
+let applicationAccount: AccountInstance
+async function getApplicationAccount () {
+ if (applicationAccount === undefined) {
+ applicationAccount = await db.Account.loadApplication()
+ }
+
+ return Promise.resolve(applicationAccount)
+}
+
type SortType = { sortModel: any, sortValue: string }
// ---------------------------------------------------------------------------
isSignupAllowed,
computeResolutionsToTranscode,
resetSequelizeInstance,
+ getApplicationAccount,
SortType
}
JobCategory
} from '../../shared/models'
import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum'
+import { FollowState } from '../../shared/models/accounts/follow.model'
// ---------------------------------------------------------------------------
// 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 = {
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',
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'
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,
--- /dev/null
+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()
+}
--- /dev/null
+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)
+}
--- /dev/null
+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'
+ })
+}
}
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)
}
}
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 {
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,
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)
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 }
setVideoAbusesSort,
setVideoChannelsSort,
setVideosSort,
- setBlacklistSort
+ setBlacklistSort,
+ setFollowersSort,
+ setFollowingSort
}
-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 })
// ---------------------------------------------------------------------------
-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)
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
}
// ---------------------------------------------------------------------------
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> {
+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: [
{
name: 'accountId',
allowNull: false
},
+ as: 'followers',
onDelete: 'CASCADE'
})
name: 'targetAccountId',
allowNull: false
},
+ as: 'following',
onDelete: 'CASCADE'
})
}
+
+loadByAccountAndTarget = function (accountId: number, targetAccountId: number) {
+ const query = {
+ where: {
+ accountId,
+ targetAccountId
+ }
+ }
+
+ return AccountFollow.findOne(query)
+}
-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
}
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 {
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
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
const classMethods = [
associate,
loadAccountByPodAndUUID,
+ loadApplication,
load,
loadByUUID,
- loadLocalAccountByName,
+ loadLocalAccountByNameAndPod,
listOwned,
listFollowerUrlsForApi,
- listFollowingUrlsForApi
+ listFollowingUrlsForApi,
+ listFollowingForApi,
+ listFollowersForApi
]
const instanceMethods = [
isOwned,
toActivityPubObject,
+ toFormattedJSON,
getFollowerSharedInboxUrls,
getFollowingUrl,
getFollowersUrl,
name: 'accountId',
allowNull: false
},
+ as: 'following',
onDelete: 'cascade'
})
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'
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) {
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)
// ------------------------------ 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
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))
VideoChannelMethods
} from './video-channel-interface'
+import { sendDeleteVideoChannel } from '../../lib/activitypub/send-request'
let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
function afterDestroy (videoChannel: VideoChannelInstance) {
if (videoChannel.isOwned()) {
- const removeVideoChannelToFriendsParams = {
- uuid: videoChannel.uuid
- }
-
- // FIXME: send remove event to followers
+ return sendDeleteVideoChannel(videoChannel, undefined)
}
return undefined
-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
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
)
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
// /!\ Before imports /!\
process.env.NODE_ENV = 'test'
-import { REQUESTS_INTERVAL } from '../../initializers/constants'
import { Video, VideoRateType, VideoFile } from '../../../shared'
import {
ServerInfo as DefaultServerInfo,
initializeRequestsPerServer(servers)
checking = false
clearInterval(waitingInterval)
- }, REQUESTS_INTERVAL)
+ }, 10000)
}, integrityInterval)
}
--- /dev/null
+export interface Account {
+ id: number
+ name: string
+ host: string
+}
--- /dev/null
+export type FollowState = 'pending' | 'accepted'
--- /dev/null
+export * from './account.model'
+export * from './follow.model'
} 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[]
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'
+}
+export * from './accounts'
export * from './activitypub'
export * from './pods'
export * from './users'