1 import * as Bluebird from 'bluebird'
2 import { values } from 'lodash'
19 } from 'sequelize-typescript'
20 import { FollowState } from '../../../shared/models/actors'
21 import { ActorFollow } from '../../../shared/models/actors/follow.model'
22 import { logger } from '../../helpers/logger'
23 import { getServerActor } from '../../helpers/utils'
24 import { ACTOR_FOLLOW_SCORE, FOLLOW_STATES } from '../../initializers/constants'
25 import { ServerModel } from '../server/server'
26 import { getSort } from '../utils'
27 import { ActorModel, unusedActorAttributesForAPI } from './actor'
28 import { VideoChannelModel } from '../video/video-channel'
29 import { AccountModel } from '../account/account'
30 import { IncludeOptions, Op, Transaction, QueryTypes } from 'sequelize'
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 loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction) {
132 targetActorId: targetActorId
149 return ActorFollowModel.findOne(query)
152 static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Transaction) {
153 const actorFollowingPartInclude: IncludeOptions = {
156 as: 'ActorFollowing',
158 preferredUsername: targetName
162 model: VideoChannelModel.unscoped(),
168 if (targetHost === null) {
169 actorFollowingPartInclude.where['serverId'] = null
171 actorFollowingPartInclude.include.push({
185 actorFollowingPartInclude,
195 return ActorFollowModel.findOne(query)
197 if (result && result.ActorFollowing.VideoChannel) {
198 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
205 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]) {
206 const whereTab = targets
212 '$preferredUsername$': t.name
224 '$preferredUsername$': t.name
247 attributes: [ 'preferredUsername' ],
248 model: ActorModel.unscoped(),
250 as: 'ActorFollowing',
253 attributes: [ 'host' ],
254 model: ServerModel.unscoped(),
262 return ActorFollowModel.findAll(query)
265 static listFollowingForApi (id: number, start: number, count: number, sort: string, search?: string) {
270 order: getSort(sort),
282 as: 'ActorFollowing',
290 [Op.iLike]: '%' + search + '%'
299 return ActorFollowModel.findAndCountAll(query)
300 .then(({ rows, count }) => {
308 static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) {
313 order: getSort(sort),
325 [ Op.iLike ]: '%' + search + '%'
333 as: 'ActorFollowing',
342 return ActorFollowModel.findAndCountAll(query)
343 .then(({ rows, count }) => {
351 static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) {
357 order: getSort(sort),
363 attributes: [ 'id' ],
364 model: ActorModel.unscoped(),
365 as: 'ActorFollowing',
369 model: VideoChannelModel.unscoped(),
374 exclude: unusedActorAttributesForAPI
380 model: AccountModel.unscoped(),
385 exclude: unusedActorAttributesForAPI
399 return ActorFollowModel.findAndCountAll(query)
400 .then(({ rows, count }) => {
402 data: rows.map(r => r.ActorFollowing.VideoChannel),
408 static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) {
409 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
412 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Transaction) {
413 return ActorFollowModel.createListAcceptedFollowForApiQuery(
424 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Transaction, start?: number, count?: number) {
425 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
428 static async getStats () {
429 const serverActor = await getServerActor()
431 const totalInstanceFollowing = await ActorFollowModel.count({
433 actorId: serverActor.id
437 const totalInstanceFollowers = await ActorFollowModel.count({
439 targetActorId: serverActor.id
444 totalInstanceFollowing,
445 totalInstanceFollowers
449 static updateFollowScore (inboxUrl: string, value: number, t?: Transaction) {
450 const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
452 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
453 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
454 `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
458 type: QueryTypes.BULKUPDATE,
462 return ActorFollowModel.sequelize.query(query, options)
465 private static async createListAcceptedFollowForApiQuery (
466 type: 'followers' | 'following',
474 let firstJoin: string
475 let secondJoin: string
477 if (type === 'followers') {
478 firstJoin = 'targetActorId'
479 secondJoin = 'actorId'
481 firstJoin = 'actorId'
482 secondJoin = 'targetActorId'
485 const selections: string[] = []
486 if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
487 else selections.push('"Follows"."' + columnUrl + '" AS "url"')
489 selections.push('COUNT(*) AS "total"')
491 const tasks: Bluebird<any>[] = []
493 for (let selection of selections) {
494 let query = 'SELECT ' + selection + ' FROM "actor" ' +
495 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
496 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
497 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
499 if (count !== undefined) query += 'LIMIT ' + count
500 if (start !== undefined) query += ' OFFSET ' + start
504 type: QueryTypes.SELECT,
507 tasks.push(ActorFollowModel.sequelize.query(query, options))
510 const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
511 const urls: string[] = followers.map(f => f.url)
515 total: dataTotal ? parseInt(dataTotal.total, 10) : 0
519 private static listBadActorFollows () {
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