1 import * as Bluebird from 'bluebird'
2 import { values } from 'lodash'
3 import * as Sequelize from 'sequelize'
5 AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IsInt, Max, Model, Table,
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
82 // Remove actor follows with a score of 0 (too many requests where they were unreachable)
83 static async removeBadActorFollows () {
84 const actorFollows = await ActorFollowModel.listBadActorFollows()
86 const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
87 await Promise.all(actorFollowsRemovePromises)
89 const numberOfActorFollowsRemoved = actorFollows.length
91 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
94 static updateActorFollowsScoreAndRemoveBadOnes (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction) {
95 if (goodInboxes.length === 0 && badInboxes.length === 0) return
97 logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
99 if (goodInboxes.length !== 0) {
100 ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
101 .catch(err => logger.error('Cannot increment scores of good actor follows.', err))
104 if (badInboxes.length !== 0) {
105 ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
106 .catch(err => logger.error('Cannot decrement scores of bad actor follows.', err))
110 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
114 targetActorId: targetActorId
131 return ActorFollowModel.findOne(query)
134 static loadByActorAndTargetHost (actorId: number, targetHost: string, t?: Sequelize.Transaction) {
148 as: 'ActorFollowing',
163 return ActorFollowModel.findOne(query)
166 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
171 order: [ getSort(sort) ],
183 as: 'ActorFollowing',
185 include: [ ServerModel ]
190 return ActorFollowModel.findAndCountAll(query)
191 .then(({ rows, count }) => {
199 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
204 order: [ getSort(sort) ],
210 include: [ ServerModel ]
214 as: 'ActorFollowing',
223 return ActorFollowModel.findAndCountAll(query)
224 .then(({ rows, count }) => {
232 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
233 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
236 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
237 return ActorFollowModel.createListAcceptedFollowForApiQuery(
248 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
249 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
252 private static async createListAcceptedFollowForApiQuery (
253 type: 'followers' | 'following',
255 t: Sequelize.Transaction,
261 let firstJoin: string
262 let secondJoin: string
264 if (type === 'followers') {
265 firstJoin = 'targetActorId'
266 secondJoin = 'actorId'
268 firstJoin = 'actorId'
269 secondJoin = 'targetActorId'
272 const selections: string[] = []
273 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
274 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
276 selections.push('COUNT(*) AS "total"')
278 const tasks: Bluebird<any>[] = []
280 for (let selection of selections) {
281 let query = 'SELECT ' + selection + ' FROM "actor" ' +
282 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
283 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
284 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
286 if (count !== undefined) query += 'LIMIT ' + count
287 if (start !== undefined) query += ' OFFSET ' + start
291 type: Sequelize.QueryTypes.SELECT,
294 tasks.push(ActorFollowModel.sequelize.query(query, options))
297 const [ followers, [ { total } ] ] = await
299 const urls: string[] = followers.map(f => f.url)
303 total: parseInt(total, 10)
307 private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction) {
308 const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
310 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
312 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
313 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
314 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
318 type: Sequelize.QueryTypes.BULKUPDATE,
322 return ActorFollowModel.sequelize.query(query, options)
325 private static listBadActorFollows () {
329 [Sequelize.Op.lte]: 0
334 return ActorFollowModel.findAll(query)
337 toFormattedJSON (): AccountFollow {
338 const follower = this.ActorFollower.toFormattedJSON()
339 const following = this.ActorFollowing.toFormattedJSON()
347 createdAt: this.createdAt,
348 updatedAt: this.updatedAt