import * as Bluebird from 'bluebird'
import { values } from 'lodash'
import * as Sequelize from 'sequelize'
-import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import {
+ AfterCreate, AfterDestroy, AfterUpdate, AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model,
+ Table, UpdatedAt
+} from 'sequelize-typescript'
import { FollowState } from '../../../shared/models/actors'
+import { AccountFollow } from '../../../shared/models/actors/follow.model'
+import { logger } from '../../helpers/logger'
+import { getServerActor } from '../../helpers/utils'
+import { ACTOR_FOLLOW_SCORE } from '../../initializers'
import { FOLLOW_STATES } from '../../initializers/constants'
import { ServerModel } from '../server/server'
import { getSort } from '../utils'
{
fields: [ 'actorId', 'targetActorId' ],
unique: true
+ },
+ {
+ fields: [ 'score' ]
}
]
})
@Column(DataType.ENUM(values(FOLLOW_STATES)))
state: FollowState
+ @AllowNull(false)
+ @Default(ACTOR_FOLLOW_SCORE.BASE)
+ @IsInt
+ @Max(ACTOR_FOLLOW_SCORE.MAX)
+ @Column
+ score: number
+
@CreatedAt
createdAt: Date
})
ActorFollowing: ActorModel
+ @AfterCreate
+ @AfterUpdate
+ static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
+ if (instance.state !== 'accepted') return undefined
+
+ return Promise.all([
+ ActorModel.incrementFollows(instance.actorId, 'followingCount', 1),
+ ActorModel.incrementFollows(instance.targetActorId, 'followersCount', 1)
+ ])
+ }
+
+ @AfterDestroy
+ static decrementFollowerAndFollowingCount (instance: ActorFollowModel) {
+ return Promise.all([
+ ActorModel.incrementFollows(instance.actorId, 'followingCount',-1),
+ ActorModel.incrementFollows(instance.targetActorId, 'followersCount', -1)
+ ])
+ }
+
+ // Remove actor follows with a score of 0 (too many requests where they were unreachable)
+ static async removeBadActorFollows () {
+ const actorFollows = await ActorFollowModel.listBadActorFollows()
+
+ const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
+ await Promise.all(actorFollowsRemovePromises)
+
+ const numberOfActorFollowsRemoved = actorFollows.length
+
+ if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
+ }
+
+ static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction) {
+ if (goodInboxes.length === 0 && badInboxes.length === 0) return
+
+ logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
+
+ if (goodInboxes.length !== 0) {
+ ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
+ .catch(err => logger.error('Cannot increment scores of good actor follows.', { err }))
+ }
+
+ if (badInboxes.length !== 0) {
+ ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
+ .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err }))
+ }
+ }
+
static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
const query = {
where: {
distinct: true,
offset: start,
limit: count,
- order: [ getSort(sort) ],
+ order: getSort(sort),
include: [
{
model: ActorModel,
distinct: true,
offset: start,
limit: count,
- order: [ getSort(sort) ],
+ order: getSort(sort),
include: [
{
model: ActorModel,
static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
return ActorFollowModel.createListAcceptedFollowForApiQuery(
- 'DISTINCT(followers)',
+ 'followers',
actorIds,
t,
undefined,
undefined,
- 'sharedInboxUrl'
+ 'sharedInboxUrl',
+ true
)
}
return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
}
- private static async createListAcceptedFollowForApiQuery (type: 'followers' | 'following' | 'DISTINCT(followers)',
- actorIds: number[],
- t: Sequelize.Transaction,
- start?: number,
- count?: number,
- columnUrl = 'url') {
+ static async getStats () {
+ const serverActor = await getServerActor()
+
+ const totalInstanceFollowing = await ActorFollowModel.count({
+ where: {
+ actorId: serverActor.id
+ }
+ })
+
+ const totalInstanceFollowers = await ActorFollowModel.count({
+ where: {
+ targetActorId: serverActor.id
+ }
+ })
+
+ return {
+ totalInstanceFollowing,
+ totalInstanceFollowers
+ }
+ }
+
+ private static async createListAcceptedFollowForApiQuery (
+ type: 'followers' | 'following',
+ actorIds: number[],
+ t: Sequelize.Transaction,
+ start?: number,
+ count?: number,
+ columnUrl = 'url',
+ distinct = false
+ ) {
let firstJoin: string
let secondJoin: string
secondJoin = 'targetActorId'
}
- const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ]
+ const selections: string[] = []
+ if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
+ else selections.push('"Follows"."' + columnUrl + '" AS "url"')
+
+ selections.push('COUNT(*) AS "total"')
+
const tasks: Bluebird<any>[] = []
- for (const selection of selections) {
+ for (let selection of selections) {
let query = 'SELECT ' + selection + ' FROM "actor" ' +
'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
tasks.push(ActorFollowModel.sequelize.query(query, options))
}
- const [ followers, [ { total } ] ] = await
- Promise.all(tasks)
+ const [ followers, [ { total } ] ] = await Promise.all(tasks)
const urls: string[] = followers.map(f => f.url)
return {
}
}
- toFormattedJSON () {
+ private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction) {
+ const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
+
+ const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
+ 'WHERE id IN (' +
+ 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
+ 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
+ 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
+ ')'
+
+ const options = {
+ type: Sequelize.QueryTypes.BULKUPDATE,
+ transaction: t
+ }
+
+ return ActorFollowModel.sequelize.query(query, options)
+ }
+
+ private static listBadActorFollows () {
+ const query = {
+ where: {
+ score: {
+ [Sequelize.Op.lte]: 0
+ }
+ },
+ logging: false
+ }
+
+ return ActorFollowModel.findAll(query)
+ }
+
+ toFormattedJSON (): AccountFollow {
const follower = this.ActorFollower.toFormattedJSON()
const following = this.ActorFollowing.toFormattedJSON()
id: this.id,
follower,
following,
+ score: this.score,
state: this.state,
createdAt: this.createdAt,
updatedAt: this.updatedAt