actor.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. import { values } from 'lodash'
  2. import { extname } from 'path'
  3. import { literal, Op, Transaction } from 'sequelize'
  4. import {
  5. AllowNull,
  6. BelongsTo,
  7. Column,
  8. CreatedAt,
  9. DataType,
  10. DefaultScope,
  11. ForeignKey,
  12. HasMany,
  13. HasOne,
  14. Is,
  15. Model,
  16. Scopes,
  17. Table,
  18. UpdatedAt
  19. } from 'sequelize-typescript'
  20. import { ModelCache } from '@server/models/model-cache'
  21. import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
  22. import { ActorImage } from '../../../shared/models/actors/actor-image.model'
  23. import { activityPubContextify } from '../../helpers/activitypub'
  24. import {
  25. isActorFollowersCountValid,
  26. isActorFollowingCountValid,
  27. isActorPreferredUsernameValid,
  28. isActorPrivateKeyValid,
  29. isActorPublicKeyValid
  30. } from '../../helpers/custom-validators/activitypub/actor'
  31. import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
  32. import {
  33. ACTIVITY_PUB,
  34. ACTIVITY_PUB_ACTOR_TYPES,
  35. CONSTRAINTS_FIELDS,
  36. MIMETYPES,
  37. SERVER_ACTOR_NAME,
  38. WEBSERVER
  39. } from '../../initializers/constants'
  40. import {
  41. MActor,
  42. MActorAccountChannelId,
  43. MActorAPAccount,
  44. MActorAPChannel,
  45. MActorFormattable,
  46. MActorFull,
  47. MActorHost,
  48. MActorServer,
  49. MActorSummaryFormattable,
  50. MActorUrl,
  51. MActorWithInboxes
  52. } from '../../types/models'
  53. import { AccountModel } from '../account/account'
  54. import { ActorImageModel } from '../account/actor-image'
  55. import { ServerModel } from '../server/server'
  56. import { isOutdated, throwIfNotValid } from '../utils'
  57. import { VideoModel } from '../video/video'
  58. import { VideoChannelModel } from '../video/video-channel'
  59. import { ActorFollowModel } from './actor-follow'
  60. enum ScopeNames {
  61. FULL = 'FULL'
  62. }
  63. export const unusedActorAttributesForAPI = [
  64. 'publicKey',
  65. 'privateKey',
  66. 'inboxUrl',
  67. 'outboxUrl',
  68. 'sharedInboxUrl',
  69. 'followersUrl',
  70. 'followingUrl',
  71. 'createdAt',
  72. 'updatedAt'
  73. ]
  74. @DefaultScope(() => ({
  75. include: [
  76. {
  77. model: ServerModel,
  78. required: false
  79. },
  80. {
  81. model: ActorImageModel,
  82. as: 'Avatar',
  83. required: false
  84. }
  85. ]
  86. }))
  87. @Scopes(() => ({
  88. [ScopeNames.FULL]: {
  89. include: [
  90. {
  91. model: AccountModel.unscoped(),
  92. required: false
  93. },
  94. {
  95. model: VideoChannelModel.unscoped(),
  96. required: false,
  97. include: [
  98. {
  99. model: AccountModel,
  100. required: true
  101. }
  102. ]
  103. },
  104. {
  105. model: ServerModel,
  106. required: false
  107. },
  108. {
  109. model: ActorImageModel,
  110. as: 'Avatar',
  111. required: false
  112. },
  113. {
  114. model: ActorImageModel,
  115. as: 'Banner',
  116. required: false
  117. }
  118. ]
  119. }
  120. }))
  121. @Table({
  122. tableName: 'actor',
  123. indexes: [
  124. {
  125. fields: [ 'url' ],
  126. unique: true
  127. },
  128. {
  129. fields: [ 'preferredUsername', 'serverId' ],
  130. unique: true,
  131. where: {
  132. serverId: {
  133. [Op.ne]: null
  134. }
  135. }
  136. },
  137. {
  138. fields: [ 'preferredUsername' ],
  139. unique: true,
  140. where: {
  141. serverId: null
  142. }
  143. },
  144. {
  145. fields: [ 'inboxUrl', 'sharedInboxUrl' ]
  146. },
  147. {
  148. fields: [ 'sharedInboxUrl' ]
  149. },
  150. {
  151. fields: [ 'serverId' ]
  152. },
  153. {
  154. fields: [ 'avatarId' ]
  155. },
  156. {
  157. fields: [ 'followersUrl' ]
  158. }
  159. ]
  160. })
  161. export class ActorModel extends Model {
  162. @AllowNull(false)
  163. @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
  164. type: ActivityPubActorType
  165. @AllowNull(false)
  166. @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
  167. @Column
  168. preferredUsername: string
  169. @AllowNull(false)
  170. @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
  171. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  172. url: string
  173. @AllowNull(true)
  174. @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
  175. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
  176. publicKey: string
  177. @AllowNull(true)
  178. @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
  179. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
  180. privateKey: string
  181. @AllowNull(false)
  182. @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
  183. @Column
  184. followersCount: number
  185. @AllowNull(false)
  186. @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
  187. @Column
  188. followingCount: number
  189. @AllowNull(false)
  190. @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
  191. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  192. inboxUrl: string
  193. @AllowNull(true)
  194. @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
  195. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  196. outboxUrl: string
  197. @AllowNull(true)
  198. @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url', true))
  199. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  200. sharedInboxUrl: string
  201. @AllowNull(true)
  202. @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
  203. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  204. followersUrl: string
  205. @AllowNull(true)
  206. @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
  207. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  208. followingUrl: string
  209. @CreatedAt
  210. createdAt: Date
  211. @UpdatedAt
  212. updatedAt: Date
  213. @ForeignKey(() => ActorImageModel)
  214. @Column
  215. avatarId: number
  216. @ForeignKey(() => ActorImageModel)
  217. @Column
  218. bannerId: number
  219. @BelongsTo(() => ActorImageModel, {
  220. foreignKey: {
  221. name: 'avatarId',
  222. allowNull: true
  223. },
  224. as: 'Avatar',
  225. onDelete: 'set null',
  226. hooks: true
  227. })
  228. Avatar: ActorImageModel
  229. @BelongsTo(() => ActorImageModel, {
  230. foreignKey: {
  231. name: 'bannerId',
  232. allowNull: true
  233. },
  234. as: 'Banner',
  235. onDelete: 'set null',
  236. hooks: true
  237. })
  238. Banner: ActorImageModel
  239. @HasMany(() => ActorFollowModel, {
  240. foreignKey: {
  241. name: 'actorId',
  242. allowNull: false
  243. },
  244. as: 'ActorFollowings',
  245. onDelete: 'cascade'
  246. })
  247. ActorFollowing: ActorFollowModel[]
  248. @HasMany(() => ActorFollowModel, {
  249. foreignKey: {
  250. name: 'targetActorId',
  251. allowNull: false
  252. },
  253. as: 'ActorFollowers',
  254. onDelete: 'cascade'
  255. })
  256. ActorFollowers: ActorFollowModel[]
  257. @ForeignKey(() => ServerModel)
  258. @Column
  259. serverId: number
  260. @BelongsTo(() => ServerModel, {
  261. foreignKey: {
  262. allowNull: true
  263. },
  264. onDelete: 'cascade'
  265. })
  266. Server: ServerModel
  267. @HasOne(() => AccountModel, {
  268. foreignKey: {
  269. allowNull: true
  270. },
  271. onDelete: 'cascade',
  272. hooks: true
  273. })
  274. Account: AccountModel
  275. @HasOne(() => VideoChannelModel, {
  276. foreignKey: {
  277. allowNull: true
  278. },
  279. onDelete: 'cascade',
  280. hooks: true
  281. })
  282. VideoChannel: VideoChannelModel
  283. static load (id: number): Promise<MActor> {
  284. return ActorModel.unscoped().findByPk(id)
  285. }
  286. static loadFull (id: number): Promise<MActorFull> {
  287. return ActorModel.scope(ScopeNames.FULL).findByPk(id)
  288. }
  289. static loadFromAccountByVideoId (videoId: number, transaction: Transaction): Promise<MActor> {
  290. const query = {
  291. include: [
  292. {
  293. attributes: [ 'id' ],
  294. model: AccountModel.unscoped(),
  295. required: true,
  296. include: [
  297. {
  298. attributes: [ 'id' ],
  299. model: VideoChannelModel.unscoped(),
  300. required: true,
  301. include: [
  302. {
  303. attributes: [ 'id' ],
  304. model: VideoModel.unscoped(),
  305. required: true,
  306. where: {
  307. id: videoId
  308. }
  309. }
  310. ]
  311. }
  312. ]
  313. }
  314. ],
  315. transaction
  316. }
  317. return ActorModel.unscoped().findOne(query)
  318. }
  319. static isActorUrlExist (url: string) {
  320. const query = {
  321. raw: true,
  322. where: {
  323. url
  324. }
  325. }
  326. return ActorModel.unscoped().findOne(query)
  327. .then(a => !!a)
  328. }
  329. static listByFollowersUrls (followersUrls: string[], transaction?: Transaction): Promise<MActorFull[]> {
  330. const query = {
  331. where: {
  332. followersUrl: {
  333. [Op.in]: followersUrls
  334. }
  335. },
  336. transaction
  337. }
  338. return ActorModel.scope(ScopeNames.FULL).findAll(query)
  339. }
  340. static loadLocalByName (preferredUsername: string, transaction?: Transaction): Promise<MActorFull> {
  341. const fun = () => {
  342. const query = {
  343. where: {
  344. preferredUsername,
  345. serverId: null
  346. },
  347. transaction
  348. }
  349. return ActorModel.scope(ScopeNames.FULL)
  350. .findOne(query)
  351. }
  352. return ModelCache.Instance.doCache({
  353. cacheType: 'local-actor-name',
  354. key: preferredUsername,
  355. // The server actor never change, so we can easily cache it
  356. whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
  357. fun
  358. })
  359. }
  360. static loadLocalUrlByName (preferredUsername: string, transaction?: Transaction): Promise<MActorUrl> {
  361. const fun = () => {
  362. const query = {
  363. attributes: [ 'url' ],
  364. where: {
  365. preferredUsername,
  366. serverId: null
  367. },
  368. transaction
  369. }
  370. return ActorModel.unscoped()
  371. .findOne(query)
  372. }
  373. return ModelCache.Instance.doCache({
  374. cacheType: 'local-actor-name',
  375. key: preferredUsername,
  376. // The server actor never change, so we can easily cache it
  377. whitelist: () => preferredUsername === SERVER_ACTOR_NAME,
  378. fun
  379. })
  380. }
  381. static loadByNameAndHost (preferredUsername: string, host: string): Promise<MActorFull> {
  382. const query = {
  383. where: {
  384. preferredUsername
  385. },
  386. include: [
  387. {
  388. model: ServerModel,
  389. required: true,
  390. where: {
  391. host
  392. }
  393. }
  394. ]
  395. }
  396. return ActorModel.scope(ScopeNames.FULL).findOne(query)
  397. }
  398. static loadByUrl (url: string, transaction?: Transaction): Promise<MActorAccountChannelId> {
  399. const query = {
  400. where: {
  401. url
  402. },
  403. transaction,
  404. include: [
  405. {
  406. attributes: [ 'id' ],
  407. model: AccountModel.unscoped(),
  408. required: false
  409. },
  410. {
  411. attributes: [ 'id' ],
  412. model: VideoChannelModel.unscoped(),
  413. required: false
  414. }
  415. ]
  416. }
  417. return ActorModel.unscoped().findOne(query)
  418. }
  419. static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Transaction): Promise<MActorFull> {
  420. const query = {
  421. where: {
  422. url
  423. },
  424. transaction
  425. }
  426. return ActorModel.scope(ScopeNames.FULL).findOne(query)
  427. }
  428. static rebuildFollowsCount (ofId: number, type: 'followers' | 'following', transaction?: Transaction) {
  429. const sanitizedOfId = parseInt(ofId + '', 10)
  430. const where = { id: sanitizedOfId }
  431. let columnToUpdate: string
  432. let columnOfCount: string
  433. if (type === 'followers') {
  434. columnToUpdate = 'followersCount'
  435. columnOfCount = 'targetActorId'
  436. } else {
  437. columnToUpdate = 'followingCount'
  438. columnOfCount = 'actorId'
  439. }
  440. return ActorModel.update({
  441. [columnToUpdate]: literal(`(SELECT COUNT(*) FROM "actorFollow" WHERE "${columnOfCount}" = ${sanitizedOfId})`)
  442. }, { where, transaction })
  443. }
  444. static loadAccountActorByVideoId (videoId: number): Promise<MActor> {
  445. const query = {
  446. include: [
  447. {
  448. attributes: [ 'id' ],
  449. model: AccountModel.unscoped(),
  450. required: true,
  451. include: [
  452. {
  453. attributes: [ 'id', 'accountId' ],
  454. model: VideoChannelModel.unscoped(),
  455. required: true,
  456. include: [
  457. {
  458. attributes: [ 'id', 'channelId' ],
  459. model: VideoModel.unscoped(),
  460. where: {
  461. id: videoId
  462. }
  463. }
  464. ]
  465. }
  466. ]
  467. }
  468. ]
  469. }
  470. return ActorModel.unscoped().findOne(query)
  471. }
  472. getSharedInbox (this: MActorWithInboxes) {
  473. return this.sharedInboxUrl || this.inboxUrl
  474. }
  475. toFormattedSummaryJSON (this: MActorSummaryFormattable) {
  476. let avatar: ActorImage = null
  477. if (this.Avatar) {
  478. avatar = this.Avatar.toFormattedJSON()
  479. }
  480. return {
  481. url: this.url,
  482. name: this.preferredUsername,
  483. host: this.getHost(),
  484. avatar
  485. }
  486. }
  487. toFormattedJSON (this: MActorFormattable) {
  488. const base = this.toFormattedSummaryJSON()
  489. let banner: ActorImage = null
  490. if (this.bannerId) {
  491. banner = this.Banner.toFormattedJSON()
  492. }
  493. return Object.assign(base, {
  494. id: this.id,
  495. hostRedundancyAllowed: this.getRedundancyAllowed(),
  496. followingCount: this.followingCount,
  497. followersCount: this.followersCount,
  498. banner,
  499. createdAt: this.createdAt,
  500. updatedAt: this.updatedAt
  501. })
  502. }
  503. toActivityPubObject (this: MActorAPChannel | MActorAPAccount, name: string) {
  504. let icon: ActivityIconObject
  505. let image: ActivityIconObject
  506. if (this.avatarId) {
  507. const extension = extname(this.Avatar.filename)
  508. icon = {
  509. type: 'Image',
  510. mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
  511. height: this.Avatar.height,
  512. width: this.Avatar.width,
  513. url: this.getAvatarUrl()
  514. }
  515. }
  516. if (this.bannerId) {
  517. const banner = (this as MActorAPChannel).Banner
  518. const extension = extname(banner.filename)
  519. image = {
  520. type: 'Image',
  521. mediaType: MIMETYPES.IMAGE.EXT_MIMETYPE[extension],
  522. height: banner.height,
  523. width: banner.width,
  524. url: this.getBannerUrl()
  525. }
  526. }
  527. const json = {
  528. type: this.type,
  529. id: this.url,
  530. following: this.getFollowingUrl(),
  531. followers: this.getFollowersUrl(),
  532. playlists: this.getPlaylistsUrl(),
  533. inbox: this.inboxUrl,
  534. outbox: this.outboxUrl,
  535. preferredUsername: this.preferredUsername,
  536. url: this.url,
  537. name,
  538. endpoints: {
  539. sharedInbox: this.sharedInboxUrl
  540. },
  541. publicKey: {
  542. id: this.getPublicKeyUrl(),
  543. owner: this.url,
  544. publicKeyPem: this.publicKey
  545. },
  546. icon,
  547. image
  548. }
  549. return activityPubContextify(json)
  550. }
  551. getFollowerSharedInboxUrls (t: Transaction) {
  552. const query = {
  553. attributes: [ 'sharedInboxUrl' ],
  554. include: [
  555. {
  556. attribute: [],
  557. model: ActorFollowModel.unscoped(),
  558. required: true,
  559. as: 'ActorFollowing',
  560. where: {
  561. state: 'accepted',
  562. targetActorId: this.id
  563. }
  564. }
  565. ],
  566. transaction: t
  567. }
  568. return ActorModel.findAll(query)
  569. .then(accounts => accounts.map(a => a.sharedInboxUrl))
  570. }
  571. getFollowingUrl () {
  572. return this.url + '/following'
  573. }
  574. getFollowersUrl () {
  575. return this.url + '/followers'
  576. }
  577. getPlaylistsUrl () {
  578. return this.url + '/playlists'
  579. }
  580. getPublicKeyUrl () {
  581. return this.url + '#main-key'
  582. }
  583. isOwned () {
  584. return this.serverId === null
  585. }
  586. getWebfingerUrl (this: MActorServer) {
  587. return 'acct:' + this.preferredUsername + '@' + this.getHost()
  588. }
  589. getIdentifier () {
  590. return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
  591. }
  592. getHost (this: MActorHost) {
  593. return this.Server ? this.Server.host : WEBSERVER.HOST
  594. }
  595. getRedundancyAllowed () {
  596. return this.Server ? this.Server.redundancyAllowed : false
  597. }
  598. getAvatarUrl () {
  599. if (!this.avatarId) return undefined
  600. return WEBSERVER.URL + this.Avatar.getStaticPath()
  601. }
  602. getBannerUrl () {
  603. if (!this.bannerId) return undefined
  604. return WEBSERVER.URL + this.Banner.getStaticPath()
  605. }
  606. isOutdated () {
  607. if (this.isOwned()) return false
  608. return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
  609. }
  610. }