ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1050 lines
28 KiB

  1. import { forceNumber, hasUserRight, USER_ROLE_LABELS } from '@peertube/peertube-core-utils'
  2. import {
  3. AbuseState,
  4. MyUser,
  5. User,
  6. UserAdminFlag,
  7. UserRightType,
  8. VideoPlaylistType,
  9. type NSFWPolicyType,
  10. type UserAdminFlagType,
  11. type UserRoleType,
  12. UserRole
  13. } from '@peertube/peertube-models'
  14. import { TokensCache } from '@server/lib/auth/tokens-cache.js'
  15. import { LiveQuotaStore } from '@server/lib/live/index.js'
  16. import {
  17. MMyUserFormattable,
  18. MUser,
  19. MUserDefault,
  20. MUserFormattable,
  21. MUserNotifSettingChannelDefault,
  22. MUserWithNotificationSetting
  23. } from '@server/types/models/index.js'
  24. import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } from 'sequelize'
  25. import {
  26. AfterDestroy,
  27. AfterUpdate,
  28. AllowNull,
  29. BeforeCreate,
  30. BeforeUpdate,
  31. Column,
  32. CreatedAt,
  33. DataType,
  34. Default,
  35. DefaultScope,
  36. HasMany,
  37. HasOne,
  38. Is,
  39. IsEmail,
  40. IsUUID, Scopes,
  41. Table,
  42. UpdatedAt
  43. } from 'sequelize-typescript'
  44. import { isThemeNameValid } from '../../helpers/custom-validators/plugins.js'
  45. import {
  46. isUserAdminFlagsValid,
  47. isUserAutoPlayNextVideoPlaylistValid,
  48. isUserAutoPlayNextVideoValid,
  49. isUserAutoPlayVideoValid,
  50. isUserBlockedReasonValid,
  51. isUserBlockedValid,
  52. isUserEmailVerifiedValid,
  53. isUserNoModal,
  54. isUserNSFWPolicyValid,
  55. isUserP2PEnabledValid,
  56. isUserPasswordValid,
  57. isUserRoleValid,
  58. isUserVideoLanguages,
  59. isUserVideoQuotaDailyValid,
  60. isUserVideoQuotaValid,
  61. isUserVideosHistoryEnabledValid
  62. } from '../../helpers/custom-validators/users.js'
  63. import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto.js'
  64. import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants.js'
  65. import { getThemeOrDefault } from '../../lib/plugins/theme-utils.js'
  66. import { AccountModel } from '../account/account.js'
  67. import { ActorFollowModel } from '../actor/actor-follow.js'
  68. import { ActorImageModel } from '../actor/actor-image.js'
  69. import { ActorModel } from '../actor/actor.js'
  70. import { OAuthTokenModel } from '../oauth/oauth-token.js'
  71. import { getAdminUsersSort, parseAggregateResult, SequelizeModel, throwIfNotValid } from '../shared/index.js'
  72. import { VideoChannelModel } from '../video/video-channel.js'
  73. import { VideoImportModel } from '../video/video-import.js'
  74. import { VideoLiveModel } from '../video/video-live.js'
  75. import { VideoPlaylistModel } from '../video/video-playlist.js'
  76. import { VideoModel } from '../video/video.js'
  77. import { UserNotificationSettingModel } from './user-notification-setting.js'
  78. import { UserExportModel } from './user-export.js'
  79. enum ScopeNames {
  80. FOR_ME_API = 'FOR_ME_API',
  81. WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS',
  82. WITH_QUOTA = 'WITH_QUOTA',
  83. WITH_TOTAL_FILE_SIZES = 'WITH_TOTAL_FILE_SIZES',
  84. WITH_STATS = 'WITH_STATS'
  85. }
  86. @DefaultScope(() => ({
  87. include: [
  88. {
  89. model: AccountModel,
  90. required: true
  91. },
  92. {
  93. model: UserNotificationSettingModel,
  94. required: true
  95. }
  96. ]
  97. }))
  98. @Scopes(() => ({
  99. [ScopeNames.FOR_ME_API]: {
  100. include: [
  101. {
  102. model: AccountModel,
  103. include: [
  104. {
  105. model: VideoChannelModel.unscoped(),
  106. include: [
  107. {
  108. model: ActorModel,
  109. required: true,
  110. include: [
  111. {
  112. model: ActorImageModel,
  113. as: 'Banners',
  114. required: false
  115. }
  116. ]
  117. }
  118. ]
  119. },
  120. {
  121. attributes: [ 'id', 'name', 'type' ],
  122. model: VideoPlaylistModel.unscoped(),
  123. required: true,
  124. where: {
  125. type: {
  126. [Op.ne]: VideoPlaylistType.REGULAR
  127. }
  128. }
  129. }
  130. ]
  131. },
  132. {
  133. model: UserNotificationSettingModel,
  134. required: true
  135. }
  136. ]
  137. },
  138. [ScopeNames.WITH_VIDEOCHANNELS]: {
  139. include: [
  140. {
  141. model: AccountModel,
  142. include: [
  143. {
  144. model: VideoChannelModel
  145. },
  146. {
  147. attributes: [ 'id', 'name', 'type' ],
  148. model: VideoPlaylistModel.unscoped(),
  149. required: true,
  150. where: {
  151. type: {
  152. [Op.ne]: VideoPlaylistType.REGULAR
  153. }
  154. }
  155. }
  156. ]
  157. }
  158. ]
  159. },
  160. [ScopeNames.WITH_QUOTA]: {
  161. attributes: {
  162. include: [
  163. [
  164. literal(
  165. '(' +
  166. UserModel.generateUserQuotaBaseSQL({
  167. whereUserId: '"UserModel"."id"',
  168. daily: false,
  169. onlyMaxResolution: true
  170. }) +
  171. ')'
  172. ),
  173. 'videoQuotaUsed'
  174. ],
  175. [
  176. literal(
  177. '(' +
  178. UserModel.generateUserQuotaBaseSQL({
  179. whereUserId: '"UserModel"."id"',
  180. daily: true,
  181. onlyMaxResolution: true
  182. }) +
  183. ')'
  184. ),
  185. 'videoQuotaUsedDaily'
  186. ]
  187. ]
  188. }
  189. },
  190. [ScopeNames.WITH_TOTAL_FILE_SIZES]: {
  191. attributes: {
  192. include: [
  193. [
  194. literal(
  195. '(' +
  196. UserModel.generateUserQuotaBaseSQL({
  197. whereUserId: '"UserModel"."id"',
  198. daily: false,
  199. onlyMaxResolution: false
  200. }) +
  201. ')'
  202. ),
  203. 'totalVideoFileSize'
  204. ]
  205. ]
  206. }
  207. },
  208. [ScopeNames.WITH_STATS]: {
  209. attributes: {
  210. include: [
  211. [
  212. literal(
  213. '(' +
  214. 'SELECT COUNT("video"."id") ' +
  215. 'FROM "video" ' +
  216. 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
  217. 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
  218. 'WHERE "account"."userId" = "UserModel"."id"' +
  219. ')'
  220. ),
  221. 'videosCount'
  222. ],
  223. [
  224. literal(
  225. '(' +
  226. `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
  227. 'FROM (' +
  228. 'SELECT COUNT("abuse"."id") AS "abuses", ' +
  229. `COUNT("abuse"."id") FILTER (WHERE "abuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
  230. 'FROM "abuse" ' +
  231. 'INNER JOIN "account" ON "account"."id" = "abuse"."flaggedAccountId" ' +
  232. 'WHERE "account"."userId" = "UserModel"."id"' +
  233. ') t' +
  234. ')'
  235. ),
  236. 'abusesCount'
  237. ],
  238. [
  239. literal(
  240. '(' +
  241. 'SELECT COUNT("abuse"."id") ' +
  242. 'FROM "abuse" ' +
  243. 'INNER JOIN "account" ON "account"."id" = "abuse"."reporterAccountId" ' +
  244. 'WHERE "account"."userId" = "UserModel"."id"' +
  245. ')'
  246. ),
  247. 'abusesCreatedCount'
  248. ],
  249. [
  250. literal(
  251. '(' +
  252. 'SELECT COUNT("videoComment"."id") ' +
  253. 'FROM "videoComment" ' +
  254. 'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' +
  255. 'WHERE "account"."userId" = "UserModel"."id"' +
  256. ')'
  257. ),
  258. 'videoCommentsCount'
  259. ]
  260. ]
  261. }
  262. }
  263. }))
  264. @Table({
  265. tableName: 'user',
  266. indexes: [
  267. {
  268. fields: [ 'username' ],
  269. unique: true
  270. },
  271. {
  272. fields: [ 'email' ],
  273. unique: true
  274. }
  275. ]
  276. })
  277. export class UserModel extends SequelizeModel<UserModel> {
  278. @AllowNull(true)
  279. @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
  280. @Column
  281. password: string
  282. @AllowNull(false)
  283. @Column
  284. username: string
  285. @AllowNull(false)
  286. @IsEmail
  287. @Column(DataType.STRING(400))
  288. email: string
  289. @AllowNull(true)
  290. @IsEmail
  291. @Column(DataType.STRING(400))
  292. pendingEmail: string
  293. @AllowNull(true)
  294. @Default(null)
  295. @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
  296. @Column
  297. emailVerified: boolean
  298. @AllowNull(false)
  299. @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
  300. @Column(DataType.ENUM(...Object.values(NSFW_POLICY_TYPES)))
  301. nsfwPolicy: NSFWPolicyType
  302. @AllowNull(false)
  303. @Is('p2pEnabled', value => throwIfNotValid(value, isUserP2PEnabledValid, 'P2P enabled'))
  304. @Column
  305. p2pEnabled: boolean
  306. @AllowNull(false)
  307. @Default(true)
  308. @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
  309. @Column
  310. videosHistoryEnabled: boolean
  311. @AllowNull(false)
  312. @Default(true)
  313. @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
  314. @Column
  315. autoPlayVideo: boolean
  316. @AllowNull(false)
  317. @Default(false)
  318. @Is('UserAutoPlayNextVideo', value => throwIfNotValid(value, isUserAutoPlayNextVideoValid, 'auto play next video boolean'))
  319. @Column
  320. autoPlayNextVideo: boolean
  321. @AllowNull(false)
  322. @Default(true)
  323. @Is(
  324. 'UserAutoPlayNextVideoPlaylist',
  325. value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean')
  326. )
  327. @Column
  328. autoPlayNextVideoPlaylist: boolean
  329. @AllowNull(true)
  330. @Default(null)
  331. @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
  332. @Column(DataType.ARRAY(DataType.STRING))
  333. videoLanguages: string[]
  334. @AllowNull(false)
  335. @Default(UserAdminFlag.NONE)
  336. @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
  337. @Column
  338. adminFlags?: UserAdminFlagType
  339. @AllowNull(false)
  340. @Default(false)
  341. @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
  342. @Column
  343. blocked: boolean
  344. @AllowNull(true)
  345. @Default(null)
  346. @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true))
  347. @Column
  348. blockedReason: string
  349. @AllowNull(false)
  350. @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
  351. @Column
  352. role: UserRoleType
  353. @AllowNull(false)
  354. @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
  355. @Column(DataType.BIGINT)
  356. videoQuota: number
  357. @AllowNull(false)
  358. @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
  359. @Column(DataType.BIGINT)
  360. videoQuotaDaily: number
  361. @AllowNull(false)
  362. @Default(DEFAULT_USER_THEME_NAME)
  363. @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme'))
  364. @Column
  365. theme: string
  366. @AllowNull(false)
  367. @Default(false)
  368. @Is(
  369. 'UserNoInstanceConfigWarningModal',
  370. value => throwIfNotValid(value, isUserNoModal, 'no instance config warning modal')
  371. )
  372. @Column
  373. noInstanceConfigWarningModal: boolean
  374. @AllowNull(false)
  375. @Default(false)
  376. @Is(
  377. 'UserNoWelcomeModal',
  378. value => throwIfNotValid(value, isUserNoModal, 'no welcome modal')
  379. )
  380. @Column
  381. noWelcomeModal: boolean
  382. @AllowNull(false)
  383. @Default(false)
  384. @Is(
  385. 'UserNoAccountSetupWarningModal',
  386. value => throwIfNotValid(value, isUserNoModal, 'no account setup warning modal')
  387. )
  388. @Column
  389. noAccountSetupWarningModal: boolean
  390. @AllowNull(true)
  391. @Default(null)
  392. @Column
  393. pluginAuth: string
  394. @AllowNull(false)
  395. @Default(DataType.UUIDV4)
  396. @IsUUID(4)
  397. @Column(DataType.UUID)
  398. feedToken: string
  399. @AllowNull(true)
  400. @Default(null)
  401. @Column
  402. lastLoginDate: Date
  403. @AllowNull(false)
  404. @Default(false)
  405. @Column
  406. emailPublic: boolean
  407. @AllowNull(true)
  408. @Default(null)
  409. @Column
  410. otpSecret: string
  411. @CreatedAt
  412. createdAt: Date
  413. @UpdatedAt
  414. updatedAt: Date
  415. @HasOne(() => AccountModel, {
  416. foreignKey: 'userId',
  417. onDelete: 'cascade',
  418. hooks: true
  419. })
  420. Account: Awaited<AccountModel>
  421. @HasOne(() => UserNotificationSettingModel, {
  422. foreignKey: 'userId',
  423. onDelete: 'cascade',
  424. hooks: true
  425. })
  426. NotificationSetting: Awaited<UserNotificationSettingModel>
  427. @HasMany(() => VideoImportModel, {
  428. foreignKey: 'userId',
  429. onDelete: 'cascade'
  430. })
  431. VideoImports: Awaited<VideoImportModel>[]
  432. @HasMany(() => OAuthTokenModel, {
  433. foreignKey: 'userId',
  434. onDelete: 'cascade'
  435. })
  436. OAuthTokens: Awaited<OAuthTokenModel>[]
  437. @HasMany(() => UserExportModel, {
  438. foreignKey: 'userId',
  439. onDelete: 'cascade',
  440. hooks: true
  441. })
  442. UserExports: Awaited<UserExportModel>[]
  443. // Used if we already set an encrypted password in user model
  444. skipPasswordEncryption = false
  445. @BeforeCreate
  446. @BeforeUpdate
  447. static async cryptPasswordIfNeeded (instance: UserModel) {
  448. if (instance.skipPasswordEncryption) return
  449. if (!instance.changed('password')) return
  450. if (!instance.password) return
  451. instance.password = await cryptPassword(instance.password)
  452. }
  453. @AfterUpdate
  454. @AfterDestroy
  455. static removeTokenCache (instance: UserModel) {
  456. return TokensCache.Instance.clearCacheByUserId(instance.id)
  457. }
  458. static countTotal () {
  459. return UserModel.unscoped().count()
  460. }
  461. static listForAdminApi (parameters: {
  462. start: number
  463. count: number
  464. sort: string
  465. search?: string
  466. blocked?: boolean
  467. }) {
  468. const { start, count, sort, search, blocked } = parameters
  469. const where: WhereOptions = {}
  470. if (search) {
  471. Object.assign(where, {
  472. [Op.or]: [
  473. {
  474. email: {
  475. [Op.iLike]: '%' + search + '%'
  476. }
  477. },
  478. {
  479. username: {
  480. [Op.iLike]: '%' + search + '%'
  481. }
  482. }
  483. ]
  484. })
  485. }
  486. if (blocked !== undefined) {
  487. Object.assign(where, { blocked })
  488. }
  489. const query: FindOptions = {
  490. offset: start,
  491. limit: count,
  492. order: getAdminUsersSort(sort),
  493. where
  494. }
  495. return Promise.all([
  496. UserModel.unscoped().count(query),
  497. UserModel.scope([ 'defaultScope', ScopeNames.WITH_QUOTA, ScopeNames.WITH_TOTAL_FILE_SIZES ]).findAll(query)
  498. ]).then(([ total, data ]) => ({ total, data }))
  499. }
  500. static listWithRight (right: UserRightType): Promise<MUserDefault[]> {
  501. const roles = Object.keys(USER_ROLE_LABELS)
  502. .map(k => parseInt(k, 10) as UserRoleType)
  503. .filter(role => hasUserRight(role, right))
  504. const query = {
  505. where: {
  506. role: {
  507. [Op.in]: roles
  508. }
  509. }
  510. }
  511. return UserModel.findAll(query)
  512. }
  513. static listUserSubscribersOf (actorId: number): Promise<MUserWithNotificationSetting[]> {
  514. const query = {
  515. include: [
  516. {
  517. model: UserNotificationSettingModel.unscoped(),
  518. required: true
  519. },
  520. {
  521. attributes: [ 'userId' ],
  522. model: AccountModel.unscoped(),
  523. required: true,
  524. include: [
  525. {
  526. attributes: [],
  527. model: ActorModel.unscoped(),
  528. required: true,
  529. where: {
  530. serverId: null
  531. },
  532. include: [
  533. {
  534. attributes: [],
  535. as: 'ActorFollowings',
  536. model: ActorFollowModel.unscoped(),
  537. required: true,
  538. where: {
  539. state: 'accepted',
  540. targetActorId: actorId
  541. }
  542. }
  543. ]
  544. }
  545. ]
  546. }
  547. ]
  548. }
  549. return UserModel.unscoped().findAll(query)
  550. }
  551. static listByUsernames (usernames: string[]): Promise<MUserDefault[]> {
  552. const query = {
  553. where: {
  554. username: usernames
  555. }
  556. }
  557. return UserModel.findAll(query)
  558. }
  559. static loadById (id: number): Promise<MUser> {
  560. return UserModel.unscoped().findByPk(id)
  561. }
  562. static loadByIdFull (id: number): Promise<MUserDefault> {
  563. return UserModel.findByPk(id)
  564. }
  565. static loadByIdWithChannels (id: number, withStats = false): Promise<MUserDefault> {
  566. const scopes = [
  567. ScopeNames.WITH_VIDEOCHANNELS
  568. ]
  569. if (withStats) {
  570. scopes.push(ScopeNames.WITH_QUOTA)
  571. scopes.push(ScopeNames.WITH_STATS)
  572. scopes.push(ScopeNames.WITH_TOTAL_FILE_SIZES)
  573. }
  574. return UserModel.scope(scopes).findByPk(id)
  575. }
  576. static loadByUsername (username: string): Promise<MUserDefault> {
  577. const query = {
  578. where: {
  579. username
  580. }
  581. }
  582. return UserModel.findOne(query)
  583. }
  584. static loadForMeAPI (id: number): Promise<MUserNotifSettingChannelDefault> {
  585. const query = {
  586. where: {
  587. id
  588. }
  589. }
  590. return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query)
  591. }
  592. static loadByEmail (email: string): Promise<MUserDefault> {
  593. const query = {
  594. where: {
  595. email
  596. }
  597. }
  598. return UserModel.findOne(query)
  599. }
  600. static loadByUsernameOrEmail (username: string, email?: string): Promise<MUserDefault> {
  601. if (!email) email = username
  602. const query = {
  603. where: {
  604. [Op.or]: [
  605. where(fn('lower', col('username')), fn('lower', username) as any),
  606. { email }
  607. ]
  608. }
  609. }
  610. return UserModel.findOne(query)
  611. }
  612. static loadByVideoId (videoId: number): Promise<MUserDefault> {
  613. const query = {
  614. include: [
  615. {
  616. required: true,
  617. attributes: [ 'id' ],
  618. model: AccountModel.unscoped(),
  619. include: [
  620. {
  621. required: true,
  622. attributes: [ 'id' ],
  623. model: VideoChannelModel.unscoped(),
  624. include: [
  625. {
  626. required: true,
  627. attributes: [ 'id' ],
  628. model: VideoModel.unscoped(),
  629. where: {
  630. id: videoId
  631. }
  632. }
  633. ]
  634. }
  635. ]
  636. }
  637. ]
  638. }
  639. return UserModel.findOne(query)
  640. }
  641. static loadByVideoImportId (videoImportId: number): Promise<MUserDefault> {
  642. const query = {
  643. include: [
  644. {
  645. required: true,
  646. attributes: [ 'id' ],
  647. model: VideoImportModel.unscoped(),
  648. where: {
  649. id: videoImportId
  650. }
  651. }
  652. ]
  653. }
  654. return UserModel.findOne(query)
  655. }
  656. static loadByChannelActorId (videoChannelActorId: number): Promise<MUserDefault> {
  657. const query = {
  658. include: [
  659. {
  660. required: true,
  661. attributes: [ 'id' ],
  662. model: AccountModel.unscoped(),
  663. include: [
  664. {
  665. required: true,
  666. attributes: [ 'id' ],
  667. model: VideoChannelModel.unscoped(),
  668. where: {
  669. actorId: videoChannelActorId
  670. }
  671. }
  672. ]
  673. }
  674. ]
  675. }
  676. return UserModel.findOne(query)
  677. }
  678. static loadByAccountId (accountId: number): Promise<MUserDefault> {
  679. const query = {
  680. include: [
  681. {
  682. required: true,
  683. attributes: [ 'id' ],
  684. model: AccountModel.unscoped(),
  685. where: {
  686. id: accountId
  687. }
  688. }
  689. ]
  690. }
  691. return UserModel.findOne(query)
  692. }
  693. static loadByAccountActorId (accountActorId: number): Promise<MUserDefault> {
  694. const query = {
  695. include: [
  696. {
  697. required: true,
  698. attributes: [ 'id' ],
  699. model: AccountModel.unscoped(),
  700. where: {
  701. actorId: accountActorId
  702. }
  703. }
  704. ]
  705. }
  706. return UserModel.findOne(query)
  707. }
  708. static loadByLiveId (liveId: number): Promise<MUser> {
  709. const query = {
  710. include: [
  711. {
  712. attributes: [ 'id' ],
  713. model: AccountModel.unscoped(),
  714. required: true,
  715. include: [
  716. {
  717. attributes: [ 'id' ],
  718. model: VideoChannelModel.unscoped(),
  719. required: true,
  720. include: [
  721. {
  722. attributes: [ 'id' ],
  723. model: VideoModel.unscoped(),
  724. required: true,
  725. include: [
  726. {
  727. attributes: [],
  728. model: VideoLiveModel.unscoped(),
  729. required: true,
  730. where: {
  731. id: liveId
  732. }
  733. }
  734. ]
  735. }
  736. ]
  737. }
  738. ]
  739. }
  740. ]
  741. }
  742. return UserModel.unscoped().findOne(query)
  743. }
  744. static generateUserQuotaBaseSQL (options: {
  745. daily: boolean
  746. whereUserId: '$userId' | '"UserModel"."id"'
  747. onlyMaxResolution: boolean
  748. }) {
  749. const { daily, whereUserId, onlyMaxResolution } = options
  750. const andWhere = daily === true
  751. ? 'AND "video"."createdAt" > now() - interval \'24 hours\''
  752. : ''
  753. const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
  754. 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
  755. `WHERE "account"."userId" = ${whereUserId} ${andWhere}`
  756. const webVideoFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
  757. 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" AND "video"."isLive" IS FALSE ' +
  758. videoChannelJoin
  759. const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
  760. 'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' +
  761. 'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" AND "video"."isLive" IS FALSE ' +
  762. videoChannelJoin
  763. const sizeSelect = onlyMaxResolution
  764. ? 'MAX("t1"."size")'
  765. : 'SUM("t1"."size")'
  766. return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' +
  767. 'FROM (' +
  768. `SELECT ${sizeSelect} AS "size" FROM (${webVideoFiles} UNION ${hlsFiles}) t1 ` +
  769. 'GROUP BY "t1"."videoId"' +
  770. ') t2'
  771. }
  772. static async getUserQuota (options: {
  773. userId: number
  774. daily: boolean
  775. }) {
  776. const { daily, userId } = options
  777. const sql = this.generateUserQuotaBaseSQL({ daily, whereUserId: '$userId', onlyMaxResolution: true })
  778. const queryOptions = {
  779. bind: { userId },
  780. type: QueryTypes.SELECT as QueryTypes.SELECT
  781. }
  782. const [ { total } ] = await UserModel.sequelize.query<{ total: string }>(sql, queryOptions)
  783. if (!total) return 0
  784. return parseInt(total, 10)
  785. }
  786. static getStats () {
  787. const query = `SELECT ` +
  788. `COUNT(*) AS "totalUsers", ` +
  789. `COUNT(*) FILTER (WHERE "lastLoginDate" > NOW() - INTERVAL '1d') AS "totalDailyActiveUsers", ` +
  790. `COUNT(*) FILTER (WHERE "lastLoginDate" > NOW() - INTERVAL '7d') AS "totalWeeklyActiveUsers", ` +
  791. `COUNT(*) FILTER (WHERE "lastLoginDate" > NOW() - INTERVAL '30d') AS "totalMonthlyActiveUsers", ` +
  792. `COUNT(*) FILTER (WHERE "lastLoginDate" > NOW() - INTERVAL '180d') AS "totalHalfYearActiveUsers", ` +
  793. `COUNT(*) FILTER (WHERE "role" = ${UserRole.MODERATOR}) AS "totalModerators", ` +
  794. `COUNT(*) FILTER (WHERE "role" = ${UserRole.ADMINISTRATOR}) AS "totalAdmins" ` +
  795. `FROM "user"`
  796. return UserModel.sequelize.query<any>(query, {
  797. type: QueryTypes.SELECT,
  798. raw: true
  799. }).then(([ row ]) => {
  800. return {
  801. totalUsers: parseAggregateResult(row.totalUsers),
  802. totalDailyActiveUsers: parseAggregateResult(row.totalDailyActiveUsers),
  803. totalWeeklyActiveUsers: parseAggregateResult(row.totalWeeklyActiveUsers),
  804. totalMonthlyActiveUsers: parseAggregateResult(row.totalMonthlyActiveUsers),
  805. totalHalfYearActiveUsers: parseAggregateResult(row.totalHalfYearActiveUsers),
  806. totalModerators: parseAggregateResult(row.totalModerators),
  807. totalAdmins: parseAggregateResult(row.totalAdmins)
  808. }
  809. })
  810. }
  811. static autoComplete (search: string) {
  812. const query = {
  813. where: {
  814. username: {
  815. [Op.like]: `%${search}%`
  816. }
  817. },
  818. limit: 10
  819. }
  820. return UserModel.findAll(query)
  821. .then(u => u.map(u => u.username))
  822. }
  823. hasRight (right: UserRightType) {
  824. return hasUserRight(this.role, right)
  825. }
  826. hasAdminFlag (flag: UserAdminFlagType) {
  827. return this.adminFlags & flag
  828. }
  829. isPasswordMatch (password: string) {
  830. if (!password || !this.password) return false
  831. return comparePassword(password, this.password)
  832. }
  833. toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
  834. const videoQuotaUsed = this.get('videoQuotaUsed')
  835. const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
  836. const videosCount = this.get('videosCount')
  837. const [ abusesCount, abusesAcceptedCount ] = (this.get('abusesCount') as string || ':').split(':')
  838. const abusesCreatedCount = this.get('abusesCreatedCount')
  839. const videoCommentsCount = this.get('videoCommentsCount')
  840. const totalVideoFileSize = this.get('totalVideoFileSize')
  841. const json: User = {
  842. id: this.id,
  843. username: this.username,
  844. email: this.email,
  845. theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
  846. pendingEmail: this.pendingEmail,
  847. emailPublic: this.emailPublic,
  848. emailVerified: this.emailVerified,
  849. nsfwPolicy: this.nsfwPolicy,
  850. p2pEnabled: this.p2pEnabled,
  851. videosHistoryEnabled: this.videosHistoryEnabled,
  852. autoPlayVideo: this.autoPlayVideo,
  853. autoPlayNextVideo: this.autoPlayNextVideo,
  854. autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist,
  855. videoLanguages: this.videoLanguages,
  856. role: {
  857. id: this.role,
  858. label: USER_ROLE_LABELS[this.role]
  859. },
  860. videoQuota: this.videoQuota,
  861. videoQuotaDaily: this.videoQuotaDaily,
  862. totalVideoFileSize: totalVideoFileSize !== undefined
  863. ? forceNumber(totalVideoFileSize)
  864. : undefined,
  865. videoQuotaUsed: videoQuotaUsed !== undefined
  866. ? forceNumber(videoQuotaUsed) + LiveQuotaStore.Instance.getLiveQuotaOfUser(this.id)
  867. : undefined,
  868. videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
  869. ? forceNumber(videoQuotaUsedDaily) + LiveQuotaStore.Instance.getLiveQuotaOfUser(this.id)
  870. : undefined,
  871. videosCount: videosCount !== undefined
  872. ? forceNumber(videosCount)
  873. : undefined,
  874. abusesCount: abusesCount
  875. ? forceNumber(abusesCount)
  876. : undefined,
  877. abusesAcceptedCount: abusesAcceptedCount
  878. ? forceNumber(abusesAcceptedCount)
  879. : undefined,
  880. abusesCreatedCount: abusesCreatedCount !== undefined
  881. ? forceNumber(abusesCreatedCount)
  882. : undefined,
  883. videoCommentsCount: videoCommentsCount !== undefined
  884. ? forceNumber(videoCommentsCount)
  885. : undefined,
  886. noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
  887. noWelcomeModal: this.noWelcomeModal,
  888. noAccountSetupWarningModal: this.noAccountSetupWarningModal,
  889. blocked: this.blocked,
  890. blockedReason: this.blockedReason,
  891. account: this.Account.toFormattedJSON(),
  892. notificationSettings: this.NotificationSetting
  893. ? this.NotificationSetting.toFormattedJSON()
  894. : undefined,
  895. videoChannels: [],
  896. createdAt: this.createdAt,
  897. pluginAuth: this.pluginAuth,
  898. lastLoginDate: this.lastLoginDate,
  899. twoFactorEnabled: !!this.otpSecret
  900. }
  901. if (parameters.withAdminFlags) {
  902. Object.assign(json, { adminFlags: this.adminFlags })
  903. }
  904. if (Array.isArray(this.Account.VideoChannels) === true) {
  905. json.videoChannels = this.Account.VideoChannels
  906. .map(c => c.toFormattedJSON())
  907. .sort((v1, v2) => {
  908. if (v1.createdAt < v2.createdAt) return -1
  909. if (v1.createdAt === v2.createdAt) return 0
  910. return 1
  911. })
  912. }
  913. return json
  914. }
  915. toMeFormattedJSON (this: MMyUserFormattable): MyUser {
  916. const formatted = this.toFormattedJSON({ withAdminFlags: true })
  917. const specialPlaylists = this.Account.VideoPlaylists
  918. .map(p => ({ id: p.id, name: p.name, type: p.type }))
  919. return Object.assign(formatted, { specialPlaylists })
  920. }
  921. }