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 { ActorFollow } 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, unusedActorAttributesForAPI } from './actor'
30 import { VideoChannelModel } from '../video/video-channel'
31 import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions'
32 import { AccountModel } from '../account/account'
35 tableName: 'actorFollow',
41 fields: [ 'targetActorId' ]
44 fields: [ 'actorId', 'targetActorId' ],
52 export class ActorFollowModel extends Model<ActorFollowModel> {
55 @Column(DataType.ENUM(values(FOLLOW_STATES)))
59 @Default(ACTOR_FOLLOW_SCORE.BASE)
61 @Max(ACTOR_FOLLOW_SCORE.MAX)
71 @ForeignKey(() => ActorModel)
75 @BelongsTo(() => ActorModel, {
83 ActorFollower: ActorModel
85 @ForeignKey(() => ActorModel)
89 @BelongsTo(() => ActorModel, {
91 name: 'targetActorId',
97 ActorFollowing: ActorModel
101 static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
102 if (instance.state !== 'accepted') return undefined
105 ActorModel.incrementFollows(instance.actorId, 'followingCount', 1),
106 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', 1)
111 static decrementFollowerAndFollowingCount (instance: ActorFollowModel) {
113 ActorModel.incrementFollows(instance.actorId, 'followingCount',-1),
114 ActorModel.incrementFollows(instance.targetActorId, 'followersCount', -1)
118 // Remove actor follows with a score of 0 (too many requests where they were unreachable)
119 static async removeBadActorFollows () {
120 const actorFollows = await ActorFollowModel.listBadActorFollows()
122 const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
123 await Promise.all(actorFollowsRemovePromises)
125 const numberOfActorFollowsRemoved = actorFollows.length
127 if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
130 static updateActorFollowsScore (goodInboxes: string[], badInboxes: string[], t: Sequelize.Transaction | undefined) {
131 if (goodInboxes.length === 0 && badInboxes.length === 0) return
133 logger.info('Updating %d good actor follows and %d bad actor follows scores.', goodInboxes.length, badInboxes.length)
135 if (goodInboxes.length !== 0) {
136 ActorFollowModel.incrementScores(goodInboxes, ACTOR_FOLLOW_SCORE.BONUS, t)
137 .catch(err => logger.error('Cannot increment scores of good actor follows.', { err }))
140 if (badInboxes.length !== 0) {
141 ActorFollowModel.incrementScores(badInboxes, ACTOR_FOLLOW_SCORE.PENALTY, t)
142 .catch(err => logger.error('Cannot decrement scores of bad actor follows.', { err }))
146 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
150 targetActorId: targetActorId
167 return ActorFollowModel.findOne(query)
170 static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) {
171 const actorFollowingPartInclude: IIncludeOptions = {
174 as: 'ActorFollowing',
176 preferredUsername: targetName
180 model: VideoChannelModel.unscoped(),
186 if (targetHost === null) {
187 actorFollowingPartInclude.where['serverId'] = null
189 actorFollowingPartInclude.include.push({
203 actorFollowingPartInclude,
213 return ActorFollowModel.findOne(query)
215 if (result && result.ActorFollowing.VideoChannel) {
216 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
223 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]) {
224 const whereTab = targets
228 [ Sequelize.Op.and ]: [
230 '$preferredUsername$': t.name
240 [ Sequelize.Op.and ]: [
242 '$preferredUsername$': t.name
254 [ Sequelize.Op.and ]: [
256 [ Sequelize.Op.or ]: whereTab
265 attributes: [ 'preferredUsername' ],
266 model: ActorModel.unscoped(),
268 as: 'ActorFollowing',
271 attributes: [ 'host' ],
272 model: ServerModel.unscoped(),
280 return ActorFollowModel.findAll(query)
283 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
288 order: getSort(sort),
300 as: 'ActorFollowing',
302 include: [ ServerModel ]
307 return ActorFollowModel.findAndCountAll(query)
308 .then(({ rows, count }) => {
316 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
322 order: getSort(sort),
328 attributes: [ 'id' ],
329 model: ActorModel.unscoped(),
330 as: 'ActorFollowing',
334 model: VideoChannelModel.unscoped(),
339 exclude: unusedActorAttributesForAPI
345 model: AccountModel.unscoped(),
350 exclude: unusedActorAttributesForAPI
364 return ActorFollowModel.findAndCountAll(query)
365 .then(({ rows, count }) => {
367 data: rows.map(r => r.ActorFollowing.VideoChannel),
373 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
378 order: getSort(sort),
384 include: [ ServerModel ]
388 as: 'ActorFollowing',
397 return ActorFollowModel.findAndCountAll(query)
398 .then(({ rows, count }) => {
406 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
407 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
410 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
411 return ActorFollowModel.createListAcceptedFollowForApiQuery(
422 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
423 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
426 static async getStats () {
427 const serverActor = await getServerActor()
429 const totalInstanceFollowing = await ActorFollowModel.count({
431 actorId: serverActor.id
435 const totalInstanceFollowers = await ActorFollowModel.count({
437 targetActorId: serverActor.id
442 totalInstanceFollowing,
443 totalInstanceFollowers
447 private static async createListAcceptedFollowForApiQuery (
448 type: 'followers' | 'following',
450 t: Sequelize.Transaction,
456 let firstJoin: string
457 let secondJoin: string
459 if (type === 'followers') {
460 firstJoin = 'targetActorId'
461 secondJoin = 'actorId'
463 firstJoin = 'actorId'
464 secondJoin = 'targetActorId'
467 const selections: string[] = []
468 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
469 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
471 selections.push('COUNT(*) AS "total"')
473 const tasks: Bluebird<any>[] = []
475 for (let selection of selections) {
476 let query = 'SELECT ' + selection + ' FROM "actor" ' +
477 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
478 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
479 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
481 if (count !== undefined) query += 'LIMIT ' + count
482 if (start !== undefined) query += ' OFFSET ' + start
486 type: Sequelize.QueryTypes.SELECT,
489 tasks.push(ActorFollowModel.sequelize.query(query, options))
492 const [ followers, [ { total } ] ] = await Promise.all(tasks)
493 const urls: string[] = followers.map(f => f.url)
497 total: parseInt(total, 10)
501 private static incrementScores (inboxUrls: string[], value: number, t: Sequelize.Transaction | undefined) {
502 const inboxUrlsString = inboxUrls.map(url => `'${url}'`).join(',')
504 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
506 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
507 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
508 'WHERE "actor"."inboxUrl" IN (' + inboxUrlsString + ') OR "actor"."sharedInboxUrl" IN (' + inboxUrlsString + ')' +
511 const options = t ? {
512 type: Sequelize.QueryTypes.BULKUPDATE,
516 return ActorFollowModel.sequelize.query(query, options)
519 private static listBadActorFollows () {
523 [Sequelize.Op.lte]: 0
529 return ActorFollowModel.findAll(query)
532 toFormattedJSON (): ActorFollow {
533 const follower = this.ActorFollower.toFormattedJSON()
534 const following = this.ActorFollowing.toFormattedJSON()
542 createdAt: this.createdAt,
543 updatedAt: this.updatedAt