1 import * as Bluebird from 'bluebird'
2 import { values } from 'lodash'
3 import * as Sequelize from 'sequelize'
20 } from 'sequelize-typescript'
21 import { FollowState } from '../../../shared/models/actors'
22 import { AccountFollow } from '../../../shared/models/actors/follow.model'
23 import { logger } from '../../helpers/logger'
24 import { getServerActor } from '../../helpers/utils'
25 import { ACTOR_FOLLOW_SCORE } from '../../initializers'
26 import { FOLLOW_STATES } from '../../initializers/constants'
27 import { ServerModel } from '../server/server'
28 import { getSort } from '../utils'
29 import { ActorModel } from './actor'
30 import { VideoChannelModel } from '../video/video-channel'
33 tableName: 'actorFollow',
39 fields: [ 'targetActorId' ]
42 fields: [ 'actorId', 'targetActorId' ],
50 export class ActorFollowModel extends Model<ActorFollowModel> {
53 @Column(DataType.ENUM(values(FOLLOW_STATES)))
57 @Default(ACTOR_FOLLOW_SCORE.BASE)
59 @Max(ACTOR_FOLLOW_SCORE.MAX)
69 @ForeignKey(() => ActorModel)
73 @BelongsTo(() => ActorModel, {
81 ActorFollower: ActorModel
83 @ForeignKey(() => ActorModel)
87 @BelongsTo(() => ActorModel, {
89 name: 'targetActorId',
95 ActorFollowing: ActorModel
99 static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
100 if (instance.state !== 'accepted') return undefined
103 ActorModel.incrementFollows(instance.actorId, 'followingCount', 1),
104 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', 1)
109 static decrementFollowerAndFollowingCount (instance: ActorFollowModel) {
111 ActorModel.incrementFollows(instance.actorId, 'followingCount',-1),
112 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', -1)
116 // Remove actor follows with a score of 0 (too many requests where they were unreachable)
117 static async removeBadActorFollows () {
118 const actorFollows = await ActorFollowModel.listBadActorFollows()
120 const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
121 await Promise.all(actorFollowsRemovePromises)
123 const numberOfActorFollowsRemoved = actorFollows.length
125 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
128 static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction | undefined) {
129 if (goodInboxes.length === 0 && badInboxes.length === 0) return
131 logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
133 if (goodInboxes.length !== 0) {
134 ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
135 .catch(err => logger.error('Cannot increment scores of good actor follows.', { err }))
138 if (badInboxes.length !== 0) {
139 ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
140 .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err }))
144 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
148 targetActorId: targetActorId
165 return ActorFollowModel.findOne(query)
168 static loadByActorAndTargetNameAndHost (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) {
169 const actorFollowingPartInclude = {
172 as: 'ActorFollowing',
174 preferredUsername: targetName
178 if (targetHost === null) {
179 actorFollowingPartInclude.where['serverId'] = null
181 Object.assign(actorFollowingPartInclude, {
204 actorFollowingPartInclude
209 return ActorFollowModel.findOne(query)
212 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
217 order: getSort(sort),
229 as: 'ActorFollowing',
231 include: [ ServerModel ]
236 return ActorFollowModel.findAndCountAll(query)
237 .then(({ rows, count }) => {
245 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
250 order: getSort(sort),
257 as: 'ActorFollowing',
261 model: VideoChannelModel,
269 return ActorFollowModel.findAndCountAll(query)
270 .then(({ rows, count }) => {
272 data: rows.map(r => r.ActorFollowing.VideoChannel),
278 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
283 order: getSort(sort),
289 include: [ ServerModel ]
293 as: 'ActorFollowing',
302 return ActorFollowModel.findAndCountAll(query)
303 .then(({ rows, count }) => {
311 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
312 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
315 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
316 return ActorFollowModel.createListAcceptedFollowForApiQuery(
327 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
328 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
331 static async getStats () {
332 const serverActor = await getServerActor()
334 const totalInstanceFollowing = await ActorFollowModel.count({
336 actorId: serverActor.id
340 const totalInstanceFollowers = await ActorFollowModel.count({
342 targetActorId: serverActor.id
347 totalInstanceFollowing,
348 totalInstanceFollowers
352 private static async createListAcceptedFollowForApiQuery (
353 type: 'followers' | 'following',
355 t: Sequelize.Transaction,
361 let firstJoin: string
362 let secondJoin: string
364 if (type === 'followers') {
365 firstJoin = 'targetActorId'
366 secondJoin = 'actorId'
368 firstJoin = 'actorId'
369 secondJoin = 'targetActorId'
372 const selections: string[] = []
373 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
374 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
376 selections.push('COUNT(*) AS "total"')
378 const tasks: Bluebird<any>[] = []
380 for (let selection of selections) {
381 let query = 'SELECT ' + selection + ' FROM "actor" ' +
382 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
383 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
384 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
386 if (count !== undefined) query += 'LIMIT ' + count
387 if (start !== undefined) query += ' OFFSET ' + start
391 type: Sequelize.QueryTypes.SELECT,
394 tasks.push(ActorFollowModel.sequelize.query(query, options))
397 const [ followers, [ { total } ] ] = await Promise.all(tasks)
398 const urls: string[] = followers.map(f => f.url)
402 total: parseInt(total, 10)
406 private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) {
407 const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
409 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
411 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
412 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
413 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
416 const options = t ? {
417 type: Sequelize.QueryTypes.BULKUPDATE,
421 return ActorFollowModel.sequelize.query(query, options)
424 private static listBadActorFollows () {
428 [Sequelize.Op.lte]: 0
434 return ActorFollowModel.findAll(query)
437 toFormattedJSON (): AccountFollow {
438 const follower = this.ActorFollower.toFormattedJSON()
439 const following = this.ActorFollowing.toFormattedJSON()
447 createdAt: this.createdAt,
448 updatedAt: this.updatedAt