actor.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. import { values } from 'lodash'
  2. import { extname } from 'path'
  3. import * as Sequelize 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 { ActivityPubActorType } from '../../../shared/models/activitypub'
  21. import { Avatar } from '../../../shared/models/avatars/avatar.model'
  22. import { activityPubContextify } from '../../helpers/activitypub'
  23. import {
  24. isActorFollowersCountValid,
  25. isActorFollowingCountValid,
  26. isActorPreferredUsernameValid,
  27. isActorPrivateKeyValid,
  28. isActorPublicKeyValid
  29. } from '../../helpers/custom-validators/activitypub/actor'
  30. import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
  31. import { ACTIVITY_PUB, ACTIVITY_PUB_ACTOR_TYPES, CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
  32. import { AccountModel } from '../account/account'
  33. import { AvatarModel } from '../avatar/avatar'
  34. import { ServerModel } from '../server/server'
  35. import { isOutdated, throwIfNotValid } from '../utils'
  36. import { VideoChannelModel } from '../video/video-channel'
  37. import { ActorFollowModel } from './actor-follow'
  38. import { VideoModel } from '../video/video'
  39. import {
  40. MActor,
  41. MActorAccountChannelId,
  42. MActorAP,
  43. MActorFormattable,
  44. MActorFull,
  45. MActorHost,
  46. MActorServer,
  47. MActorSummaryFormattable
  48. } from '../../typings/models'
  49. import * as Bluebird from 'bluebird'
  50. enum ScopeNames {
  51. FULL = 'FULL'
  52. }
  53. export const unusedActorAttributesForAPI = [
  54. 'publicKey',
  55. 'privateKey',
  56. 'inboxUrl',
  57. 'outboxUrl',
  58. 'sharedInboxUrl',
  59. 'followersUrl',
  60. 'followingUrl',
  61. 'url',
  62. 'createdAt',
  63. 'updatedAt'
  64. ]
  65. @DefaultScope(() => ({
  66. include: [
  67. {
  68. model: ServerModel,
  69. required: false
  70. },
  71. {
  72. model: AvatarModel,
  73. required: false
  74. }
  75. ]
  76. }))
  77. @Scopes(() => ({
  78. [ScopeNames.FULL]: {
  79. include: [
  80. {
  81. model: AccountModel.unscoped(),
  82. required: false
  83. },
  84. {
  85. model: VideoChannelModel.unscoped(),
  86. required: false,
  87. include: [
  88. {
  89. model: AccountModel,
  90. required: true
  91. }
  92. ]
  93. },
  94. {
  95. model: ServerModel,
  96. required: false
  97. },
  98. {
  99. model: AvatarModel,
  100. required: false
  101. }
  102. ]
  103. }
  104. }))
  105. @Table({
  106. tableName: 'actor',
  107. indexes: [
  108. {
  109. fields: [ 'url' ],
  110. unique: true
  111. },
  112. {
  113. fields: [ 'preferredUsername', 'serverId' ],
  114. unique: true
  115. },
  116. {
  117. fields: [ 'inboxUrl', 'sharedInboxUrl' ]
  118. },
  119. {
  120. fields: [ 'sharedInboxUrl' ]
  121. },
  122. {
  123. fields: [ 'serverId' ]
  124. },
  125. {
  126. fields: [ 'avatarId' ]
  127. },
  128. {
  129. fields: [ 'followersUrl' ]
  130. }
  131. ]
  132. })
  133. export class ActorModel extends Model<ActorModel> {
  134. @AllowNull(false)
  135. @Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
  136. type: ActivityPubActorType
  137. @AllowNull(false)
  138. @Is('ActorPreferredUsername', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor preferred username'))
  139. @Column
  140. preferredUsername: string
  141. @AllowNull(false)
  142. @Is('ActorUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
  143. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  144. url: string
  145. @AllowNull(true)
  146. @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPublicKeyValid, 'public key', true))
  147. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PUBLIC_KEY.max))
  148. publicKey: string
  149. @AllowNull(true)
  150. @Is('ActorPublicKey', value => throwIfNotValid(value, isActorPrivateKeyValid, 'private key', true))
  151. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.PRIVATE_KEY.max))
  152. privateKey: string
  153. @AllowNull(false)
  154. @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowersCountValid, 'followers count'))
  155. @Column
  156. followersCount: number
  157. @AllowNull(false)
  158. @Is('ActorFollowersCount', value => throwIfNotValid(value, isActorFollowingCountValid, 'following count'))
  159. @Column
  160. followingCount: number
  161. @AllowNull(false)
  162. @Is('ActorInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
  163. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  164. inboxUrl: string
  165. @AllowNull(true)
  166. @Is('ActorOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url', true))
  167. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  168. outboxUrl: string
  169. @AllowNull(false)
  170. @Is('ActorSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url'))
  171. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  172. sharedInboxUrl: string
  173. @AllowNull(true)
  174. @Is('ActorFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url', true))
  175. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  176. followersUrl: string
  177. @AllowNull(true)
  178. @Is('ActorFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url', true))
  179. @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACTORS.URL.max))
  180. followingUrl: string
  181. @CreatedAt
  182. createdAt: Date
  183. @UpdatedAt
  184. updatedAt: Date
  185. @ForeignKey(() => AvatarModel)
  186. @Column
  187. avatarId: number
  188. @BelongsTo(() => AvatarModel, {
  189. foreignKey: {
  190. allowNull: true
  191. },
  192. onDelete: 'set null',
  193. hooks: true
  194. })
  195. Avatar: AvatarModel
  196. @HasMany(() => ActorFollowModel, {
  197. foreignKey: {
  198. name: 'actorId',
  199. allowNull: false
  200. },
  201. as: 'ActorFollowings',
  202. onDelete: 'cascade'
  203. })
  204. ActorFollowing: ActorFollowModel[]
  205. @HasMany(() => ActorFollowModel, {
  206. foreignKey: {
  207. name: 'targetActorId',
  208. allowNull: false
  209. },
  210. as: 'ActorFollowers',
  211. onDelete: 'cascade'
  212. })
  213. ActorFollowers: ActorFollowModel[]
  214. @ForeignKey(() => ServerModel)
  215. @Column
  216. serverId: number
  217. @BelongsTo(() => ServerModel, {
  218. foreignKey: {
  219. allowNull: true
  220. },
  221. onDelete: 'cascade'
  222. })
  223. Server: ServerModel
  224. @HasOne(() => AccountModel, {
  225. foreignKey: {
  226. allowNull: true
  227. },
  228. onDelete: 'cascade',
  229. hooks: true
  230. })
  231. Account: AccountModel
  232. @HasOne(() => VideoChannelModel, {
  233. foreignKey: {
  234. allowNull: true
  235. },
  236. onDelete: 'cascade',
  237. hooks: true
  238. })
  239. VideoChannel: VideoChannelModel
  240. static load (id: number): Bluebird<MActor> {
  241. return ActorModel.unscoped().findByPk(id)
  242. }
  243. static loadFull (id: number): Bluebird<MActorFull> {
  244. return ActorModel.scope(ScopeNames.FULL).findByPk(id)
  245. }
  246. static loadFromAccountByVideoId (videoId: number, transaction: Sequelize.Transaction): Bluebird<MActor> {
  247. const query = {
  248. include: [
  249. {
  250. attributes: [ 'id' ],
  251. model: AccountModel.unscoped(),
  252. required: true,
  253. include: [
  254. {
  255. attributes: [ 'id' ],
  256. model: VideoChannelModel.unscoped(),
  257. required: true,
  258. include: [
  259. {
  260. attributes: [ 'id' ],
  261. model: VideoModel.unscoped(),
  262. required: true,
  263. where: {
  264. id: videoId
  265. }
  266. }
  267. ]
  268. }
  269. ]
  270. }
  271. ],
  272. transaction
  273. }
  274. return ActorModel.unscoped().findOne(query)
  275. }
  276. static isActorUrlExist (url: string) {
  277. const query = {
  278. raw: true,
  279. where: {
  280. url
  281. }
  282. }
  283. return ActorModel.unscoped().findOne(query)
  284. .then(a => !!a)
  285. }
  286. static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction): Bluebird<MActorFull[]> {
  287. const query = {
  288. where: {
  289. followersUrl: {
  290. [ Sequelize.Op.in ]: followersUrls
  291. }
  292. },
  293. transaction
  294. }
  295. return ActorModel.scope(ScopeNames.FULL).findAll(query)
  296. }
  297. static loadLocalByName (preferredUsername: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
  298. const query = {
  299. where: {
  300. preferredUsername,
  301. serverId: null
  302. },
  303. transaction
  304. }
  305. return ActorModel.scope(ScopeNames.FULL).findOne(query)
  306. }
  307. static loadByNameAndHost (preferredUsername: string, host: string): Bluebird<MActorFull> {
  308. const query = {
  309. where: {
  310. preferredUsername
  311. },
  312. include: [
  313. {
  314. model: ServerModel,
  315. required: true,
  316. where: {
  317. host
  318. }
  319. }
  320. ]
  321. }
  322. return ActorModel.scope(ScopeNames.FULL).findOne(query)
  323. }
  324. static loadByUrl (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorAccountChannelId> {
  325. const query = {
  326. where: {
  327. url
  328. },
  329. transaction,
  330. include: [
  331. {
  332. attributes: [ 'id' ],
  333. model: AccountModel.unscoped(),
  334. required: false
  335. },
  336. {
  337. attributes: [ 'id' ],
  338. model: VideoChannelModel.unscoped(),
  339. required: false
  340. }
  341. ]
  342. }
  343. return ActorModel.unscoped().findOne(query)
  344. }
  345. static loadByUrlAndPopulateAccountAndChannel (url: string, transaction?: Sequelize.Transaction): Bluebird<MActorFull> {
  346. const query = {
  347. where: {
  348. url
  349. },
  350. transaction
  351. }
  352. return ActorModel.scope(ScopeNames.FULL).findOne(query)
  353. }
  354. static incrementFollows (id: number, column: 'followersCount' | 'followingCount', by: number) {
  355. return ActorModel.increment(column, {
  356. by,
  357. where: {
  358. id
  359. }
  360. })
  361. }
  362. toFormattedSummaryJSON (this: MActorSummaryFormattable) {
  363. let avatar: Avatar = null
  364. if (this.Avatar) {
  365. avatar = this.Avatar.toFormattedJSON()
  366. }
  367. return {
  368. url: this.url,
  369. name: this.preferredUsername,
  370. host: this.getHost(),
  371. avatar
  372. }
  373. }
  374. toFormattedJSON (this: MActorFormattable) {
  375. const base = this.toFormattedSummaryJSON()
  376. return Object.assign(base, {
  377. id: this.id,
  378. hostRedundancyAllowed: this.getRedundancyAllowed(),
  379. followingCount: this.followingCount,
  380. followersCount: this.followersCount,
  381. createdAt: this.createdAt,
  382. updatedAt: this.updatedAt
  383. })
  384. }
  385. toActivityPubObject (this: MActorAP, name: string) {
  386. let activityPubType
  387. let icon = undefined
  388. if (this.avatarId) {
  389. const extension = extname(this.Avatar.filename)
  390. icon = {
  391. type: 'Image',
  392. mediaType: extension === '.png' ? 'image/png' : 'image/jpeg',
  393. url: this.getAvatarUrl()
  394. }
  395. }
  396. const json = {
  397. type: this.type,
  398. id: this.url,
  399. following: this.getFollowingUrl(),
  400. followers: this.getFollowersUrl(),
  401. playlists: this.getPlaylistsUrl(),
  402. inbox: this.inboxUrl,
  403. outbox: this.outboxUrl,
  404. preferredUsername: this.preferredUsername,
  405. url: this.url,
  406. name,
  407. endpoints: {
  408. sharedInbox: this.sharedInboxUrl
  409. },
  410. publicKey: {
  411. id: this.getPublicKeyUrl(),
  412. owner: this.url,
  413. publicKeyPem: this.publicKey
  414. },
  415. icon
  416. }
  417. return activityPubContextify(json)
  418. }
  419. getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
  420. const query = {
  421. attributes: [ 'sharedInboxUrl' ],
  422. include: [
  423. {
  424. attribute: [],
  425. model: ActorFollowModel.unscoped(),
  426. required: true,
  427. as: 'ActorFollowing',
  428. where: {
  429. state: 'accepted',
  430. targetActorId: this.id
  431. }
  432. }
  433. ],
  434. transaction: t
  435. }
  436. return ActorModel.findAll(query)
  437. .then(accounts => accounts.map(a => a.sharedInboxUrl))
  438. }
  439. getFollowingUrl () {
  440. return this.url + '/following'
  441. }
  442. getFollowersUrl () {
  443. return this.url + '/followers'
  444. }
  445. getPlaylistsUrl () {
  446. return this.url + '/playlists'
  447. }
  448. getPublicKeyUrl () {
  449. return this.url + '#main-key'
  450. }
  451. isOwned () {
  452. return this.serverId === null
  453. }
  454. getWebfingerUrl (this: MActorServer) {
  455. return 'acct:' + this.preferredUsername + '@' + this.getHost()
  456. }
  457. getIdentifier () {
  458. return this.Server ? `${this.preferredUsername}@${this.Server.host}` : this.preferredUsername
  459. }
  460. getHost (this: MActorHost) {
  461. return this.Server ? this.Server.host : WEBSERVER.HOST
  462. }
  463. getRedundancyAllowed () {
  464. return this.Server ? this.Server.redundancyAllowed : false
  465. }
  466. getAvatarUrl () {
  467. if (!this.avatarId) return undefined
  468. return WEBSERVER.URL + this.Avatar.getStaticPath()
  469. }
  470. isOutdated () {
  471. if (this.isOwned()) return false
  472. return isOutdated(this, ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL)
  473. }
  474. }