actor.ts 17 KB

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