Handle follow/accept

pull/128/head
Chocobozzz 2017-11-13 17:39:41 +01:00
parent 571389d43b
commit 7a7724e66e
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
29 changed files with 493 additions and 208 deletions

View File

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

View File

@ -16,12 +16,12 @@ activityPubClientRouter.get('/account/:name',
executeIfActivityPub(asyncMiddleware(accountController))
)
activityPubClientRouter.get('/account/:name/followers',
activityPubClientRouter.get('/account/:nameWithHost/followers',
executeIfActivityPub(localAccountValidator),
executeIfActivityPub(asyncMiddleware(accountFollowersController))
)
activityPubClientRouter.get('/account/:name/following',
activityPubClientRouter.get('/account/:nameWithHost/following',
executeIfActivityPub(localAccountValidator),
executeIfActivityPub(asyncMiddleware(accountFollowingController))
)
@ -46,7 +46,7 @@ async function accountFollowersController (req: express.Request, res: express.Re
const page = req.params.page || 1
const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
const result = await db.Account.listFollowerUrlsForApi(account.name, start, count)
const result = await db.Account.listFollowerUrlsForApi(account.id, start, count)
const activityPubResult = activityPubCollectionPagination(req.url, page, result)
return res.json(activityPubResult)
@ -58,7 +58,7 @@ async function accountFollowingController (req: express.Request, res: express.Re
const page = req.params.page || 1
const { start, count } = pageToStartAndCount(page, ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE)
const result = await db.Account.listFollowingUrlsForApi(account.name, start, count)
const result = await db.Account.listFollowingUrlsForApi(account.id, start, count)
const activityPubResult = activityPubCollectionPagination(req.url, page, result)
return res.json(activityPubResult)

View File

@ -3,26 +3,41 @@ import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, Activity
import { logger } from '../../helpers'
import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
import { processCreateActivity, processFlagActivity, processUpdateActivity } from '../../lib'
import { processAcceptActivity } from '../../lib/activitypub/process-accept'
import { processAddActivity } from '../../lib/activitypub/process-add'
import { asyncMiddleware, checkSignature, signatureValidator } from '../../middlewares'
import { processDeleteActivity } from '../../lib/activitypub/process-delete'
import { processFollowActivity } from '../../lib/activitypub/process-follow'
import { asyncMiddleware, checkSignature, localAccountValidator, signatureValidator } from '../../middlewares'
import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
import { AccountInstance } from '../../models/account/account-interface'
const processActivity: { [ P in ActivityType ]: (activity: Activity) => Promise<any> } = {
const processActivity: { [ P in ActivityType ]: (activity: Activity, inboxAccount?: AccountInstance) => Promise<any> } = {
Create: processCreateActivity,
Add: processAddActivity,
Update: processUpdateActivity,
Flag: processFlagActivity
Flag: processFlagActivity,
Delete: processDeleteActivity,
Follow: processFollowActivity,
Accept: processAcceptActivity
}
const inboxRouter = express.Router()
inboxRouter.post('/',
inboxRouter.post('/inbox',
signatureValidator,
asyncMiddleware(checkSignature),
activityPubValidator,
asyncMiddleware(inboxController)
)
inboxRouter.post('/:nameWithHost/inbox',
signatureValidator,
asyncMiddleware(checkSignature),
localAccountValidator,
activityPubValidator,
asyncMiddleware(inboxController)
)
// ---------------------------------------------------------------------------
export {
@ -46,12 +61,12 @@ async function inboxController (req: express.Request, res: express.Response, nex
// Only keep activities we are able to process
activities = activities.filter(a => isActivityValid(a))
await processActivities(activities)
await processActivities(activities, res.locals.account)
res.status(204).end()
}
async function processActivities (activities: Activity[]) {
async function processActivities (activities: Activity[], inboxAccount?: AccountInstance) {
for (const activity of activities) {
const activityProcessor = processActivity[activity.type]
if (activityProcessor === undefined) {
@ -59,6 +74,6 @@ async function processActivities (activities: Activity[]) {
continue
}
await activityProcessor(activity)
await activityProcessor(activity, inboxAccount)
}
}

View File

@ -6,7 +6,7 @@ import { activityPubClientRouter } from './client'
const remoteRouter = express.Router()
remoteRouter.use('/inbox', inboxRouter)
remoteRouter.use('/', inboxRouter)
remoteRouter.use('/', activityPubClientRouter)
remoteRouter.use('/*', badRequest)

View File

@ -224,67 +224,6 @@
// logger.info('Remote video with uuid %s quick and dirty updated', videoUUID)
// }
//
// async function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
// const options = {
// arguments: [ videoToRemoveData, fromPod ],
// errorMessage: 'Cannot remove the remote video channel with many retries.'
// }
//
// await retryTransactionWrapper(removeRemoteVideo, options)
// }
//
// async function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) {
// logger.debug('Removing remote video "%s".', videoToRemoveData.uuid)
//
// await db.sequelize.transaction(async t => {
// // We need the instance because we have to remove some other stuffs (thumbnail etc)
// const videoInstance = await fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t)
// await videoInstance.destroy({ transaction: t })
// })
//
// logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid)
// }
//
// async function removeRemoteVideoAccountRetryWrapper (accountAttributesToRemove: RemoteVideoAccountRemoveData, fromPod: PodInstance) {
// const options = {
// arguments: [ accountAttributesToRemove, fromPod ],
// errorMessage: 'Cannot remove the remote video account with many retries.'
// }
//
// await retryTransactionWrapper(removeRemoteVideoAccount, options)
// }
//
// async function removeRemoteVideoAccount (accountAttributesToRemove: RemoteVideoAccountRemoveData, fromPod: PodInstance) {
// logger.debug('Removing remote video account "%s".', accountAttributesToRemove.uuid)
//
// await db.sequelize.transaction(async t => {
// const videoAccount = await db.Account.loadAccountByPodAndUUID(accountAttributesToRemove.uuid, fromPod.id, t)
// await videoAccount.destroy({ transaction: t })
// })
//
// logger.info('Remote video account with uuid %s removed.', accountAttributesToRemove.uuid)
// }
//
// async function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
// const options = {
// arguments: [ videoChannelAttributesToRemove, fromPod ],
// errorMessage: 'Cannot remove the remote video channel with many retries.'
// }
//
// await retryTransactionWrapper(removeRemoteVideoChannel, options)
// }
//
// async function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) {
// logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid)
//
// await db.sequelize.transaction(async t => {
// const videoChannel = await fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t)
// await videoChannel.destroy({ transaction: t })
// })
//
// logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid)
// }
//
// async function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) {
// const options = {
// arguments: [ reportData, fromPod ],

View File

@ -1,16 +1,27 @@
import * as express from 'express'
import { getFormattedObjects } from '../../helpers'
import { getApplicationAccount } from '../../helpers/utils'
import { database as db } from '../../initializers/database'
import { asyncMiddleware, paginationValidator, podsSortValidator, setPagination, setPodsSort } from '../../middlewares'
import { asyncMiddleware, paginationValidator, setFollowersSort, setPagination } from '../../middlewares'
import { setFollowingSort } from '../../middlewares/sort'
import { followersSortValidator, followingSortValidator } from '../../middlewares/validators/sort'
const podsRouter = express.Router()
podsRouter.get('/',
podsRouter.get('/following',
paginationValidator,
podsSortValidator,
setPodsSort,
followingSortValidator,
setFollowingSort,
setPagination,
asyncMiddleware(listPods)
asyncMiddleware(listFollowing)
)
podsRouter.get('/followers',
paginationValidator,
followersSortValidator,
setFollowersSort,
setPagination,
asyncMiddleware(listFollowers)
)
// ---------------------------------------------------------------------------
@ -21,8 +32,16 @@ export {
// ---------------------------------------------------------------------------
async function listPods (req: express.Request, res: express.Response, next: express.NextFunction) {
const resultList = await db.Pod.listForApi(req.query.start, req.query.count, req.query.sort)
async function listFollowing (req: express.Request, res: express.Response, next: express.NextFunction) {
const applicationAccount = await getApplicationAccount()
const resultList = await db.Account.listFollowingForApi(applicationAccount.id, req.query.start, req.query.count, req.query.sort)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function listFollowers (req: express.Request, res: express.Response, next: express.NextFunction) {
const applicationAccount = await getApplicationAccount()
const resultList = await db.Account.listFollowersForApi(applicationAccount.id, req.query.start, req.query.count, req.query.sort)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}

View File

@ -8,11 +8,18 @@ import { AccountInstance } from '../../models'
import { logger } from '../logger'
import { isUserUsernameValid } from './users'
import { isHostValid } from './pods'
function isVideoAccountNameValid (value: string) {
return isUserUsernameValid(value)
}
function isAccountNameWithHostValid (value: string) {
const [ name, host ] = value.split('@')
return isVideoAccountNameValid(name) && isHostValid(host)
}
function checkVideoAccountExists (id: string, res: express.Response, callback: () => void) {
let promise: Promise<AccountInstance>
if (validator.isInt(id)) {
@ -41,5 +48,6 @@ function checkVideoAccountExists (id: string, res: express.Response, callback: (
export {
checkVideoAccountExists,
isAccountNameWithHostValid,
isVideoAccountNameValid
}

View File

@ -5,6 +5,7 @@ import { pseudoRandomBytesPromise } from './core-utils'
import { CONFIG, database as db } from '../initializers'
import { ResultList } from '../../shared'
import { VideoResolution } from '../../shared/models/videos/video-resolution.enum'
import { AccountInstance } from '../models/account/account-interface'
function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) {
return res.type('json').status(400).end()
@ -78,6 +79,15 @@ function resetSequelizeInstance (instance: Sequelize.Instance<any>, savedFields:
})
}
let applicationAccount: AccountInstance
async function getApplicationAccount () {
if (applicationAccount === undefined) {
applicationAccount = await db.Account.loadApplication()
}
return Promise.resolve(applicationAccount)
}
type SortType = { sortModel: any, sortValue: string }
// ---------------------------------------------------------------------------
@ -89,5 +99,6 @@ export {
isSignupAllowed,
computeResolutionsToTranscode,
resetSequelizeInstance,
getApplicationAccount,
SortType
}

View File

@ -14,6 +14,7 @@ import {
JobCategory
} from '../../shared/models'
import { VideoPrivacy } from '../../shared/models/videos/video-privacy.enum'
import { FollowState } from '../../shared/models/accounts/follow.model'
// ---------------------------------------------------------------------------
@ -34,12 +35,13 @@ const SEARCHABLE_COLUMNS = {
// Sortable columns per schema
const SORTABLE_COLUMNS = {
PODS: [ 'id', 'host', 'score', 'createdAt' ],
USERS: [ 'id', 'username', 'createdAt' ],
VIDEO_ABUSES: [ 'id', 'createdAt' ],
VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ],
BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ]
BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
FOLLOWERS: [ 'createdAt' ],
FOLLOWING: [ 'createdAt' ]
}
const OAUTH_LIFETIME = {
@ -236,27 +238,10 @@ const PODS_SCORE = {
BONUS: 10
}
// Time to wait between requests to the friends (10 min)
let REQUESTS_INTERVAL = 600000
// Number of requests in parallel we can make
const REQUESTS_IN_PARALLEL = 10
// To how many pods we send requests
const REQUESTS_LIMIT_PODS = 10
// How many requests we send to a pod per interval
const REQUESTS_LIMIT_PER_POD = 5
const REQUESTS_VIDEO_QADU_LIMIT_PODS = 10
// The QADU requests are not big
const REQUESTS_VIDEO_QADU_LIMIT_PER_POD = 50
const REQUESTS_VIDEO_EVENT_LIMIT_PODS = 10
// The EVENTS requests are not big
const REQUESTS_VIDEO_EVENT_LIMIT_PER_POD = 50
// Number of requests to retry for replay requests module
const RETRY_REQUESTS = 5
const FOLLOW_STATES: { [ id: string ]: FollowState } = {
PENDING: 'pending',
ACCEPTED: 'accepted'
}
const REMOTE_SCHEME = {
HTTP: 'https',
@ -333,7 +318,6 @@ const OPENGRAPH_AND_OEMBED_COMMENT = '<!-- open graph and oembed tags -->'
if (isTestInstance() === true) {
CONSTRAINTS_FIELDS.VIDEOS.DURATION.max = 14
FRIEND_SCORE.BASE = 20
REQUESTS_INTERVAL = 10000
JOBS_FETCHING_INTERVAL = 10000
REMOTE_SCHEME.HTTP = 'http'
REMOTE_SCHEME.WS = 'ws'
@ -361,15 +345,7 @@ export {
PODS_SCORE,
PREVIEWS_SIZE,
REMOTE_SCHEME,
REQUESTS_IN_PARALLEL,
REQUESTS_INTERVAL,
REQUESTS_LIMIT_PER_POD,
REQUESTS_LIMIT_PODS,
REQUESTS_VIDEO_EVENT_LIMIT_PER_POD,
REQUESTS_VIDEO_EVENT_LIMIT_PODS,
REQUESTS_VIDEO_QADU_LIMIT_PER_POD,
REQUESTS_VIDEO_QADU_LIMIT_PODS,
RETRY_REQUESTS,
FOLLOW_STATES,
SEARCHABLE_COLUMNS,
PRIVATE_RSA_KEY_SIZE,
SORTABLE_COLUMNS,

View File

@ -0,0 +1,27 @@
import { ActivityAccept } from '../../../shared/models/activitypub/activity'
import { database as db } from '../../initializers'
import { AccountInstance } from '../../models/account/account-interface'
async function processAcceptActivity (activity: ActivityAccept, inboxAccount?: AccountInstance) {
if (inboxAccount === undefined) throw new Error('Need to accept on explicit inbox.')
const targetAccount = await db.Account.loadByUrl(activity.actor)
return processFollow(inboxAccount, targetAccount)
}
// ---------------------------------------------------------------------------
export {
processAcceptActivity
}
// ---------------------------------------------------------------------------
async function processFollow (account: AccountInstance, targetAccount: AccountInstance) {
const follow = await db.AccountFollow.loadByAccountAndTarget(account.id, targetAccount.id)
if (!follow) throw new Error('Cannot find associated follow.')
follow.set('state', 'accepted')
return follow.save()
}

View File

@ -0,0 +1,105 @@
import { ActivityDelete } from '../../../shared/models/activitypub/activity'
import { getOrCreateAccount } from '../../helpers/activitypub'
import { retryTransactionWrapper } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger'
import { database as db } from '../../initializers'
import { AccountInstance } from '../../models/account/account-interface'
import { VideoChannelInstance } from '../../models/video/video-channel-interface'
import { VideoInstance } from '../../models/video/video-interface'
async function processDeleteActivity (activity: ActivityDelete) {
const account = await getOrCreateAccount(activity.actor)
if (account.url === activity.id) {
return processDeleteAccount(account)
}
{
let videoObject = await db.Video.loadByUrl(activity.id)
if (videoObject !== undefined) {
return processDeleteVideo(account, videoObject)
}
}
{
let videoChannelObject = await db.VideoChannel.loadByUrl(activity.id)
if (videoChannelObject !== undefined) {
return processDeleteVideoChannel(account, videoChannelObject)
}
}
return undefined
}
// ---------------------------------------------------------------------------
export {
processDeleteActivity
}
// ---------------------------------------------------------------------------
async function processDeleteVideo (account: AccountInstance, videoToDelete: VideoInstance) {
const options = {
arguments: [ account, videoToDelete ],
errorMessage: 'Cannot remove the remote video with many retries.'
}
await retryTransactionWrapper(deleteRemoteVideo, options)
}
async function deleteRemoteVideo (account: AccountInstance, videoToDelete: VideoInstance) {
logger.debug('Removing remote video "%s".', videoToDelete.uuid)
await db.sequelize.transaction(async t => {
if (videoToDelete.VideoChannel.Account.id !== account.id) {
throw new Error('Account ' + account.url + ' does not own video channel ' + videoToDelete.VideoChannel.url)
}
await videoToDelete.destroy({ transaction: t })
})
logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
}
async function processDeleteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) {
const options = {
arguments: [ account, videoChannelToRemove ],
errorMessage: 'Cannot remove the remote video channel with many retries.'
}
await retryTransactionWrapper(deleteRemoteVideoChannel, options)
}
async function deleteRemoteVideoChannel (account: AccountInstance, videoChannelToRemove: VideoChannelInstance) {
logger.debug('Removing remote video channel "%s".', videoChannelToRemove.uuid)
await db.sequelize.transaction(async t => {
if (videoChannelToRemove.Account.id !== account.id) {
throw new Error('Account ' + account.url + ' does not own video channel ' + videoChannelToRemove.url)
}
await videoChannelToRemove.destroy({ transaction: t })
})
logger.info('Remote video channel with uuid %s removed.', videoChannelToRemove.uuid)
}
async function processDeleteAccount (accountToRemove: AccountInstance) {
const options = {
arguments: [ accountToRemove ],
errorMessage: 'Cannot remove the remote account with many retries.'
}
await retryTransactionWrapper(deleteRemoteAccount, options)
}
async function deleteRemoteAccount (accountToRemove: AccountInstance) {
logger.debug('Removing remote account "%s".', accountToRemove.uuid)
await db.sequelize.transaction(async t => {
await accountToRemove.destroy({ transaction: t })
})
logger.info('Remote account with uuid %s removed.', accountToRemove.uuid)
}

View File

@ -0,0 +1,32 @@
import { ActivityFollow } from '../../../shared/models/activitypub/activity'
import { getOrCreateAccount } from '../../helpers'
import { database as db } from '../../initializers'
import { AccountInstance } from '../../models/account/account-interface'
async function processFollowActivity (activity: ActivityFollow) {
const activityObject = activity.object
const account = await getOrCreateAccount(activity.actor)
return processFollow(account, activityObject)
}
// ---------------------------------------------------------------------------
export {
processFollowActivity
}
// ---------------------------------------------------------------------------
async function processFollow (account: AccountInstance, targetAccountURL: string) {
const targetAccount = await db.Account.loadByUrl(targetAccountURL)
if (targetAccount === undefined) throw new Error('Unknown account')
if (targetAccount.isOwned() === false) throw new Error('This is not a local account.')
return db.AccountFollow.create({
accountId: account.id,
targetAccountId: targetAccount.id,
state: 'accepted'
})
}

View File

@ -25,8 +25,7 @@ function sendUpdateVideoChannel (videoChannel: VideoChannelInstance, t: Sequeliz
}
function sendDeleteVideoChannel (videoChannel: VideoChannelInstance, t: Sequelize.Transaction) {
const videoChannelObject = videoChannel.toActivityPubObject()
const data = deleteActivityData(videoChannel.url, videoChannel.Account, videoChannelObject)
const data = deleteActivityData(videoChannel.url, videoChannel.Account)
return broadcastToFollowers(data, videoChannel.Account, t)
}
@ -46,12 +45,17 @@ function sendUpdateVideo (video: VideoInstance, t: Sequelize.Transaction) {
}
function sendDeleteVideo (video: VideoInstance, t: Sequelize.Transaction) {
const videoObject = video.toActivityPubObject()
const data = deleteActivityData(video.url, video.VideoChannel.Account, videoObject)
const data = deleteActivityData(video.url, video.VideoChannel.Account)
return broadcastToFollowers(data, video.VideoChannel.Account, t)
}
function sendDeleteAccount (account: AccountInstance, t: Sequelize.Transaction) {
const data = deleteActivityData(account.url, account)
return broadcastToFollowers(data, account, t)
}
// ---------------------------------------------------------------------------
export {
@ -60,13 +64,14 @@ export {
sendDeleteVideoChannel,
sendAddVideo,
sendUpdateVideo,
sendDeleteVideo
sendDeleteVideo,
sendDeleteAccount
}
// ---------------------------------------------------------------------------
async function broadcastToFollowers (data: any, fromAccount: AccountInstance, t: Sequelize.Transaction) {
const result = await db.Account.listFollowerUrlsForApi(fromAccount.name, 0)
const result = await db.Account.listFollowerUrlsForApi(fromAccount.id, 0)
const jobPayload = {
uris: result.data,
@ -114,14 +119,11 @@ async function updateActivityData (url: string, byAccount: AccountInstance, obje
return buildSignedActivity(byAccount, base)
}
async function deleteActivityData (url: string, byAccount: AccountInstance, object: any) {
const to = await getPublicActivityTo(byAccount)
async function deleteActivityData (url: string, byAccount: AccountInstance) {
const base = {
type: 'Update',
type: 'Delete',
id: url,
actor: byAccount.url,
to,
object
actor: byAccount.url
}
return buildSignedActivity(byAccount, base)

View File

@ -34,6 +34,18 @@ function setVideosSort (req: express.Request, res: express.Response, next: expre
return next()
}
function setFollowersSort (req: express.Request, res: express.Response, next: express.NextFunction) {
if (!req.query.sort) req.query.sort = '-createdAt'
return next()
}
function setFollowingSort (req: express.Request, res: express.Response, next: express.NextFunction) {
if (!req.query.sort) req.query.sort = '-createdAt'
return next()
}
function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) {
let newSort: SortType = { sortModel: undefined, sortValue: undefined }
@ -63,5 +75,7 @@ export {
setVideoAbusesSort,
setVideoChannelsSort,
setVideosSort,
setBlacklistSort
setBlacklistSort,
setFollowersSort,
setFollowingSort
}

View File

@ -1,21 +1,20 @@
import { param } from 'express-validator/check'
import * as express from 'express'
import { database as db } from '../../initializers/database'
import { checkErrors } from './utils'
import { param } from 'express-validator/check'
import {
logger,
isUserUsernameValid,
isUserPasswordValid,
isUserVideoQuotaValid,
isUserDisplayNSFWValid,
isUserPasswordValid,
isUserRoleValid,
isAccountNameValid
isUserUsernameValid,
isUserVideoQuotaValid,
logger
} from '../../helpers'
import { isAccountNameWithHostValid } from '../../helpers/custom-validators/video-accounts'
import { database as db } from '../../initializers/database'
import { AccountInstance } from '../../models'
import { checkErrors } from './utils'
const localAccountValidator = [
param('name').custom(isAccountNameValid).withMessage('Should have a valid account name'),
param('nameWithHost').custom(isAccountNameWithHostValid).withMessage('Should have a valid account with domain name (myuser@domain.tld)'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking localAccountValidator parameters', { parameters: req.params })
@ -34,8 +33,10 @@ export {
// ---------------------------------------------------------------------------
function checkLocalAccountExists (name: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) {
db.Account.loadLocalAccountByName(name)
function checkLocalAccountExists (nameWithHost: string, res: express.Response, callback: (err: Error, account: AccountInstance) => void) {
const [ name, host ] = nameWithHost.split('@')
db.Account.loadLocalAccountByNameAndPod(name, host)
.then(account => {
if (!account) {
return res.status(404)

View File

@ -6,29 +6,32 @@ import { logger } from '../../helpers'
import { SORTABLE_COLUMNS } from '../../initializers'
// Initialize constants here for better performances
const SORTABLE_PODS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.PODS)
const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS)
const SORTABLE_FOLLOWERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWERS)
const SORTABLE_FOLLOWING_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.FOLLOWING)
const podsSortValidator = checkSort(SORTABLE_PODS_COLUMNS)
const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS)
const followersSortValidator = checkSort(SORTABLE_FOLLOWERS_COLUMNS)
const followingSortValidator = checkSort(SORTABLE_FOLLOWING_COLUMNS)
// ---------------------------------------------------------------------------
export {
podsSortValidator,
usersSortValidator,
videoAbusesSortValidator,
videoChannelsSortValidator,
videosSortValidator,
blacklistSortValidator
blacklistSortValidator,
followersSortValidator,
followingSortValidator
}
// ---------------------------------------------------------------------------

View File

@ -1,17 +1,19 @@
import * as Sequelize from 'sequelize'
import * as Promise from 'bluebird'
import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
import * as Bluebird from 'bluebird'
import { FollowState } from '../../../shared/models/accounts/follow.model'
export namespace AccountFollowMethods {
export type LoadByAccountAndTarget = (accountId: number, targetAccountId: number) => Bluebird<AccountFollowInstance>
}
export interface AccountFollowClass {
loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget
}
export interface AccountFollowAttributes {
accountId: number
targetAccountId: number
state: FollowState
}
export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> {

View File

@ -1,18 +1,21 @@
import { values } from 'lodash'
import * as Sequelize from 'sequelize'
import { addMethodsToModel } from '../utils'
import {
AccountFollowInstance,
AccountFollowAttributes,
AccountFollowMethods
} from './account-follow-interface'
import { AccountFollowAttributes, AccountFollowInstance, AccountFollowMethods } from './account-follow-interface'
import { FOLLOW_STATES } from '../../initializers/constants'
let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes>
let loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow',
{ },
{
state: {
type: DataTypes.ENUM(values(FOLLOW_STATES)),
allowNull: false
}
},
{
indexes: [
{
@ -43,6 +46,7 @@ function associate (models) {
name: 'accountId',
allowNull: false
},
as: 'followers',
onDelete: 'CASCADE'
})
@ -51,6 +55,18 @@ function associate (models) {
name: 'targetAccountId',
allowNull: false
},
as: 'following',
onDelete: 'CASCADE'
})
}
loadByAccountAndTarget = function (accountId: number, targetAccountId: number) {
const query = {
where: {
accountId,
targetAccountId
}
}
return AccountFollow.findOne(query)
}

View File

@ -1,22 +1,26 @@
import * as Sequelize from 'sequelize'
import * as Bluebird from 'bluebird'
import * as Sequelize from 'sequelize'
import { Account as FormattedAccount, ActivityPubActor } from '../../../shared'
import { ResultList } from '../../../shared/models/result-list.model'
import { PodInstance } from '../pod/pod-interface'
import { VideoChannelInstance } from '../video/video-channel-interface'
import { ActivityPubActor } from '../../../shared'
import { ResultList } from '../../../shared/models/result-list.model'
export namespace AccountMethods {
export type LoadApplication = () => Bluebird<AccountInstance>
export type Load = (id: number) => Bluebird<AccountInstance>
export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
export type LoadByUrl = (url: string) => Bluebird<AccountInstance>
export type LoadAccountByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Bluebird<AccountInstance>
export type LoadLocalAccountByName = (name: string) => Bluebird<AccountInstance>
export type LoadLocalAccountByNameAndPod = (name: string, host: string) => Bluebird<AccountInstance>
export type ListOwned = () => Bluebird<AccountInstance[]>
export type ListFollowerUrlsForApi = (name: string, start: number, count?: number) => Promise< ResultList<string> >
export type ListFollowingUrlsForApi = (name: string, start: number, count?: number) => Promise< ResultList<string> >
export type ListFollowerUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
export type ListFollowingUrlsForApi = (id: number, start: number, count?: number) => Promise< ResultList<string> >
export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> >
export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountInstance> >
export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor
export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount
export type IsOwned = (this: AccountInstance) => boolean
export type GetFollowerSharedInboxUrls = (this: AccountInstance) => Bluebird<string[]>
export type GetFollowingUrl = (this: AccountInstance) => string
@ -25,14 +29,17 @@ export namespace AccountMethods {
}
export interface AccountClass {
loadApplication: AccountMethods.LoadApplication
loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
load: AccountMethods.Load
loadByUUID: AccountMethods.LoadByUUID
loadByUrl: AccountMethods.LoadByUrl
loadLocalAccountByName: AccountMethods.LoadLocalAccountByName
loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod
listOwned: AccountMethods.ListOwned
listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
listFollowingForApi: AccountMethods.ListFollowingForApi
listFollowersForApi: AccountMethods.ListFollowersForApi
}
export interface AccountAttributes {
@ -58,6 +65,7 @@ export interface AccountAttributes {
export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
isOwned: AccountMethods.IsOwned
toActivityPubObject: AccountMethods.ToActivityPubObject
toFormattedJSON: AccountMethods.ToFormattedJSON
getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
getFollowingUrl: AccountMethods.GetFollowingUrl
getFollowersUrl: AccountMethods.GetFollowersUrl

View File

@ -15,25 +15,31 @@ import {
activityPubContextify
} from '../../helpers'
import { addMethodsToModel } from '../utils'
import { addMethodsToModel, getSort } from '../utils'
import {
AccountInstance,
AccountAttributes,
AccountMethods
} from './account-interface'
import LoadApplication = AccountMethods.LoadApplication
import { sendDeleteAccount } from '../../lib/activitypub/send-request'
let Account: Sequelize.Model<AccountInstance, AccountAttributes>
let loadAccountByPodAndUUID: AccountMethods.LoadAccountByPodAndUUID
let load: AccountMethods.Load
let loadApplication: AccountMethods.LoadApplication
let loadByUUID: AccountMethods.LoadByUUID
let loadByUrl: AccountMethods.LoadByUrl
let loadLocalAccountByName: AccountMethods.LoadLocalAccountByName
let loadLocalAccountByNameAndPod: AccountMethods.LoadLocalAccountByNameAndPod
let listOwned: AccountMethods.ListOwned
let listFollowerUrlsForApi: AccountMethods.ListFollowerUrlsForApi
let listFollowingUrlsForApi: AccountMethods.ListFollowingUrlsForApi
let listFollowingForApi: AccountMethods.ListFollowingForApi
let listFollowersForApi: AccountMethods.ListFollowersForApi
let isOwned: AccountMethods.IsOwned
let toActivityPubObject: AccountMethods.ToActivityPubObject
let toFormattedJSON: AccountMethods.ToFormattedJSON
let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
let getFollowingUrl: AccountMethods.GetFollowingUrl
let getFollowersUrl: AccountMethods.GetFollowersUrl
@ -189,16 +195,20 @@ export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes
const classMethods = [
associate,
loadAccountByPodAndUUID,
loadApplication,
load,
loadByUUID,
loadLocalAccountByName,
loadLocalAccountByNameAndPod,
listOwned,
listFollowerUrlsForApi,
listFollowingUrlsForApi
listFollowingUrlsForApi,
listFollowingForApi,
listFollowersForApi
]
const instanceMethods = [
isOwned,
toActivityPubObject,
toFormattedJSON,
getFollowerSharedInboxUrls,
getFollowingUrl,
getFollowersUrl,
@ -250,6 +260,7 @@ function associate (models) {
name: 'accountId',
allowNull: false
},
as: 'following',
onDelete: 'cascade'
})
@ -258,23 +269,29 @@ function associate (models) {
name: 'targetAccountId',
allowNull: false
},
as: 'followers',
onDelete: 'cascade'
})
}
function afterDestroy (account: AccountInstance) {
if (account.isOwned()) {
const removeVideoAccountToFriendsParams = {
uuid: account.uuid
}
// FIXME: remove account in followers
// return removeVideoAccountToFriends(removeVideoAccountToFriendsParams)
return sendDeleteAccount(account, undefined)
}
return undefined
}
toFormattedJSON = function (this: AccountInstance) {
const json = {
id: this.id,
host: this.Pod.host,
name: this.name
}
return json
}
toActivityPubObject = function (this: AccountInstance) {
const type = this.podId ? 'Application' as 'Application' : 'Person' as 'Person'
@ -347,12 +364,85 @@ listOwned = function () {
return Account.findAll(query)
}
listFollowerUrlsForApi = function (name: string, start: number, count?: number) {
return createListFollowForApiQuery('followers', name, start, count)
listFollowerUrlsForApi = function (id: number, start: number, count?: number) {
return createListFollowForApiQuery('followers', id, start, count)
}
listFollowingUrlsForApi = function (name: string, start: number, count?: number) {
return createListFollowForApiQuery('following', name, 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
}
})
}
loadApplication = function () {
return Account.findOne({
include: [
{
model: Account['sequelize'].model.Application,
required: true
}
]
})
}
load = function (id: number) {
@ -369,14 +459,22 @@ loadByUUID = function (uuid: string) {
return Account.findOne(query)
}
loadLocalAccountByName = function (name: string) {
loadLocalAccountByNameAndPod = function (name: string, host: string) {
const query: Sequelize.FindOptions<AccountAttributes> = {
where: {
name,
userId: {
[Sequelize.Op.ne]: null
}
}
},
include: [
{
model: Account['sequelize'].models.Pod,
where: {
host
}
}
]
}
return Account.findOne(query)
@ -406,7 +504,7 @@ loadAccountByPodAndUUID = function (uuid: string, podId: number, transaction: Se
// ------------------------------ UTILS ------------------------------
async function createListFollowForApiQuery (type: 'followers' | 'following', name: string, start: number, count?: number) {
async function createListFollowForApiQuery (type: 'followers' | 'following', id: number, start: number, count?: number) {
let firstJoin: string
let secondJoin: string
@ -424,14 +522,14 @@ async function createListFollowForApiQuery (type: 'followers' | 'following', nam
for (const selection of selections) {
let query = 'SELECT ' + selection + ' FROM "Account" ' +
'INNER JOIN "AccountFollower" ON "AccountFollower"."' + firstJoin + '" = "Account"."id" ' +
'INNER JOIN "Account" AS "Followers" ON "Followers"."id" = "AccountFollower"."' + secondJoin + '" ' +
'WHERE "Account"."name" = \'$name\' ' +
'INNER JOIN "Account" AS "Follows" ON "Followers"."id" = "Follows"."' + secondJoin + '" ' +
'WHERE "Account"."id" = $id ' +
'LIMIT ' + start
if (count !== undefined) query += ', ' + count
const options = {
bind: { name },
bind: { id },
type: Sequelize.QueryTypes.SELECT
}
tasks.push(Account['sequelize'].query(query, options))

View File

@ -9,6 +9,7 @@ import {
VideoChannelMethods
} from './video-channel-interface'
import { sendDeleteVideoChannel } from '../../lib/activitypub/send-request'
let VideoChannel: Sequelize.Model<VideoChannelInstance, VideoChannelAttributes>
let toFormattedJSON: VideoChannelMethods.ToFormattedJSON
@ -176,11 +177,7 @@ function associate (models) {
function afterDestroy (videoChannel: VideoChannelInstance) {
if (videoChannel.isOwned()) {
const removeVideoChannelToFriendsParams = {
uuid: videoChannel.uuid
}
// FIXME: send remove event to followers
return sendDeleteVideoChannel(videoChannel, undefined)
}
return undefined

View File

@ -1,19 +1,12 @@
import * as Sequelize from 'sequelize'
import * as Bluebird from 'bluebird'
import * as Sequelize from 'sequelize'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
import { ResultList } from '../../../shared/models/result-list.model'
import { Video as FormattedVideo, VideoDetails as FormattedDetailsVideo } from '../../../shared/models/videos/video.model'
import { TagAttributes, TagInstance } from './tag-interface'
import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
// Don't use barrel, import just what we need
import {
Video as FormattedVideo,
VideoDetails as FormattedDetailsVideo
} from '../../../shared/models/videos/video.model'
import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model'
import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model'
import { ResultList } from '../../../shared/models/result-list.model'
import { VideoChannelInstance } from './video-channel-interface'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects/video-torrent-object'
import { VideoFileAttributes, VideoFileInstance } from './video-file-interface'
export namespace VideoMethods {
export type GetThumbnailName = (this: VideoInstance) => string

View File

@ -45,6 +45,7 @@ import { addMethodsToModel, getSort } from '../utils'
import { TagInstance } from './tag-interface'
import { VideoFileInstance, VideoFileModel } from './video-file-interface'
import { VideoAttributes, VideoInstance, VideoMethods } from './video-interface'
import { sendDeleteVideo } from '../../lib/activitypub/send-request'
const Buffer = safeBuffer.Buffer
@ -363,13 +364,9 @@ function afterDestroy (video: VideoInstance) {
)
if (video.isOwned()) {
const removeVideoToFriendsParams = {
uuid: video.uuid
}
tasks.push(
video.removePreview()
// FIXME: remove video for followers
video.removePreview(),
sendDeleteVideo(video, undefined)
)
// Remove physical files and torrents

View File

@ -3,7 +3,6 @@ import * as program from 'commander'
// /!\ Before imports /!\
process.env.NODE_ENV = 'test'
import { REQUESTS_INTERVAL } from '../../initializers/constants'
import { Video, VideoRateType, VideoFile } from '../../../shared'
import {
ServerInfo as DefaultServerInfo,
@ -137,7 +136,7 @@ async function start () {
initializeRequestsPerServer(servers)
checking = false
clearInterval(waitingInterval)
}, REQUESTS_INTERVAL)
}, 10000)
}, integrityInterval)
}

View File

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

View File

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

View File

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

View File

@ -4,10 +4,11 @@ import {
} from './objects'
import { ActivityPubSignature } from './activitypub-signature'
export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | ActivityFlag
export type Activity = ActivityCreate | ActivityAdd | ActivityUpdate | ActivityFlag |
ActivityDelete | ActivityFollow | ActivityAccept
// Flag -> report abuse
export type ActivityType = 'Create' | 'Add' | 'Update' | 'Flag'
export type ActivityType = 'Create' | 'Add' | 'Update' | 'Flag' | 'Delete' | 'Follow' | 'Accept'
export interface BaseActivity {
'@context'?: any[]
@ -37,3 +38,16 @@ export interface ActivityFlag extends BaseActivity {
type: 'Flag'
object: string
}
export interface ActivityDelete extends BaseActivity {
type: 'Delete'
}
export interface ActivityFollow extends BaseActivity {
type: 'Follow'
object: string
}
export interface ActivityAccept extends BaseActivity {
type: 'Accept'
}

View File

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