account.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. import { FindOptions, Includeable, IncludeOptions, Op, Transaction, WhereOptions } from 'sequelize'
  2. import {
  3. AllowNull,
  4. BeforeDestroy,
  5. BelongsTo,
  6. Column,
  7. CreatedAt,
  8. DataType,
  9. Default,
  10. DefaultScope,
  11. ForeignKey,
  12. HasMany,
  13. Is,
  14. Model,
  15. Scopes,
  16. Table,
  17. UpdatedAt
  18. } from 'sequelize-typescript'
  19. import { ModelCache } from '@server/models/model-cache'
  20. import { AttributesOnly } from '@shared/typescript-utils'
  21. import { Account, AccountSummary } from '../../../shared/models/actors'
  22. import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts'
  23. import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
  24. import { sendDeleteActor } from '../../lib/activitypub/send/send-delete'
  25. import {
  26. MAccount,
  27. MAccountActor,
  28. MAccountAP,
  29. MAccountDefault,
  30. MAccountFormattable,
  31. MAccountSummaryFormattable,
  32. MChannelActor
  33. } from '../../types/models'
  34. import { ActorModel } from '../actor/actor'
  35. import { ActorFollowModel } from '../actor/actor-follow'
  36. import { ActorImageModel } from '../actor/actor-image'
  37. import { ApplicationModel } from '../application/application'
  38. import { ServerModel } from '../server/server'
  39. import { ServerBlocklistModel } from '../server/server-blocklist'
  40. import { UserModel } from '../user/user'
  41. import { getSort, throwIfNotValid } from '../utils'
  42. import { VideoModel } from '../video/video'
  43. import { VideoChannelModel } from '../video/video-channel'
  44. import { VideoCommentModel } from '../video/video-comment'
  45. import { VideoPlaylistModel } from '../video/video-playlist'
  46. import { AccountBlocklistModel } from './account-blocklist'
  47. export enum ScopeNames {
  48. SUMMARY = 'SUMMARY'
  49. }
  50. export type SummaryOptions = {
  51. actorRequired?: boolean // Default: true
  52. whereActor?: WhereOptions
  53. whereServer?: WhereOptions
  54. withAccountBlockerIds?: number[]
  55. forCount?: boolean
  56. }
  57. @DefaultScope(() => ({
  58. include: [
  59. {
  60. model: ActorModel, // Default scope includes avatar and server
  61. required: true
  62. }
  63. ]
  64. }))
  65. @Scopes(() => ({
  66. [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
  67. const serverInclude: IncludeOptions = {
  68. attributes: [ 'host' ],
  69. model: ServerModel.unscoped(),
  70. required: !!options.whereServer,
  71. where: options.whereServer
  72. }
  73. const actorInclude: Includeable = {
  74. attributes: [ 'id', 'preferredUsername', 'url', 'serverId' ],
  75. model: ActorModel.unscoped(),
  76. required: options.actorRequired ?? true,
  77. where: options.whereActor,
  78. include: [ serverInclude ]
  79. }
  80. if (options.forCount !== true) {
  81. actorInclude.include.push({
  82. model: ActorImageModel,
  83. as: 'Avatars',
  84. required: false
  85. })
  86. }
  87. const queryInclude: Includeable[] = [
  88. actorInclude
  89. ]
  90. const query: FindOptions = {
  91. attributes: [ 'id', 'name', 'actorId' ]
  92. }
  93. if (options.withAccountBlockerIds) {
  94. queryInclude.push({
  95. attributes: [ 'id' ],
  96. model: AccountBlocklistModel.unscoped(),
  97. as: 'BlockedBy',
  98. required: false,
  99. where: {
  100. accountId: {
  101. [Op.in]: options.withAccountBlockerIds
  102. }
  103. }
  104. })
  105. serverInclude.include = [
  106. {
  107. attributes: [ 'id' ],
  108. model: ServerBlocklistModel.unscoped(),
  109. required: false,
  110. where: {
  111. accountId: {
  112. [Op.in]: options.withAccountBlockerIds
  113. }
  114. }
  115. }
  116. ]
  117. }
  118. query.include = queryInclude
  119. return query
  120. }
  121. }))
  122. @Table({
  123. tableName: 'account',
  124. indexes: [
  125. {
  126. fields: [ 'actorId' ],
  127. unique: true
  128. },
  129. {
  130. fields: [ 'applicationId' ]
  131. },
  132. {
  133. fields: [ 'userId' ]
  134. }
  135. ]
  136. })
  137. export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
  138. @AllowNull(false)
  139. @Column
  140. name: string
  141. @AllowNull(true)
  142. @Default(null)
  143. @Is('AccountDescription', value => throwIfNotValid(value, isAccountDescriptionValid, 'description', true))
  144. @Column(DataType.STRING(CONSTRAINTS_FIELDS.USERS.DESCRIPTION.max))
  145. description: string
  146. @CreatedAt
  147. createdAt: Date
  148. @UpdatedAt
  149. updatedAt: Date
  150. @ForeignKey(() => ActorModel)
  151. @Column
  152. actorId: number
  153. @BelongsTo(() => ActorModel, {
  154. foreignKey: {
  155. allowNull: false
  156. },
  157. onDelete: 'cascade'
  158. })
  159. Actor: ActorModel
  160. @ForeignKey(() => UserModel)
  161. @Column
  162. userId: number
  163. @BelongsTo(() => UserModel, {
  164. foreignKey: {
  165. allowNull: true
  166. },
  167. onDelete: 'cascade'
  168. })
  169. User: UserModel
  170. @ForeignKey(() => ApplicationModel)
  171. @Column
  172. applicationId: number
  173. @BelongsTo(() => ApplicationModel, {
  174. foreignKey: {
  175. allowNull: true
  176. },
  177. onDelete: 'cascade'
  178. })
  179. Application: ApplicationModel
  180. @HasMany(() => VideoChannelModel, {
  181. foreignKey: {
  182. allowNull: false
  183. },
  184. onDelete: 'cascade',
  185. hooks: true
  186. })
  187. VideoChannels: VideoChannelModel[]
  188. @HasMany(() => VideoPlaylistModel, {
  189. foreignKey: {
  190. allowNull: false
  191. },
  192. onDelete: 'cascade',
  193. hooks: true
  194. })
  195. VideoPlaylists: VideoPlaylistModel[]
  196. @HasMany(() => VideoCommentModel, {
  197. foreignKey: {
  198. allowNull: true
  199. },
  200. onDelete: 'cascade',
  201. hooks: true
  202. })
  203. VideoComments: VideoCommentModel[]
  204. @HasMany(() => AccountBlocklistModel, {
  205. foreignKey: {
  206. name: 'targetAccountId',
  207. allowNull: false
  208. },
  209. as: 'BlockedBy',
  210. onDelete: 'CASCADE'
  211. })
  212. BlockedBy: AccountBlocklistModel[]
  213. @BeforeDestroy
  214. static async sendDeleteIfOwned (instance: AccountModel, options) {
  215. if (!instance.Actor) {
  216. instance.Actor = await instance.$get('Actor', { transaction: options.transaction })
  217. }
  218. await ActorFollowModel.removeFollowsOf(instance.Actor.id, options.transaction)
  219. if (instance.isOwned()) {
  220. return sendDeleteActor(instance.Actor, options.transaction)
  221. }
  222. return undefined
  223. }
  224. static load (id: number, transaction?: Transaction): Promise<MAccountDefault> {
  225. return AccountModel.findByPk(id, { transaction })
  226. }
  227. static loadByNameWithHost (nameWithHost: string): Promise<MAccountDefault> {
  228. const [ accountName, host ] = nameWithHost.split('@')
  229. if (!host || host === WEBSERVER.HOST) return AccountModel.loadLocalByName(accountName)
  230. return AccountModel.loadByNameAndHost(accountName, host)
  231. }
  232. static loadLocalByName (name: string): Promise<MAccountDefault> {
  233. const fun = () => {
  234. const query = {
  235. where: {
  236. [Op.or]: [
  237. {
  238. userId: {
  239. [Op.ne]: null
  240. }
  241. },
  242. {
  243. applicationId: {
  244. [Op.ne]: null
  245. }
  246. }
  247. ]
  248. },
  249. include: [
  250. {
  251. model: ActorModel,
  252. required: true,
  253. where: {
  254. preferredUsername: name
  255. }
  256. }
  257. ]
  258. }
  259. return AccountModel.findOne(query)
  260. }
  261. return ModelCache.Instance.doCache({
  262. cacheType: 'local-account-name',
  263. key: name,
  264. fun,
  265. // The server actor never change, so we can easily cache it
  266. whitelist: () => name === SERVER_ACTOR_NAME
  267. })
  268. }
  269. static loadByNameAndHost (name: string, host: string): Promise<MAccountDefault> {
  270. const query = {
  271. include: [
  272. {
  273. model: ActorModel,
  274. required: true,
  275. where: {
  276. preferredUsername: name
  277. },
  278. include: [
  279. {
  280. model: ServerModel,
  281. required: true,
  282. where: {
  283. host
  284. }
  285. }
  286. ]
  287. }
  288. ]
  289. }
  290. return AccountModel.findOne(query)
  291. }
  292. static loadByUrl (url: string, transaction?: Transaction): Promise<MAccountDefault> {
  293. const query = {
  294. include: [
  295. {
  296. model: ActorModel,
  297. required: true,
  298. where: {
  299. url
  300. }
  301. }
  302. ],
  303. transaction
  304. }
  305. return AccountModel.findOne(query)
  306. }
  307. static listForApi (start: number, count: number, sort: string) {
  308. const query = {
  309. offset: start,
  310. limit: count,
  311. order: getSort(sort)
  312. }
  313. return Promise.all([
  314. AccountModel.count(),
  315. AccountModel.findAll(query)
  316. ]).then(([ total, data ]) => ({ total, data }))
  317. }
  318. static loadAccountIdFromVideo (videoId: number): Promise<MAccount> {
  319. const query = {
  320. include: [
  321. {
  322. attributes: [ 'id', 'accountId' ],
  323. model: VideoChannelModel.unscoped(),
  324. required: true,
  325. include: [
  326. {
  327. attributes: [ 'id', 'channelId' ],
  328. model: VideoModel.unscoped(),
  329. where: {
  330. id: videoId
  331. }
  332. }
  333. ]
  334. }
  335. ]
  336. }
  337. return AccountModel.findOne(query)
  338. }
  339. static listLocalsForSitemap (sort: string): Promise<MAccountActor[]> {
  340. const query = {
  341. attributes: [ ],
  342. offset: 0,
  343. order: getSort(sort),
  344. include: [
  345. {
  346. attributes: [ 'preferredUsername', 'serverId' ],
  347. model: ActorModel.unscoped(),
  348. where: {
  349. serverId: null
  350. }
  351. }
  352. ]
  353. }
  354. return AccountModel
  355. .unscoped()
  356. .findAll(query)
  357. }
  358. getClientUrl () {
  359. return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier()
  360. }
  361. toFormattedJSON (this: MAccountFormattable): Account {
  362. return {
  363. ...this.Actor.toFormattedJSON(),
  364. id: this.id,
  365. displayName: this.getDisplayName(),
  366. description: this.description,
  367. updatedAt: this.updatedAt,
  368. userId: this.userId ?? undefined
  369. }
  370. }
  371. toFormattedSummaryJSON (this: MAccountSummaryFormattable): AccountSummary {
  372. const actor = this.Actor.toFormattedSummaryJSON()
  373. return {
  374. id: this.id,
  375. displayName: this.getDisplayName(),
  376. name: actor.name,
  377. url: actor.url,
  378. host: actor.host,
  379. avatars: actor.avatars,
  380. // TODO: remove, deprecated in 4.2
  381. avatar: actor.avatar
  382. }
  383. }
  384. toActivityPubObject (this: MAccountAP) {
  385. const obj = this.Actor.toActivityPubObject(this.name)
  386. return Object.assign(obj, {
  387. summary: this.description
  388. })
  389. }
  390. isOwned () {
  391. return this.Actor.isOwned()
  392. }
  393. isOutdated () {
  394. return this.Actor.isOutdated()
  395. }
  396. getDisplayName () {
  397. return this.name
  398. }
  399. getLocalUrl (this: MAccountActor | MChannelActor) {
  400. return WEBSERVER.URL + `/accounts/` + this.Actor.preferredUsername
  401. }
  402. isBlocked () {
  403. return this.BlockedBy && this.BlockedBy.length !== 0
  404. }
  405. }