actor-follow.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. import * as Bluebird from 'bluebird'
  2. import { values } from 'lodash'
  3. import {
  4. AfterCreate,
  5. AfterDestroy,
  6. AfterUpdate,
  7. AllowNull,
  8. BelongsTo,
  9. Column,
  10. CreatedAt,
  11. DataType,
  12. Default,
  13. ForeignKey,
  14. IsInt,
  15. Max,
  16. Model,
  17. Table,
  18. UpdatedAt
  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'
  31. @Table({
  32. tableName: 'actorFollow',
  33. indexes: [
  34. {
  35. fields: [ 'actorId' ]
  36. },
  37. {
  38. fields: [ 'targetActorId' ]
  39. },
  40. {
  41. fields: [ 'actorId', 'targetActorId' ],
  42. unique: true
  43. },
  44. {
  45. fields: [ 'score' ]
  46. }
  47. ]
  48. })
  49. export class ActorFollowModel extends Model<ActorFollowModel> {
  50. @AllowNull(false)
  51. @Column(DataType.ENUM(...values(FOLLOW_STATES)))
  52. state: FollowState
  53. @AllowNull(false)
  54. @Default(ACTOR_FOLLOW_SCORE.BASE)
  55. @IsInt
  56. @Max(ACTOR_FOLLOW_SCORE.MAX)
  57. @Column
  58. score: number
  59. @CreatedAt
  60. createdAt: Date
  61. @UpdatedAt
  62. updatedAt: Date
  63. @ForeignKey(() => ActorModel)
  64. @Column
  65. actorId: number
  66. @BelongsTo(() => ActorModel, {
  67. foreignKey: {
  68. name: 'actorId',
  69. allowNull: false
  70. },
  71. as: 'ActorFollower',
  72. onDelete: 'CASCADE'
  73. })
  74. ActorFollower: ActorModel
  75. @ForeignKey(() => ActorModel)
  76. @Column
  77. targetActorId: number
  78. @BelongsTo(() => ActorModel, {
  79. foreignKey: {
  80. name: 'targetActorId',
  81. allowNull: false
  82. },
  83. as: 'ActorFollowing',
  84. onDelete: 'CASCADE'
  85. })
  86. ActorFollowing: ActorModel
  87. @AfterCreate
  88. @AfterUpdate
  89. static incrementFollowerAndFollowingCount (instance: ActorFollowModel) {
  90. if (instance.state !== 'accepted') return undefined
  91. return Promise.all([
  92. ActorModel.incrementFollows(instance.actorId, 'followingCount', 1),
  93. ActorModel.incrementFollows(instance.targetActorId, 'followersCount', 1)
  94. ])
  95. }
  96. @AfterDestroy
  97. static decrementFollowerAndFollowingCount (instance: ActorFollowModel) {
  98. return Promise.all([
  99. ActorModel.incrementFollows(instance.actorId, 'followingCount',-1),
  100. ActorModel.incrementFollows(instance.targetActorId, 'followersCount', -1)
  101. ])
  102. }
  103. // Remove actor follows with a score of 0 (too many requests where they were unreachable)
  104. static async removeBadActorFollows () {
  105. const actorFollows = await ActorFollowModel.listBadActorFollows()
  106. const actorFollowsRemovePromises = actorFollows.map(actorFollow => actorFollow.destroy())
  107. await Promise.all(actorFollowsRemovePromises)
  108. const numberOfActorFollowsRemoved = actorFollows.length
  109. if (numberOfActorFollowsRemoved) logger.info('Removed bad %d actor follows.', numberOfActorFollowsRemoved)
  110. }
  111. static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction) {
  112. const query = {
  113. where: {
  114. actorId,
  115. targetActorId: targetActorId
  116. },
  117. include: [
  118. {
  119. model: ActorModel,
  120. required: true,
  121. as: 'ActorFollower'
  122. },
  123. {
  124. model: ActorModel,
  125. required: true,
  126. as: 'ActorFollowing'
  127. }
  128. ],
  129. transaction: t
  130. }
  131. return ActorFollowModel.findOne(query)
  132. }
  133. static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Transaction) {
  134. const actorFollowingPartInclude: IncludeOptions = {
  135. model: ActorModel,
  136. required: true,
  137. as: 'ActorFollowing',
  138. where: {
  139. preferredUsername: targetName
  140. },
  141. include: [
  142. {
  143. model: VideoChannelModel.unscoped(),
  144. required: false
  145. }
  146. ]
  147. }
  148. if (targetHost === null) {
  149. actorFollowingPartInclude.where['serverId'] = null
  150. } else {
  151. actorFollowingPartInclude.include.push({
  152. model: ServerModel,
  153. required: true,
  154. where: {
  155. host: targetHost
  156. }
  157. })
  158. }
  159. const query = {
  160. where: {
  161. actorId
  162. },
  163. include: [
  164. actorFollowingPartInclude,
  165. {
  166. model: ActorModel,
  167. required: true,
  168. as: 'ActorFollower'
  169. }
  170. ],
  171. transaction: t
  172. }
  173. return ActorFollowModel.findOne(query)
  174. .then(result => {
  175. if (result && result.ActorFollowing.VideoChannel) {
  176. result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
  177. }
  178. return result
  179. })
  180. }
  181. static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]) {
  182. const whereTab = targets
  183. .map(t => {
  184. if (t.host) {
  185. return {
  186. [ Op.and ]: [
  187. {
  188. '$preferredUsername$': t.name
  189. },
  190. {
  191. '$host$': t.host
  192. }
  193. ]
  194. }
  195. }
  196. return {
  197. [ Op.and ]: [
  198. {
  199. '$preferredUsername$': t.name
  200. },
  201. {
  202. '$serverId$': null
  203. }
  204. ]
  205. }
  206. })
  207. const query = {
  208. attributes: [],
  209. where: {
  210. [ Op.and ]: [
  211. {
  212. [ Op.or ]: whereTab
  213. },
  214. {
  215. actorId
  216. }
  217. ]
  218. },
  219. include: [
  220. {
  221. attributes: [ 'preferredUsername' ],
  222. model: ActorModel.unscoped(),
  223. required: true,
  224. as: 'ActorFollowing',
  225. include: [
  226. {
  227. attributes: [ 'host' ],
  228. model: ServerModel.unscoped(),
  229. required: false
  230. }
  231. ]
  232. }
  233. ]
  234. }
  235. return ActorFollowModel.findAll(query)
  236. }
  237. static listFollowingForApi (id: number, start: number, count: number, sort: string, search?: string) {
  238. const query = {
  239. distinct: true,
  240. offset: start,
  241. limit: count,
  242. order: getSort(sort),
  243. include: [
  244. {
  245. model: ActorModel,
  246. required: true,
  247. as: 'ActorFollower',
  248. where: {
  249. id
  250. }
  251. },
  252. {
  253. model: ActorModel,
  254. as: 'ActorFollowing',
  255. required: true,
  256. include: [
  257. {
  258. model: ServerModel,
  259. required: true,
  260. where: search ? {
  261. host: {
  262. [Op.iLike]: '%' + search + '%'
  263. }
  264. } : undefined
  265. }
  266. ]
  267. }
  268. ]
  269. }
  270. return ActorFollowModel.findAndCountAll(query)
  271. .then(({ rows, count }) => {
  272. return {
  273. data: rows,
  274. total: count
  275. }
  276. })
  277. }
  278. static listFollowersForApi (actorId: number, start: number, count: number, sort: string, search?: string) {
  279. const query = {
  280. distinct: true,
  281. offset: start,
  282. limit: count,
  283. order: getSort(sort),
  284. include: [
  285. {
  286. model: ActorModel,
  287. required: true,
  288. as: 'ActorFollower',
  289. include: [
  290. {
  291. model: ServerModel,
  292. required: true,
  293. where: search ? {
  294. host: {
  295. [ Op.iLike ]: '%' + search + '%'
  296. }
  297. } : undefined
  298. }
  299. ]
  300. },
  301. {
  302. model: ActorModel,
  303. as: 'ActorFollowing',
  304. required: true,
  305. where: {
  306. id: actorId
  307. }
  308. }
  309. ]
  310. }
  311. return ActorFollowModel.findAndCountAll(query)
  312. .then(({ rows, count }) => {
  313. return {
  314. data: rows,
  315. total: count
  316. }
  317. })
  318. }
  319. static listSubscriptionsForApi (actorId: number, start: number, count: number, sort: string) {
  320. const query = {
  321. attributes: [],
  322. distinct: true,
  323. offset: start,
  324. limit: count,
  325. order: getSort(sort),
  326. where: {
  327. actorId: actorId
  328. },
  329. include: [
  330. {
  331. attributes: [ 'id' ],
  332. model: ActorModel.unscoped(),
  333. as: 'ActorFollowing',
  334. required: true,
  335. include: [
  336. {
  337. model: VideoChannelModel.unscoped(),
  338. required: true,
  339. include: [
  340. {
  341. attributes: {
  342. exclude: unusedActorAttributesForAPI
  343. },
  344. model: ActorModel,
  345. required: true
  346. },
  347. {
  348. model: AccountModel.unscoped(),
  349. required: true,
  350. include: [
  351. {
  352. attributes: {
  353. exclude: unusedActorAttributesForAPI
  354. },
  355. model: ActorModel,
  356. required: true
  357. }
  358. ]
  359. }
  360. ]
  361. }
  362. ]
  363. }
  364. ]
  365. }
  366. return ActorFollowModel.findAndCountAll(query)
  367. .then(({ rows, count }) => {
  368. return {
  369. data: rows.map(r => r.ActorFollowing.VideoChannel),
  370. total: count
  371. }
  372. })
  373. }
  374. static listAcceptedFollowerUrlsForAP (actorIds: number[], t: Transaction, start?: number, count?: number) {
  375. return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
  376. }
  377. static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Transaction) {
  378. return ActorFollowModel.createListAcceptedFollowForApiQuery(
  379. 'followers',
  380. actorIds,
  381. t,
  382. undefined,
  383. undefined,
  384. 'sharedInboxUrl',
  385. true
  386. )
  387. }
  388. static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Transaction, start?: number, count?: number) {
  389. return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
  390. }
  391. static async getStats () {
  392. const serverActor = await getServerActor()
  393. const totalInstanceFollowing = await ActorFollowModel.count({
  394. where: {
  395. actorId: serverActor.id
  396. }
  397. })
  398. const totalInstanceFollowers = await ActorFollowModel.count({
  399. where: {
  400. targetActorId: serverActor.id
  401. }
  402. })
  403. return {
  404. totalInstanceFollowing,
  405. totalInstanceFollowers
  406. }
  407. }
  408. static updateFollowScore (inboxUrl: string, value: number, t?: Transaction) {
  409. const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
  410. 'WHERE id IN (' +
  411. 'SELECT "actorFollow"."id" FROM "actorFollow" ' +
  412. 'INNER JOIN "actor" ON "actor"."id" = "actorFollow"."actorId" ' +
  413. `WHERE "actor"."inboxUrl" = '${inboxUrl}' OR "actor"."sharedInboxUrl" = '${inboxUrl}'` +
  414. ')'
  415. const options = {
  416. type: QueryTypes.BULKUPDATE,
  417. transaction: t
  418. }
  419. return ActorFollowModel.sequelize.query(query, options)
  420. }
  421. private static async createListAcceptedFollowForApiQuery (
  422. type: 'followers' | 'following',
  423. actorIds: number[],
  424. t: Transaction,
  425. start?: number,
  426. count?: number,
  427. columnUrl = 'url',
  428. distinct = false
  429. ) {
  430. let firstJoin: string
  431. let secondJoin: string
  432. if (type === 'followers') {
  433. firstJoin = 'targetActorId'
  434. secondJoin = 'actorId'
  435. } else {
  436. firstJoin = 'actorId'
  437. secondJoin = 'targetActorId'
  438. }
  439. const selections: string[] = []
  440. if (distinct === true) selections.push('DISTINCT("Follows"."' + columnUrl + '") AS "url"')
  441. else selections.push('"Follows"."' + columnUrl + '" AS "url"')
  442. selections.push('COUNT(*) AS "total"')
  443. const tasks: Bluebird<any>[] = []
  444. for (let selection of selections) {
  445. let query = 'SELECT ' + selection + ' FROM "actor" ' +
  446. 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
  447. 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
  448. 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
  449. if (count !== undefined) query += 'LIMIT ' + count
  450. if (start !== undefined) query += ' OFFSET ' + start
  451. const options = {
  452. bind: { actorIds },
  453. type: QueryTypes.SELECT,
  454. transaction: t
  455. }
  456. tasks.push(ActorFollowModel.sequelize.query(query, options))
  457. }
  458. const [ followers, [ dataTotal ] ] = await Promise.all(tasks)
  459. const urls: string[] = followers.map(f => f.url)
  460. return {
  461. data: urls,
  462. total: dataTotal ? parseInt(dataTotal.total, 10) : 0
  463. }
  464. }
  465. private static listBadActorFollows () {
  466. const query = {
  467. where: {
  468. score: {
  469. [Op.lte]: 0
  470. }
  471. },
  472. logging: false
  473. }
  474. return ActorFollowModel.findAll(query)
  475. }
  476. toFormattedJSON (): ActorFollow {
  477. const follower = this.ActorFollower.toFormattedJSON()
  478. const following = this.ActorFollowing.toFormattedJSON()
  479. return {
  480. id: this.id,
  481. follower,
  482. following,
  483. score: this.score,
  484. state: this.state,
  485. createdAt: this.createdAt,
  486. updatedAt: this.updatedAt
  487. }
  488. }
  489. }