1 import * as Bluebird from 'bluebird'
2 import { values } from 'lodash'
3 import * as Sequelize from 'sequelize'
5 AfterCreate, AfterDestroy, AfterUpdate, AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model,
7 } from 'sequelize-typescript'
8 import { FollowState } from '../../../shared/models/actors'
9 import { AccountFollow } from '../../../shared/models/actors/follow.model'
10 import { logger } from '../../helpers/logger'
11 import { ACTOR_FOLLOW_SCORE } from '../../initializers'
12 import { FOLLOW_STATES } from '../../initializers/constants'
13 import { ServerModel } from '../server/server'
14 import { getSort } from '../utils'
15 import { ActorModel } from './actor'
18 tableName: 'actorFollow',
24 fields: [ 'targetActorId' ]
27 fields: [ 'actorId', 'targetActorId' ],
35 export class ActorFollowModel extends Model<ActorFollowModel> {
38 @Column(DataType.ENUM(values(FOLLOW_STATES)))
42 @Default(ACTOR_FOLLOW_SCORE.BASE)
44 @Max(ACTOR_FOLLOW_SCORE.MAX)
54 @ForeignKey(() => ActorModel)
58 @BelongsTo(() => ActorModel, {
66 ActorFollower: ActorModel
68 @ForeignKey(() => ActorModel)
72 @BelongsTo(() => ActorModel, {
74 name: 'targetActorId',
80 ActorFollowing: ActorModel
84 static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
85 if (instance.state !== 'accepted') return undefined
88 ActorModel.incrementFollows(instance.actorId, 'followingCount', 1),
89 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', 1)
94 static decrementFollowerAndFollowingCount (instance: ActorFollowModel) {
96 ActorModel.incrementFollows(instance.actorId, 'followingCount',-1),
97 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', -1)
101 // Remove actor follows with a score of 0 (too many requests where they were unreachable)
102 static async removeBadActorFollows () {
103 const actorFollows = await ActorFollowModel.listBadActorFollows()
105 const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
106 await Promise.all(actorFollowsRemovePromises)
108 const numberOfActorFollowsRemoved = actorFollows.length
110 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
113 static updateActorFollowsScoreAndRemoveBadOnes (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction) {
114 if (goodInboxes.length === 0 && badInboxes.length === 0) return
116 logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
118 if (goodInboxes.length !== 0) {
119 ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
120 .catch(err => logger.error('Cannot increment scores of good actor follows.', err))
123 if (badInboxes.length !== 0) {
124 ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
125 .catch(err => logger.error('Cannot decrement scores of bad actor follows.', err))
129 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
133 targetActorId: targetActorId
150 return ActorFollowModel.findOne(query)
153 static loadByActorAndTargetHost (actorId: number, targetHost: string, t?: Sequelize.Transaction) {
167 as: 'ActorFollowing',
182 return ActorFollowModel.findOne(query)
185 static loadByFollowerInbox (url: string, t?: Sequelize.Transaction) {
208 } as any // FIXME: typings does not work
210 return ActorFollowModel.findOne(query)
213 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
218 order: [ getSort(sort) ],
230 as: 'ActorFollowing',
232 include: [ ServerModel ]
237 return ActorFollowModel.findAndCountAll(query)
238 .then(({ rows, count }) => {
246 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
251 order: [ getSort(sort) ],
257 include: [ ServerModel ]
261 as: 'ActorFollowing',
270 return ActorFollowModel.findAndCountAll(query)
271 .then(({ rows, count }) => {
279 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
280 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
283 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
284 return ActorFollowModel.createListAcceptedFollowForApiQuery(
295 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
296 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
299 private static async createListAcceptedFollowForApiQuery (
300 type: 'followers' | 'following',
302 t: Sequelize.Transaction,
308 let firstJoin: string
309 let secondJoin: string
311 if (type === 'followers') {
312 firstJoin = 'targetActorId'
313 secondJoin = 'actorId'
315 firstJoin = 'actorId'
316 secondJoin = 'targetActorId'
319 const selections: string[] = []
320 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
321 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
323 selections.push('COUNT(*) AS "total"')
325 const tasks: Bluebird<any>[] = []
327 for (let selection of selections) {
328 let query = 'SELECT ' + selection + ' FROM "actor" ' +
329 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
330 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
331 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
333 if (count !== undefined) query += 'LIMIT ' + count
334 if (start !== undefined) query += ' OFFSET ' + start
338 type: Sequelize.QueryTypes.SELECT,
341 tasks.push(ActorFollowModel.sequelize.query(query, options))
344 const [ followers, [ { total } ] ] = await
346 const urls: string[] = followers.map(f => f.url)
350 total: parseInt(total, 10)
354 private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction) {
355 const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
357 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
359 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
360 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
361 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
365 type: Sequelize.QueryTypes.BULKUPDATE,
369 return ActorFollowModel.sequelize.query(query, options)
372 private static listBadActorFollows () {
376 [Sequelize.Op.lte]: 0
382 return ActorFollowModel.findAll(query)
385 toFormattedJSON (): AccountFollow {
386 const follower = this.ActorFollower.toFormattedJSON()
387 const following = this.ActorFollowing.toFormattedJSON()
395 createdAt: this.createdAt,
396 updatedAt: this.updatedAt