user.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898
  1. import { col, FindOptions, fn, literal, Op, QueryTypes, where, WhereOptions } from 'sequelize'
  2. import {
  3. AfterDestroy,
  4. AfterUpdate,
  5. AllowNull,
  6. BeforeCreate,
  7. BeforeUpdate,
  8. Column,
  9. CreatedAt,
  10. DataType,
  11. Default,
  12. DefaultScope,
  13. HasMany,
  14. HasOne,
  15. Is,
  16. IsEmail,
  17. Model,
  18. Scopes,
  19. Table,
  20. UpdatedAt
  21. } from 'sequelize-typescript'
  22. import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
  23. import { User, UserRole } from '../../../shared/models/users'
  24. import {
  25. isNoInstanceConfigWarningModal,
  26. isNoWelcomeModal,
  27. isUserAdminFlagsValid,
  28. isUserAutoPlayNextVideoPlaylistValid,
  29. isUserAutoPlayNextVideoValid,
  30. isUserAutoPlayVideoValid,
  31. isUserBlockedReasonValid,
  32. isUserBlockedValid,
  33. isUserEmailVerifiedValid,
  34. isUserNSFWPolicyValid,
  35. isUserPasswordValid,
  36. isUserRoleValid,
  37. isUserUsernameValid,
  38. isUserVideoLanguages,
  39. isUserVideoQuotaDailyValid,
  40. isUserVideoQuotaValid,
  41. isUserVideosHistoryEnabledValid,
  42. isUserWebTorrentEnabledValid
  43. } from '../../helpers/custom-validators/users'
  44. import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
  45. import { OAuthTokenModel } from '../oauth/oauth-token'
  46. import { getSort, throwIfNotValid } from '../utils'
  47. import { VideoChannelModel } from '../video/video-channel'
  48. import { VideoPlaylistModel } from '../video/video-playlist'
  49. import { AccountModel } from './account'
  50. import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
  51. import { values } from 'lodash'
  52. import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
  53. import { clearCacheByUserId } from '../../lib/oauth-model'
  54. import { UserNotificationSettingModel } from './user-notification-setting'
  55. import { VideoModel } from '../video/video'
  56. import { ActorModel } from '../activitypub/actor'
  57. import { ActorFollowModel } from '../activitypub/actor-follow'
  58. import { VideoImportModel } from '../video/video-import'
  59. import { UserAdminFlag } from '../../../shared/models/users/user-flag.model'
  60. import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
  61. import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
  62. import * as Bluebird from 'bluebird'
  63. import {
  64. MMyUserFormattable,
  65. MUserDefault,
  66. MUserFormattable,
  67. MUserId,
  68. MUserNotifSettingChannelDefault,
  69. MUserWithNotificationSetting,
  70. MVideoFullLight
  71. } from '@server/typings/models'
  72. enum ScopeNames {
  73. FOR_ME_API = 'FOR_ME_API',
  74. WITH_VIDEOCHANNELS = 'WITH_VIDEOCHANNELS',
  75. WITH_STATS = 'WITH_STATS'
  76. }
  77. @DefaultScope(() => ({
  78. include: [
  79. {
  80. model: AccountModel,
  81. required: true
  82. },
  83. {
  84. model: UserNotificationSettingModel,
  85. required: true
  86. }
  87. ]
  88. }))
  89. @Scopes(() => ({
  90. [ScopeNames.FOR_ME_API]: {
  91. include: [
  92. {
  93. model: AccountModel,
  94. include: [
  95. {
  96. model: VideoChannelModel
  97. },
  98. {
  99. attributes: [ 'id', 'name', 'type' ],
  100. model: VideoPlaylistModel.unscoped(),
  101. required: true,
  102. where: {
  103. type: {
  104. [Op.ne]: VideoPlaylistType.REGULAR
  105. }
  106. }
  107. }
  108. ]
  109. },
  110. {
  111. model: UserNotificationSettingModel,
  112. required: true
  113. }
  114. ]
  115. },
  116. [ScopeNames.WITH_VIDEOCHANNELS]: {
  117. include: [
  118. {
  119. model: AccountModel,
  120. include: [
  121. {
  122. model: VideoChannelModel
  123. },
  124. {
  125. attributes: [ 'id', 'name', 'type' ],
  126. model: VideoPlaylistModel.unscoped(),
  127. required: true,
  128. where: {
  129. type: {
  130. [Op.ne]: VideoPlaylistType.REGULAR
  131. }
  132. }
  133. }
  134. ]
  135. }
  136. ]
  137. },
  138. [ScopeNames.WITH_STATS]: {
  139. attributes: {
  140. include: [
  141. [
  142. literal(
  143. '(' +
  144. UserModel.generateUserQuotaBaseSQL({
  145. withSelect: false,
  146. whereUserId: '"UserModel"."id"'
  147. }) +
  148. ')'
  149. ),
  150. 'videoQuotaUsed'
  151. ],
  152. [
  153. literal(
  154. '(' +
  155. 'SELECT COUNT("video"."id") ' +
  156. 'FROM "video" ' +
  157. 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
  158. 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
  159. 'WHERE "account"."userId" = "UserModel"."id"' +
  160. ')'
  161. ),
  162. 'videosCount'
  163. ],
  164. [
  165. literal(
  166. '(' +
  167. `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
  168. 'FROM (' +
  169. 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' +
  170. `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
  171. 'FROM "videoAbuse" ' +
  172. 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
  173. 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
  174. 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
  175. 'WHERE "account"."userId" = "UserModel"."id"' +
  176. ') t' +
  177. ')'
  178. ),
  179. 'videoAbusesCount'
  180. ],
  181. [
  182. literal(
  183. '(' +
  184. 'SELECT COUNT("videoAbuse"."id") ' +
  185. 'FROM "videoAbuse" ' +
  186. 'INNER JOIN "account" ON "account"."id" = "videoAbuse"."reporterAccountId" ' +
  187. 'WHERE "account"."userId" = "UserModel"."id"' +
  188. ')'
  189. ),
  190. 'videoAbusesCreatedCount'
  191. ],
  192. [
  193. literal(
  194. '(' +
  195. 'SELECT COUNT("videoComment"."id") ' +
  196. 'FROM "videoComment" ' +
  197. 'INNER JOIN "account" ON "account"."id" = "videoComment"."accountId" ' +
  198. 'WHERE "account"."userId" = "UserModel"."id"' +
  199. ')'
  200. ),
  201. 'videoCommentsCount'
  202. ]
  203. ]
  204. }
  205. }
  206. }))
  207. @Table({
  208. tableName: 'user',
  209. indexes: [
  210. {
  211. fields: [ 'username' ],
  212. unique: true
  213. },
  214. {
  215. fields: [ 'email' ],
  216. unique: true
  217. }
  218. ]
  219. })
  220. export class UserModel extends Model<UserModel> {
  221. @AllowNull(true)
  222. @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
  223. @Column
  224. password: string
  225. @AllowNull(false)
  226. @Is('UserUsername', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
  227. @Column
  228. username: string
  229. @AllowNull(false)
  230. @IsEmail
  231. @Column(DataType.STRING(400))
  232. email: string
  233. @AllowNull(true)
  234. @IsEmail
  235. @Column(DataType.STRING(400))
  236. pendingEmail: string
  237. @AllowNull(true)
  238. @Default(null)
  239. @Is('UserEmailVerified', value => throwIfNotValid(value, isUserEmailVerifiedValid, 'email verified boolean', true))
  240. @Column
  241. emailVerified: boolean
  242. @AllowNull(false)
  243. @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
  244. @Column(DataType.ENUM(...values(NSFW_POLICY_TYPES)))
  245. nsfwPolicy: NSFWPolicyType
  246. @AllowNull(false)
  247. @Default(true)
  248. @Is('UserWebTorrentEnabled', value => throwIfNotValid(value, isUserWebTorrentEnabledValid, 'WebTorrent enabled'))
  249. @Column
  250. webTorrentEnabled: boolean
  251. @AllowNull(false)
  252. @Default(true)
  253. @Is('UserVideosHistoryEnabled', value => throwIfNotValid(value, isUserVideosHistoryEnabledValid, 'Videos history enabled'))
  254. @Column
  255. videosHistoryEnabled: boolean
  256. @AllowNull(false)
  257. @Default(true)
  258. @Is('UserAutoPlayVideo', value => throwIfNotValid(value, isUserAutoPlayVideoValid, 'auto play video boolean'))
  259. @Column
  260. autoPlayVideo: boolean
  261. @AllowNull(false)
  262. @Default(false)
  263. @Is('UserAutoPlayNextVideo', value => throwIfNotValid(value, isUserAutoPlayNextVideoValid, 'auto play next video boolean'))
  264. @Column
  265. autoPlayNextVideo: boolean
  266. @AllowNull(false)
  267. @Default(true)
  268. @Is(
  269. 'UserAutoPlayNextVideoPlaylist',
  270. value => throwIfNotValid(value, isUserAutoPlayNextVideoPlaylistValid, 'auto play next video for playlists boolean')
  271. )
  272. @Column
  273. autoPlayNextVideoPlaylist: boolean
  274. @AllowNull(true)
  275. @Default(null)
  276. @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
  277. @Column(DataType.ARRAY(DataType.STRING))
  278. videoLanguages: string[]
  279. @AllowNull(false)
  280. @Default(UserAdminFlag.NONE)
  281. @Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
  282. @Column
  283. adminFlags?: UserAdminFlag
  284. @AllowNull(false)
  285. @Default(false)
  286. @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
  287. @Column
  288. blocked: boolean
  289. @AllowNull(true)
  290. @Default(null)
  291. @Is('UserBlockedReason', value => throwIfNotValid(value, isUserBlockedReasonValid, 'blocked reason', true))
  292. @Column
  293. blockedReason: string
  294. @AllowNull(false)
  295. @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
  296. @Column
  297. role: number
  298. @AllowNull(false)
  299. @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
  300. @Column(DataType.BIGINT)
  301. videoQuota: number
  302. @AllowNull(false)
  303. @Is('UserVideoQuotaDaily', value => throwIfNotValid(value, isUserVideoQuotaDailyValid, 'video quota daily'))
  304. @Column(DataType.BIGINT)
  305. videoQuotaDaily: number
  306. @AllowNull(false)
  307. @Default(DEFAULT_USER_THEME_NAME)
  308. @Is('UserTheme', value => throwIfNotValid(value, isThemeNameValid, 'theme'))
  309. @Column
  310. theme: string
  311. @AllowNull(false)
  312. @Default(false)
  313. @Is(
  314. 'UserNoInstanceConfigWarningModal',
  315. value => throwIfNotValid(value, isNoInstanceConfigWarningModal, 'no instance config warning modal')
  316. )
  317. @Column
  318. noInstanceConfigWarningModal: boolean
  319. @AllowNull(false)
  320. @Default(false)
  321. @Is(
  322. 'UserNoInstanceConfigWarningModal',
  323. value => throwIfNotValid(value, isNoWelcomeModal, 'no welcome modal')
  324. )
  325. @Column
  326. noWelcomeModal: boolean
  327. @AllowNull(true)
  328. @Default(null)
  329. @Column
  330. pluginAuth: string
  331. @CreatedAt
  332. createdAt: Date
  333. @UpdatedAt
  334. updatedAt: Date
  335. @HasOne(() => AccountModel, {
  336. foreignKey: 'userId',
  337. onDelete: 'cascade',
  338. hooks: true
  339. })
  340. Account: AccountModel
  341. @HasOne(() => UserNotificationSettingModel, {
  342. foreignKey: 'userId',
  343. onDelete: 'cascade',
  344. hooks: true
  345. })
  346. NotificationSetting: UserNotificationSettingModel
  347. @HasMany(() => VideoImportModel, {
  348. foreignKey: 'userId',
  349. onDelete: 'cascade'
  350. })
  351. VideoImports: VideoImportModel[]
  352. @HasMany(() => OAuthTokenModel, {
  353. foreignKey: 'userId',
  354. onDelete: 'cascade'
  355. })
  356. OAuthTokens: OAuthTokenModel[]
  357. @BeforeCreate
  358. @BeforeUpdate
  359. static cryptPasswordIfNeeded (instance: UserModel) {
  360. if (instance.changed('password')) {
  361. return cryptPassword(instance.password)
  362. .then(hash => {
  363. instance.password = hash
  364. return undefined
  365. })
  366. }
  367. }
  368. @AfterUpdate
  369. @AfterDestroy
  370. static removeTokenCache (instance: UserModel) {
  371. return clearCacheByUserId(instance.id)
  372. }
  373. static countTotal () {
  374. return this.count()
  375. }
  376. static listForApi (start: number, count: number, sort: string, search?: string) {
  377. let where: WhereOptions
  378. if (search) {
  379. where = {
  380. [Op.or]: [
  381. {
  382. email: {
  383. [Op.iLike]: '%' + search + '%'
  384. }
  385. },
  386. {
  387. username: {
  388. [Op.iLike]: '%' + search + '%'
  389. }
  390. }
  391. ]
  392. }
  393. }
  394. const query: FindOptions = {
  395. attributes: {
  396. include: [
  397. [
  398. literal(
  399. '(' +
  400. UserModel.generateUserQuotaBaseSQL({
  401. withSelect: false,
  402. whereUserId: '"UserModel"."id"'
  403. }) +
  404. ')'
  405. ),
  406. 'videoQuotaUsed'
  407. ] as any // FIXME: typings
  408. ]
  409. },
  410. offset: start,
  411. limit: count,
  412. order: getSort(sort),
  413. where
  414. }
  415. return UserModel.findAndCountAll(query)
  416. .then(({ rows, count }) => {
  417. return {
  418. data: rows,
  419. total: count
  420. }
  421. })
  422. }
  423. static listWithRight (right: UserRight): Bluebird<MUserDefault[]> {
  424. const roles = Object.keys(USER_ROLE_LABELS)
  425. .map(k => parseInt(k, 10) as UserRole)
  426. .filter(role => hasUserRight(role, right))
  427. const query = {
  428. where: {
  429. role: {
  430. [Op.in]: roles
  431. }
  432. }
  433. }
  434. return UserModel.findAll(query)
  435. }
  436. static listUserSubscribersOf (actorId: number): Bluebird<MUserWithNotificationSetting[]> {
  437. const query = {
  438. include: [
  439. {
  440. model: UserNotificationSettingModel.unscoped(),
  441. required: true
  442. },
  443. {
  444. attributes: [ 'userId' ],
  445. model: AccountModel.unscoped(),
  446. required: true,
  447. include: [
  448. {
  449. attributes: [],
  450. model: ActorModel.unscoped(),
  451. required: true,
  452. where: {
  453. serverId: null
  454. },
  455. include: [
  456. {
  457. attributes: [],
  458. as: 'ActorFollowings',
  459. model: ActorFollowModel.unscoped(),
  460. required: true,
  461. where: {
  462. targetActorId: actorId
  463. }
  464. }
  465. ]
  466. }
  467. ]
  468. }
  469. ]
  470. }
  471. return UserModel.unscoped().findAll(query)
  472. }
  473. static listByUsernames (usernames: string[]): Bluebird<MUserDefault[]> {
  474. const query = {
  475. where: {
  476. username: usernames
  477. }
  478. }
  479. return UserModel.findAll(query)
  480. }
  481. static loadById (id: number, withStats = false): Bluebird<MUserDefault> {
  482. const scopes = [
  483. ScopeNames.WITH_VIDEOCHANNELS
  484. ]
  485. if (withStats) scopes.push(ScopeNames.WITH_STATS)
  486. return UserModel.scope(scopes).findByPk(id)
  487. }
  488. static loadByUsername (username: string): Bluebird<MUserDefault> {
  489. const query = {
  490. where: {
  491. username: { [Op.iLike]: username }
  492. }
  493. }
  494. return UserModel.findOne(query)
  495. }
  496. static loadForMeAPI (username: string): Bluebird<MUserNotifSettingChannelDefault> {
  497. const query = {
  498. where: {
  499. username: { [Op.iLike]: username }
  500. }
  501. }
  502. return UserModel.scope(ScopeNames.FOR_ME_API).findOne(query)
  503. }
  504. static loadByEmail (email: string): Bluebird<MUserDefault> {
  505. const query = {
  506. where: {
  507. email
  508. }
  509. }
  510. return UserModel.findOne(query)
  511. }
  512. static loadByUsernameOrEmail (username: string, email?: string): Bluebird<MUserDefault> {
  513. if (!email) email = username
  514. const query = {
  515. where: {
  516. [Op.or]: [
  517. where(fn('lower', col('username')), fn('lower', username)),
  518. { email }
  519. ]
  520. }
  521. }
  522. return UserModel.findOne(query)
  523. }
  524. static loadByVideoId (videoId: number): Bluebird<MUserDefault> {
  525. const query = {
  526. include: [
  527. {
  528. required: true,
  529. attributes: [ 'id' ],
  530. model: AccountModel.unscoped(),
  531. include: [
  532. {
  533. required: true,
  534. attributes: [ 'id' ],
  535. model: VideoChannelModel.unscoped(),
  536. include: [
  537. {
  538. required: true,
  539. attributes: [ 'id' ],
  540. model: VideoModel.unscoped(),
  541. where: {
  542. id: videoId
  543. }
  544. }
  545. ]
  546. }
  547. ]
  548. }
  549. ]
  550. }
  551. return UserModel.findOne(query)
  552. }
  553. static loadByVideoImportId (videoImportId: number): Bluebird<MUserDefault> {
  554. const query = {
  555. include: [
  556. {
  557. required: true,
  558. attributes: [ 'id' ],
  559. model: VideoImportModel.unscoped(),
  560. where: {
  561. id: videoImportId
  562. }
  563. }
  564. ]
  565. }
  566. return UserModel.findOne(query)
  567. }
  568. static loadByChannelActorId (videoChannelActorId: number): Bluebird<MUserDefault> {
  569. const query = {
  570. include: [
  571. {
  572. required: true,
  573. attributes: [ 'id' ],
  574. model: AccountModel.unscoped(),
  575. include: [
  576. {
  577. required: true,
  578. attributes: [ 'id' ],
  579. model: VideoChannelModel.unscoped(),
  580. where: {
  581. actorId: videoChannelActorId
  582. }
  583. }
  584. ]
  585. }
  586. ]
  587. }
  588. return UserModel.findOne(query)
  589. }
  590. static loadByAccountActorId (accountActorId: number): Bluebird<MUserDefault> {
  591. const query = {
  592. include: [
  593. {
  594. required: true,
  595. attributes: [ 'id' ],
  596. model: AccountModel.unscoped(),
  597. where: {
  598. actorId: accountActorId
  599. }
  600. }
  601. ]
  602. }
  603. return UserModel.findOne(query)
  604. }
  605. static getOriginalVideoFileTotalFromUser (user: MUserId) {
  606. // Don't use sequelize because we need to use a sub query
  607. const query = UserModel.generateUserQuotaBaseSQL({
  608. withSelect: true,
  609. whereUserId: '$userId'
  610. })
  611. return UserModel.getTotalRawQuery(query, user.id)
  612. }
  613. // Returns cumulative size of all video files uploaded in the last 24 hours.
  614. static getOriginalVideoFileTotalDailyFromUser (user: MUserId) {
  615. // Don't use sequelize because we need to use a sub query
  616. const query = UserModel.generateUserQuotaBaseSQL({
  617. withSelect: true,
  618. whereUserId: '$userId',
  619. where: '"video"."createdAt" > now() - interval \'24 hours\''
  620. })
  621. return UserModel.getTotalRawQuery(query, user.id)
  622. }
  623. static async getStats () {
  624. const totalUsers = await UserModel.count()
  625. return {
  626. totalUsers
  627. }
  628. }
  629. static autoComplete (search: string) {
  630. const query = {
  631. where: {
  632. username: {
  633. [Op.like]: `%${search}%`
  634. }
  635. },
  636. limit: 10
  637. }
  638. return UserModel.findAll(query)
  639. .then(u => u.map(u => u.username))
  640. }
  641. canGetVideo (video: MVideoFullLight) {
  642. const videoUserId = video.VideoChannel.Account.userId
  643. if (video.isBlacklisted()) {
  644. return videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
  645. }
  646. if (video.privacy === VideoPrivacy.PRIVATE) {
  647. return video.VideoChannel && videoUserId === this.id || this.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
  648. }
  649. if (video.privacy === VideoPrivacy.INTERNAL) return true
  650. return false
  651. }
  652. hasRight (right: UserRight) {
  653. return hasUserRight(this.role, right)
  654. }
  655. hasAdminFlag (flag: UserAdminFlag) {
  656. return this.adminFlags & flag
  657. }
  658. isPasswordMatch (password: string) {
  659. return comparePassword(password, this.password)
  660. }
  661. toFormattedJSON (this: MUserFormattable, parameters: { withAdminFlags?: boolean } = {}): User {
  662. const videoQuotaUsed = this.get('videoQuotaUsed')
  663. const videoQuotaUsedDaily = this.get('videoQuotaUsedDaily')
  664. const videosCount = this.get('videosCount')
  665. const [ videoAbusesCount, videoAbusesAcceptedCount ] = (this.get('videoAbusesCount') as string || ':').split(':')
  666. const videoAbusesCreatedCount = this.get('videoAbusesCreatedCount')
  667. const videoCommentsCount = this.get('videoCommentsCount')
  668. const json: User = {
  669. id: this.id,
  670. username: this.username,
  671. email: this.email,
  672. theme: getThemeOrDefault(this.theme, DEFAULT_USER_THEME_NAME),
  673. pendingEmail: this.pendingEmail,
  674. emailVerified: this.emailVerified,
  675. nsfwPolicy: this.nsfwPolicy,
  676. webTorrentEnabled: this.webTorrentEnabled,
  677. videosHistoryEnabled: this.videosHistoryEnabled,
  678. autoPlayVideo: this.autoPlayVideo,
  679. autoPlayNextVideo: this.autoPlayNextVideo,
  680. autoPlayNextVideoPlaylist: this.autoPlayNextVideoPlaylist,
  681. videoLanguages: this.videoLanguages,
  682. role: this.role,
  683. roleLabel: USER_ROLE_LABELS[this.role],
  684. videoQuota: this.videoQuota,
  685. videoQuotaDaily: this.videoQuotaDaily,
  686. videoQuotaUsed: videoQuotaUsed !== undefined
  687. ? parseInt(videoQuotaUsed + '', 10)
  688. : undefined,
  689. videoQuotaUsedDaily: videoQuotaUsedDaily !== undefined
  690. ? parseInt(videoQuotaUsedDaily + '', 10)
  691. : undefined,
  692. videosCount: videosCount !== undefined
  693. ? parseInt(videosCount + '', 10)
  694. : undefined,
  695. videoAbusesCount: videoAbusesCount
  696. ? parseInt(videoAbusesCount, 10)
  697. : undefined,
  698. videoAbusesAcceptedCount: videoAbusesAcceptedCount
  699. ? parseInt(videoAbusesAcceptedCount, 10)
  700. : undefined,
  701. videoAbusesCreatedCount: videoAbusesCreatedCount !== undefined
  702. ? parseInt(videoAbusesCreatedCount + '', 10)
  703. : undefined,
  704. videoCommentsCount: videoCommentsCount !== undefined
  705. ? parseInt(videoCommentsCount + '', 10)
  706. : undefined,
  707. noInstanceConfigWarningModal: this.noInstanceConfigWarningModal,
  708. noWelcomeModal: this.noWelcomeModal,
  709. blocked: this.blocked,
  710. blockedReason: this.blockedReason,
  711. account: this.Account.toFormattedJSON(),
  712. notificationSettings: this.NotificationSetting
  713. ? this.NotificationSetting.toFormattedJSON()
  714. : undefined,
  715. videoChannels: [],
  716. createdAt: this.createdAt
  717. }
  718. if (parameters.withAdminFlags) {
  719. Object.assign(json, { adminFlags: this.adminFlags })
  720. }
  721. if (Array.isArray(this.Account.VideoChannels) === true) {
  722. json.videoChannels = this.Account.VideoChannels
  723. .map(c => c.toFormattedJSON())
  724. .sort((v1, v2) => {
  725. if (v1.createdAt < v2.createdAt) return -1
  726. if (v1.createdAt === v2.createdAt) return 0
  727. return 1
  728. })
  729. }
  730. return json
  731. }
  732. toMeFormattedJSON (this: MMyUserFormattable): MyUser {
  733. const formatted = this.toFormattedJSON()
  734. const specialPlaylists = this.Account.VideoPlaylists
  735. .map(p => ({ id: p.id, name: p.name, type: p.type }))
  736. return Object.assign(formatted, { specialPlaylists })
  737. }
  738. async isAbleToUploadVideo (videoFile: { size: number }) {
  739. if (this.videoQuota === -1 && this.videoQuotaDaily === -1) return Promise.resolve(true)
  740. const [ totalBytes, totalBytesDaily ] = await Promise.all([
  741. UserModel.getOriginalVideoFileTotalFromUser(this),
  742. UserModel.getOriginalVideoFileTotalDailyFromUser(this)
  743. ])
  744. const uploadedTotal = videoFile.size + totalBytes
  745. const uploadedDaily = videoFile.size + totalBytesDaily
  746. if (this.videoQuotaDaily === -1) return uploadedTotal < this.videoQuota
  747. if (this.videoQuota === -1) return uploadedDaily < this.videoQuotaDaily
  748. return uploadedTotal < this.videoQuota && uploadedDaily < this.videoQuotaDaily
  749. }
  750. private static generateUserQuotaBaseSQL (options: {
  751. whereUserId: '$userId' | '"UserModel"."id"'
  752. withSelect: boolean
  753. where?: string
  754. }) {
  755. const andWhere = options.where
  756. ? 'AND ' + options.where
  757. : ''
  758. const videoChannelJoin = 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
  759. 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
  760. `WHERE "account"."userId" = ${options.whereUserId} ${andWhere}`
  761. const webtorrentFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
  762. 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
  763. videoChannelJoin
  764. const hlsFiles = 'SELECT "videoFile"."size" AS "size", "video"."id" AS "videoId" FROM "videoFile" ' +
  765. 'INNER JOIN "videoStreamingPlaylist" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ' +
  766. 'INNER JOIN "video" ON "videoStreamingPlaylist"."videoId" = "video"."id" ' +
  767. videoChannelJoin
  768. return 'SELECT COALESCE(SUM("size"), 0) AS "total" ' +
  769. 'FROM (' +
  770. `SELECT MAX("t1"."size") AS "size" FROM (${webtorrentFiles} UNION ${hlsFiles}) t1 ` +
  771. 'GROUP BY "t1"."videoId"' +
  772. ') t2'
  773. }
  774. private static getTotalRawQuery (query: string, userId: number) {
  775. const options = {
  776. bind: { userId },
  777. type: QueryTypes.SELECT as QueryTypes.SELECT
  778. }
  779. return UserModel.sequelize.query<{ total: string }>(query, options)
  780. .then(([ { total } ]) => {
  781. if (total === null) return 0
  782. return parseInt(total, 10)
  783. })
  784. }
  785. }