aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/account
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/account')
-rw-r--r--server/models/account/account-follow-interface.ts60
-rw-r--r--server/models/account/account-follow.ts364
-rw-r--r--server/models/account/account-interface.ts76
-rw-r--r--server/models/account/account-video-rate-interface.ts31
-rw-r--r--server/models/account/account-video-rate.ts97
-rw-r--r--server/models/account/account.ts673
-rw-r--r--server/models/account/index.ts4
-rw-r--r--server/models/account/user-interface.ts67
-rw-r--r--server/models/account/user.ts460
9 files changed, 728 insertions, 1104 deletions
diff --git a/server/models/account/account-follow-interface.ts b/server/models/account/account-follow-interface.ts
deleted file mode 100644
index 7975a46f3..000000000
--- a/server/models/account/account-follow-interface.ts
+++ /dev/null
@@ -1,60 +0,0 @@
1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { AccountFollow, FollowState } from '../../../shared/models/accounts/follow.model'
4import { ResultList } from '../../../shared/models/result-list.model'
5import { AccountInstance } from './account-interface'
6
7export namespace AccountFollowMethods {
8 export type LoadByAccountAndTarget = (
9 accountId: number,
10 targetAccountId: number,
11 t?: Sequelize.Transaction
12 ) => Bluebird<AccountFollowInstance>
13
14 export type ListFollowingForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>>
15 export type ListFollowersForApi = (id: number, start: number, count: number, sort: string) => Bluebird< ResultList<AccountFollowInstance>>
16
17 export type ListAcceptedFollowerUrlsForApi = (
18 accountId: number[],
19 t: Sequelize.Transaction,
20 start?: number,
21 count?: number
22 ) => Promise< ResultList<string> >
23 export type ListAcceptedFollowingUrlsForApi = (
24 accountId: number[],
25 t: Sequelize.Transaction,
26 start?: number,
27 count?: number
28 ) => Promise< ResultList<string> >
29 export type ListAcceptedFollowerSharedInboxUrls = (accountId: number[], t: Sequelize.Transaction) => Promise< ResultList<string> >
30 export type ToFormattedJSON = (this: AccountFollowInstance) => AccountFollow
31}
32
33export interface AccountFollowClass {
34 loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget
35 listFollowersForApi: AccountFollowMethods.ListFollowersForApi
36 listFollowingForApi: AccountFollowMethods.ListFollowingForApi
37
38 listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi
39 listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi
40 listAcceptedFollowerSharedInboxUrls: AccountFollowMethods.ListAcceptedFollowerSharedInboxUrls
41}
42
43export interface AccountFollowAttributes {
44 accountId: number
45 targetAccountId: number
46 state: FollowState
47}
48
49export interface AccountFollowInstance extends AccountFollowClass, AccountFollowAttributes, Sequelize.Instance<AccountFollowAttributes> {
50 id: number
51 createdAt: Date
52 updatedAt: Date
53
54 AccountFollower?: AccountInstance
55 AccountFollowing?: AccountInstance
56
57 toFormattedJSON: AccountFollowMethods.ToFormattedJSON
58}
59
60export interface AccountFollowModel extends AccountFollowClass, Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> {}
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts
index 724f37baa..975e7ee7d 100644
--- a/server/models/account/account-follow.ts
+++ b/server/models/account/account-follow.ts
@@ -1,64 +1,45 @@
1import * as Bluebird from 'bluebird'
1import { values } from 'lodash' 2import { values } from 'lodash'
2import * as Sequelize from 'sequelize' 3import * as Sequelize from 'sequelize'
3 4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { addMethodsToModel, getSort } from '../utils' 5import { FollowState } from '../../../shared/models/accounts'
5import { AccountFollowAttributes, AccountFollowInstance, AccountFollowMethods } from './account-follow-interface'
6import { FOLLOW_STATES } from '../../initializers/constants' 6import { FOLLOW_STATES } from '../../initializers/constants'
7import { ServerModel } from '../server/server'
8import { getSort } from '../utils'
9import { AccountModel } from './account'
7 10
8let AccountFollow: Sequelize.Model<AccountFollowInstance, AccountFollowAttributes> 11@Table({
9let loadByAccountAndTarget: AccountFollowMethods.LoadByAccountAndTarget 12 tableName: 'accountFollow',
10let listFollowingForApi: AccountFollowMethods.ListFollowingForApi 13 indexes: [
11let listFollowersForApi: AccountFollowMethods.ListFollowersForApi
12let listAcceptedFollowerUrlsForApi: AccountFollowMethods.ListAcceptedFollowerUrlsForApi
13let listAcceptedFollowingUrlsForApi: AccountFollowMethods.ListAcceptedFollowingUrlsForApi
14let listAcceptedFollowerSharedInboxUrls: AccountFollowMethods.ListAcceptedFollowerSharedInboxUrls
15let toFormattedJSON: AccountFollowMethods.ToFormattedJSON
16
17export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
18 AccountFollow = sequelize.define<AccountFollowInstance, AccountFollowAttributes>('AccountFollow',
19 { 14 {
20 state: { 15 fields: [ 'accountId' ]
21 type: DataTypes.ENUM(values(FOLLOW_STATES)),
22 allowNull: false
23 }
24 }, 16 },
25 { 17 {
26 indexes: [ 18 fields: [ 'targetAccountId' ]
27 { 19 },
28 fields: [ 'accountId' ] 20 {
29 }, 21 fields: [ 'accountId', 'targetAccountId' ],
30 { 22 unique: true
31 fields: [ 'targetAccountId' ]
32 },
33 {
34 fields: [ 'accountId', 'targetAccountId' ],
35 unique: true
36 }
37 ]
38 } 23 }
39 )
40
41 const classMethods = [
42 associate,
43 loadByAccountAndTarget,
44 listFollowingForApi,
45 listFollowersForApi,
46 listAcceptedFollowerUrlsForApi,
47 listAcceptedFollowingUrlsForApi,
48 listAcceptedFollowerSharedInboxUrls
49 ] 24 ]
50 const instanceMethods = [ 25})
51 toFormattedJSON 26export class AccountFollowModel extends Model<AccountFollowModel> {
52 ]
53 addMethodsToModel(AccountFollow, classMethods, instanceMethods)
54 27
55 return AccountFollow 28 @AllowNull(false)
56} 29 @Column(DataType.ENUM(values(FOLLOW_STATES)))
30 state: FollowState
57 31
58// ------------------------------ STATICS ------------------------------ 32 @CreatedAt
33 createdAt: Date
59 34
60function associate (models) { 35 @UpdatedAt
61 AccountFollow.belongsTo(models.Account, { 36 updatedAt: Date
37
38 @ForeignKey(() => AccountModel)
39 @Column
40 accountId: number
41
42 @BelongsTo(() => AccountModel, {
62 foreignKey: { 43 foreignKey: {
63 name: 'accountId', 44 name: 'accountId',
64 allowNull: false 45 allowNull: false
@@ -66,8 +47,13 @@ function associate (models) {
66 as: 'AccountFollower', 47 as: 'AccountFollower',
67 onDelete: 'CASCADE' 48 onDelete: 'CASCADE'
68 }) 49 })
50 AccountFollower: AccountModel
69 51
70 AccountFollow.belongsTo(models.Account, { 52 @ForeignKey(() => AccountModel)
53 @Column
54 targetAccountId: number
55
56 @BelongsTo(() => AccountModel, {
71 foreignKey: { 57 foreignKey: {
72 name: 'targetAccountId', 58 name: 'targetAccountId',
73 allowNull: false 59 allowNull: false
@@ -75,170 +61,168 @@ function associate (models) {
75 as: 'AccountFollowing', 61 as: 'AccountFollowing',
76 onDelete: 'CASCADE' 62 onDelete: 'CASCADE'
77 }) 63 })
78} 64 AccountFollowing: AccountModel
79 65
80toFormattedJSON = function (this: AccountFollowInstance) { 66 static loadByAccountAndTarget (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) {
81 const follower = this.AccountFollower.toFormattedJSON() 67 const query = {
82 const following = this.AccountFollowing.toFormattedJSON() 68 where: {
83 69 accountId,
84 const json = { 70 targetAccountId
85 id: this.id,
86 follower,
87 following,
88 state: this.state,
89 createdAt: this.createdAt,
90 updatedAt: this.updatedAt
91 }
92
93 return json
94}
95
96loadByAccountAndTarget = function (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) {
97 const query = {
98 where: {
99 accountId,
100 targetAccountId
101 },
102 include: [
103 {
104 model: AccountFollow[ 'sequelize' ].models.Account,
105 required: true,
106 as: 'AccountFollower'
107 }, 71 },
108 { 72 include: [
109 model: AccountFollow['sequelize'].models.Account, 73 {
110 required: true, 74 model: AccountModel,
111 as: 'AccountFollowing' 75 required: true,
112 } 76 as: 'AccountFollower'
113 ], 77 },
114 transaction: t 78 {
79 model: AccountModel,
80 required: true,
81 as: 'AccountFollowing'
82 }
83 ],
84 transaction: t
85 }
86
87 return AccountFollowModel.findOne(query)
115 } 88 }
116 89
117 return AccountFollow.findOne(query) 90 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
118} 91 const query = {
92 distinct: true,
93 offset: start,
94 limit: count,
95 order: [ getSort(sort) ],
96 include: [
97 {
98 model: AccountModel,
99 required: true,
100 as: 'AccountFollower',
101 where: {
102 id
103 }
104 },
105 {
106 model: AccountModel,
107 as: 'AccountFollowing',
108 required: true,
109 include: [ ServerModel ]
110 }
111 ]
112 }
119 113
120listFollowingForApi = function (id: number, start: number, count: number, sort: string) { 114 return AccountFollowModel.findAndCountAll(query)
121 const query = { 115 .then(({ rows, count }) => {
122 distinct: true, 116 return {
123 offset: start, 117 data: rows,
124 limit: count, 118 total: count
125 order: [ getSort(sort) ],
126 include: [
127 {
128 model: AccountFollow[ 'sequelize' ].models.Account,
129 required: true,
130 as: 'AccountFollower',
131 where: {
132 id
133 } 119 }
134 }, 120 })
135 {
136 model: AccountFollow['sequelize'].models.Account,
137 as: 'AccountFollowing',
138 required: true,
139 include: [ AccountFollow['sequelize'].models.Server ]
140 }
141 ]
142 } 121 }
143 122
144 return AccountFollow.findAndCountAll(query).then(({ rows, count }) => { 123 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
145 return { 124 const query = {
146 data: rows, 125 distinct: true,
147 total: count 126 offset: start,
127 limit: count,
128 order: [ getSort(sort) ],
129 include: [
130 {
131 model: AccountModel,
132 required: true,
133 as: 'AccountFollower',
134 include: [ ServerModel ]
135 },
136 {
137 model: AccountModel,
138 as: 'AccountFollowing',
139 required: true,
140 where: {
141 id
142 }
143 }
144 ]
148 } 145 }
149 })
150}
151 146
152listFollowersForApi = function (id: number, start: number, count: number, sort: string) { 147 return AccountFollowModel.findAndCountAll(query)
153 const query = { 148 .then(({ rows, count }) => {
154 distinct: true, 149 return {
155 offset: start, 150 data: rows,
156 limit: count, 151 total: count
157 order: [ getSort(sort) ],
158 include: [
159 {
160 model: AccountFollow[ 'sequelize' ].models.Account,
161 required: true,
162 as: 'AccountFollower',
163 include: [ AccountFollow['sequelize'].models.Server ]
164 },
165 {
166 model: AccountFollow['sequelize'].models.Account,
167 as: 'AccountFollowing',
168 required: true,
169 where: {
170 id
171 } 152 }
172 } 153 })
173 ]
174 } 154 }
175 155
176 return AccountFollow.findAndCountAll(query).then(({ rows, count }) => { 156 static listAcceptedFollowerUrlsForApi (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
177 return { 157 return AccountFollowModel.createListAcceptedFollowForApiQuery('followers', accountIds, t, start, count)
178 data: rows, 158 }
179 total: count
180 }
181 })
182}
183 159
184listAcceptedFollowerUrlsForApi = function (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { 160 static listAcceptedFollowerSharedInboxUrls (accountIds: number[], t: Sequelize.Transaction) {
185 return createListAcceptedFollowForApiQuery('followers', accountIds, t, start, count) 161 return AccountFollowModel.createListAcceptedFollowForApiQuery('followers', accountIds, t, undefined, undefined, 'sharedInboxUrl')
186} 162 }
187 163
188listAcceptedFollowerSharedInboxUrls = function (accountIds: number[], t: Sequelize.Transaction) { 164 static listAcceptedFollowingUrlsForApi (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
189 return createListAcceptedFollowForApiQuery('followers', accountIds, t, undefined, undefined, 'sharedInboxUrl') 165 return AccountFollowModel.createListAcceptedFollowForApiQuery('following', accountIds, t, start, count)
190} 166 }
191 167
192listAcceptedFollowingUrlsForApi = function (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) { 168 private static async createListAcceptedFollowForApiQuery (type: 'followers' | 'following',
193 return createListAcceptedFollowForApiQuery('following', accountIds, t, start, count) 169 accountIds: number[],
194} 170 t: Sequelize.Transaction,
171 start?: number,
172 count?: number,
173 columnUrl = 'url') {
174 let firstJoin: string
175 let secondJoin: string
176
177 if (type === 'followers') {
178 firstJoin = 'targetAccountId'
179 secondJoin = 'accountId'
180 } else {
181 firstJoin = 'accountId'
182 secondJoin = 'targetAccountId'
183 }
195 184
196// ------------------------------ UTILS ------------------------------ 185 const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ]
197 186 const tasks: Bluebird<any>[] = []
198async function createListAcceptedFollowForApiQuery (
199 type: 'followers' | 'following',
200 accountIds: number[],
201 t: Sequelize.Transaction,
202 start?: number,
203 count?: number,
204 columnUrl = 'url'
205) {
206 let firstJoin: string
207 let secondJoin: string
208
209 if (type === 'followers') {
210 firstJoin = 'targetAccountId'
211 secondJoin = 'accountId'
212 } else {
213 firstJoin = 'accountId'
214 secondJoin = 'targetAccountId'
215 }
216 187
217 const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ] 188 for (const selection of selections) {
218 const tasks: Promise<any>[] = [] 189 let query = 'SELECT ' + selection + ' FROM "account" ' +
190 'INNER JOIN "accountFollow" ON "accountFollow"."' + firstJoin + '" = "account"."id" ' +
191 'INNER JOIN "account" AS "Follows" ON "accountFollow"."' + secondJoin + '" = "Follows"."id" ' +
192 'WHERE "account"."id" = ANY ($accountIds) AND "accountFollow"."state" = \'accepted\' '
219 193
220 for (const selection of selections) { 194 if (count !== undefined) query += 'LIMIT ' + count
221 let query = 'SELECT ' + selection + ' FROM "Accounts" ' + 195 if (start !== undefined) query += ' OFFSET ' + start
222 'INNER JOIN "AccountFollows" ON "AccountFollows"."' + firstJoin + '" = "Accounts"."id" ' +
223 'INNER JOIN "Accounts" AS "Follows" ON "AccountFollows"."' + secondJoin + '" = "Follows"."id" ' +
224 'WHERE "Accounts"."id" = ANY ($accountIds) AND "AccountFollows"."state" = \'accepted\' '
225 196
226 if (count !== undefined) query += 'LIMIT ' + count 197 const options = {
227 if (start !== undefined) query += ' OFFSET ' + start 198 bind: { accountIds },
199 type: Sequelize.QueryTypes.SELECT,
200 transaction: t
201 }
202 tasks.push(AccountFollowModel.sequelize.query(query, options))
203 }
228 204
229 const options = { 205 const [ followers, [ { total } ] ] = await
230 bind: { accountIds }, 206 Promise.all(tasks)
231 type: Sequelize.QueryTypes.SELECT, 207 const urls: string[] = followers.map(f => f.url)
232 transaction: t 208
209 return {
210 data: urls,
211 total: parseInt(total, 10)
233 } 212 }
234 tasks.push(AccountFollow['sequelize'].query(query, options))
235 } 213 }
236 214
237 const [ followers, [ { total } ]] = await Promise.all(tasks) 215 toFormattedJSON () {
238 const urls: string[] = followers.map(f => f.url) 216 const follower = this.AccountFollower.toFormattedJSON()
217 const following = this.AccountFollowing.toFormattedJSON()
239 218
240 return { 219 return {
241 data: urls, 220 id: this.id,
242 total: parseInt(total, 10) 221 follower,
222 following,
223 state: this.state,
224 createdAt: this.createdAt,
225 updatedAt: this.updatedAt
226 }
243 } 227 }
244} 228}
diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts
deleted file mode 100644
index 46fe068e3..000000000
--- a/server/models/account/account-interface.ts
+++ /dev/null
@@ -1,76 +0,0 @@
1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { Account as FormattedAccount, ActivityPubActor } from '../../../shared'
4import { AvatarInstance } from '../avatar'
5import { ServerInstance } from '../server/server-interface'
6import { VideoChannelInstance } from '../video/video-channel-interface'
7
8export namespace AccountMethods {
9 export type LoadApplication = () => Bluebird<AccountInstance>
10
11 export type Load = (id: number) => Bluebird<AccountInstance>
12 export type LoadByUUID = (uuid: string) => Bluebird<AccountInstance>
13 export type LoadByUrl = (url: string, transaction?: Sequelize.Transaction) => Bluebird<AccountInstance>
14 export type LoadLocalByName = (name: string) => Bluebird<AccountInstance>
15 export type LoadByNameAndHost = (name: string, host: string) => Bluebird<AccountInstance>
16 export type ListByFollowersUrls = (followerUrls: string[], transaction: Sequelize.Transaction) => Bluebird<AccountInstance[]>
17
18 export type ToActivityPubObject = (this: AccountInstance) => ActivityPubActor
19 export type ToFormattedJSON = (this: AccountInstance) => FormattedAccount
20 export type IsOwned = (this: AccountInstance) => boolean
21 export type GetFollowerSharedInboxUrls = (this: AccountInstance, t: Sequelize.Transaction) => Bluebird<string[]>
22 export type GetFollowingUrl = (this: AccountInstance) => string
23 export type GetFollowersUrl = (this: AccountInstance) => string
24 export type GetPublicKeyUrl = (this: AccountInstance) => string
25}
26
27export interface AccountClass {
28 loadApplication: AccountMethods.LoadApplication
29 load: AccountMethods.Load
30 loadByUUID: AccountMethods.LoadByUUID
31 loadByUrl: AccountMethods.LoadByUrl
32 loadLocalByName: AccountMethods.LoadLocalByName
33 loadByNameAndHost: AccountMethods.LoadByNameAndHost
34 listByFollowersUrls: AccountMethods.ListByFollowersUrls
35}
36
37export interface AccountAttributes {
38 name: string
39 url?: string
40 publicKey: string
41 privateKey: string
42 followersCount: number
43 followingCount: number
44 inboxUrl: string
45 outboxUrl: string
46 sharedInboxUrl: string
47 followersUrl: string
48 followingUrl: string
49
50 uuid?: string
51
52 serverId?: number
53 userId?: number
54 applicationId?: number
55 avatarId?: number
56}
57
58export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
59 isOwned: AccountMethods.IsOwned
60 toActivityPubObject: AccountMethods.ToActivityPubObject
61 toFormattedJSON: AccountMethods.ToFormattedJSON
62 getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
63 getFollowingUrl: AccountMethods.GetFollowingUrl
64 getFollowersUrl: AccountMethods.GetFollowersUrl
65 getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
66
67 id: number
68 createdAt: Date
69 updatedAt: Date
70
71 Server: ServerInstance
72 VideoChannels: VideoChannelInstance[]
73 Avatar: AvatarInstance
74}
75
76export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {}
diff --git a/server/models/account/account-video-rate-interface.ts b/server/models/account/account-video-rate-interface.ts
deleted file mode 100644
index 1f395bc45..000000000
--- a/server/models/account/account-video-rate-interface.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import * as Sequelize from 'sequelize'
2import * as Promise from 'bluebird'
3
4import { VideoRateType } from '../../../shared/models/videos/video-rate.type'
5import { AccountInstance } from './account-interface'
6
7export namespace AccountVideoRateMethods {
8 export type Load = (accountId: number, videoId: number, transaction: Sequelize.Transaction) => Promise<AccountVideoRateInstance>
9}
10
11export interface AccountVideoRateClass {
12 load: AccountVideoRateMethods.Load
13}
14
15export interface AccountVideoRateAttributes {
16 type: VideoRateType
17 accountId: number
18 videoId: number
19
20 Account?: AccountInstance
21}
22
23export interface AccountVideoRateInstance
24 extends AccountVideoRateClass, AccountVideoRateAttributes, Sequelize.Instance<AccountVideoRateAttributes> {
25 id: number
26 createdAt: Date
27 updatedAt: Date
28}
29
30export interface AccountVideoRateModel
31 extends AccountVideoRateClass, Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes> {}
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts
index d92834bbb..e969e4a43 100644
--- a/server/models/account/account-video-rate.ts
+++ b/server/models/account/account-video-rate.ts
@@ -1,78 +1,69 @@
1/*
2 Account rates per video.
3*/
4import { values } from 'lodash' 1import { values } from 'lodash'
5import * as Sequelize from 'sequelize' 2import { Transaction } from 'sequelize'
6 3import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
4import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions'
5import { VideoRateType } from '../../../shared/models/videos'
7import { VIDEO_RATE_TYPES } from '../../initializers' 6import { VIDEO_RATE_TYPES } from '../../initializers'
7import { VideoModel } from '../video/video'
8import { AccountModel } from './account'
8 9
9import { addMethodsToModel } from '../utils' 10/*
10import { 11 Account rates per video.
11 AccountVideoRateInstance, 12*/
12 AccountVideoRateAttributes, 13@Table({
13 14 tableName: 'accountVideoRate',
14 AccountVideoRateMethods 15 indexes: [
15} from './account-video-rate-interface'
16
17let AccountVideoRate: Sequelize.Model<AccountVideoRateInstance, AccountVideoRateAttributes>
18let load: AccountVideoRateMethods.Load
19
20export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
21 AccountVideoRate = sequelize.define<AccountVideoRateInstance, AccountVideoRateAttributes>('AccountVideoRate',
22 {
23 type: {
24 type: DataTypes.ENUM(values(VIDEO_RATE_TYPES)),
25 allowNull: false
26 }
27 },
28 { 16 {
29 indexes: [ 17 fields: [ 'videoId', 'accountId' ],
30 { 18 unique: true
31 fields: [ 'videoId', 'accountId' ],
32 unique: true
33 }
34 ]
35 } 19 }
36 ) 20 ]
21})
22export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
37 23
38 const classMethods = [ 24 @AllowNull(false)
39 associate, 25 @Column(DataType.ENUM(values(VIDEO_RATE_TYPES)))
26 type: VideoRateType
40 27
41 load 28 @CreatedAt
42 ] 29 createdAt: Date
43 addMethodsToModel(AccountVideoRate, classMethods)
44 30
45 return AccountVideoRate 31 @UpdatedAt
46} 32 updatedAt: Date
47 33
48// ------------------------------ STATICS ------------------------------ 34 @ForeignKey(() => VideoModel)
35 @Column
36 videoId: number
49 37
50function associate (models) { 38 @BelongsTo(() => VideoModel, {
51 AccountVideoRate.belongsTo(models.Video, {
52 foreignKey: { 39 foreignKey: {
53 name: 'videoId',
54 allowNull: false 40 allowNull: false
55 }, 41 },
56 onDelete: 'CASCADE' 42 onDelete: 'CASCADE'
57 }) 43 })
44 Video: VideoModel
58 45
59 AccountVideoRate.belongsTo(models.Account, { 46 @ForeignKey(() => AccountModel)
47 @Column
48 accountId: number
49
50 @BelongsTo(() => AccountModel, {
60 foreignKey: { 51 foreignKey: {
61 name: 'accountId',
62 allowNull: false 52 allowNull: false
63 }, 53 },
64 onDelete: 'CASCADE' 54 onDelete: 'CASCADE'
65 }) 55 })
66} 56 Account: AccountModel
67 57
68load = function (accountId: number, videoId: number, transaction: Sequelize.Transaction) { 58 static load (accountId: number, videoId: number, transaction: Transaction) {
69 const options: Sequelize.FindOptions<AccountVideoRateAttributes> = { 59 const options: IFindOptions<AccountVideoRateModel> = {
70 where: { 60 where: {
71 accountId, 61 accountId,
72 videoId 62 videoId
63 }
73 } 64 }
74 } 65 if (transaction) options.transaction = transaction
75 if (transaction) options.transaction = transaction
76 66
77 return AccountVideoRate.findOne(options) 67 return AccountVideoRateModel.findOne(options)
68 }
78} 69}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 8b0819f39..d6758fa10 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -1,253 +1,200 @@
1import { join } from 'path' 1import { join } from 'path'
2import * as Sequelize from 'sequelize' 2import * as Sequelize from 'sequelize'
3import {
4 AfterDestroy,
5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 Default,
11 ForeignKey,
12 HasMany,
13 Is,
14 IsUUID,
15 Model,
16 Table,
17 UpdatedAt
18} from 'sequelize-typescript'
3import { Avatar } from '../../../shared/models/avatars/avatar.model' 19import { Avatar } from '../../../shared/models/avatars/avatar.model'
20import { activityPubContextify } from '../../helpers'
4import { 21import {
5 activityPubContextify,
6 isAccountFollowersCountValid, 22 isAccountFollowersCountValid,
7 isAccountFollowingCountValid, 23 isAccountFollowingCountValid,
8 isAccountPrivateKeyValid, 24 isAccountPrivateKeyValid,
9 isAccountPublicKeyValid, 25 isAccountPublicKeyValid,
10 isUserUsernameValid 26 isActivityPubUrlValid
11} from '../../helpers' 27} from '../../helpers/custom-validators/activitypub'
12import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 28import { isUserUsernameValid } from '../../helpers/custom-validators/users'
13import { AVATARS_DIR } from '../../initializers' 29import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
14import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' 30import { sendDeleteAccount } from '../../lib/activitypub/send'
15import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete' 31import { ApplicationModel } from '../application/application'
16import { addMethodsToModel } from '../utils' 32import { AvatarModel } from '../avatar/avatar'
17import { AccountAttributes, AccountInstance, AccountMethods } from './account-interface' 33import { ServerModel } from '../server/server'
18 34import { throwIfNotValid } from '../utils'
19let Account: Sequelize.Model<AccountInstance, AccountAttributes> 35import { VideoChannelModel } from '../video/video-channel'
20let load: AccountMethods.Load 36import { AccountFollowModel } from './account-follow'
21let loadApplication: AccountMethods.LoadApplication 37import { UserModel } from './user'
22let loadByUUID: AccountMethods.LoadByUUID 38
23let loadByUrl: AccountMethods.LoadByUrl 39@Table({
24let loadLocalByName: AccountMethods.LoadLocalByName 40 tableName: 'account',
25let loadByNameAndHost: AccountMethods.LoadByNameAndHost 41 indexes: [
26let listByFollowersUrls: AccountMethods.ListByFollowersUrls
27let isOwned: AccountMethods.IsOwned
28let toActivityPubObject: AccountMethods.ToActivityPubObject
29let toFormattedJSON: AccountMethods.ToFormattedJSON
30let getFollowerSharedInboxUrls: AccountMethods.GetFollowerSharedInboxUrls
31let getFollowingUrl: AccountMethods.GetFollowingUrl
32let getFollowersUrl: AccountMethods.GetFollowersUrl
33let getPublicKeyUrl: AccountMethods.GetPublicKeyUrl
34
35export default function defineAccount (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
36 Account = sequelize.define<AccountInstance, AccountAttributes>('Account',
37 { 42 {
38 uuid: { 43 fields: [ 'name' ]
39 type: DataTypes.UUID,
40 defaultValue: DataTypes.UUIDV4,
41 allowNull: false,
42 validate: {
43 isUUID: 4
44 }
45 },
46 name: {
47 type: DataTypes.STRING,
48 allowNull: false,
49 validate: {
50 nameValid: value => {
51 const res = isUserUsernameValid(value)
52 if (res === false) throw new Error('Name is not valid.')
53 }
54 }
55 },
56 url: {
57 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
58 allowNull: false,
59 validate: {
60 urlValid: value => {
61 const res = isActivityPubUrlValid(value)
62 if (res === false) throw new Error('URL is not valid.')
63 }
64 }
65 },
66 publicKey: {
67 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max),
68 allowNull: true,
69 validate: {
70 publicKeyValid: value => {
71 const res = isAccountPublicKeyValid(value)
72 if (res === false) throw new Error('Public key is not valid.')
73 }
74 }
75 },
76 privateKey: {
77 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max),
78 allowNull: true,
79 validate: {
80 privateKeyValid: value => {
81 const res = isAccountPrivateKeyValid(value)
82 if (res === false) throw new Error('Private key is not valid.')
83 }
84 }
85 },
86 followersCount: {
87 type: DataTypes.INTEGER,
88 allowNull: false,
89 validate: {
90 followersCountValid: value => {
91 const res = isAccountFollowersCountValid(value)
92 if (res === false) throw new Error('Followers count is not valid.')
93 }
94 }
95 },
96 followingCount: {
97 type: DataTypes.INTEGER,
98 allowNull: false,
99 validate: {
100 followingCountValid: value => {
101 const res = isAccountFollowingCountValid(value)
102 if (res === false) throw new Error('Following count is not valid.')
103 }
104 }
105 },
106 inboxUrl: {
107 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
108 allowNull: false,
109 validate: {
110 inboxUrlValid: value => {
111 const res = isActivityPubUrlValid(value)
112 if (res === false) throw new Error('Inbox URL is not valid.')
113 }
114 }
115 },
116 outboxUrl: {
117 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
118 allowNull: false,
119 validate: {
120 outboxUrlValid: value => {
121 const res = isActivityPubUrlValid(value)
122 if (res === false) throw new Error('Outbox URL is not valid.')
123 }
124 }
125 },
126 sharedInboxUrl: {
127 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
128 allowNull: false,
129 validate: {
130 sharedInboxUrlValid: value => {
131 const res = isActivityPubUrlValid(value)
132 if (res === false) throw new Error('Shared inbox URL is not valid.')
133 }
134 }
135 },
136 followersUrl: {
137 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
138 allowNull: false,
139 validate: {
140 followersUrlValid: value => {
141 const res = isActivityPubUrlValid(value)
142 if (res === false) throw new Error('Followers URL is not valid.')
143 }
144 }
145 },
146 followingUrl: {
147 type: DataTypes.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max),
148 allowNull: false,
149 validate: {
150 followingUrlValid: value => {
151 const res = isActivityPubUrlValid(value)
152 if (res === false) throw new Error('Following URL is not valid.')
153 }
154 }
155 }
156 }, 44 },
157 { 45 {
158 indexes: [ 46 fields: [ 'serverId' ]
159 { 47 },
160 fields: [ 'name' ] 48 {
161 }, 49 fields: [ 'userId' ],
162 { 50 unique: true
163 fields: [ 'serverId' ] 51 },
164 }, 52 {
165 { 53 fields: [ 'applicationId' ],
166 fields: [ 'userId' ], 54 unique: true
167 unique: true 55 },
168 }, 56 {
169 { 57 fields: [ 'name', 'serverId', 'applicationId' ],
170 fields: [ 'applicationId' ], 58 unique: true
171 unique: true
172 },
173 {
174 fields: [ 'name', 'serverId', 'applicationId' ],
175 unique: true
176 }
177 ],
178 hooks: { afterDestroy }
179 } 59 }
180 )
181
182 const classMethods = [
183 associate,
184 loadApplication,
185 load,
186 loadByUUID,
187 loadByUrl,
188 loadLocalByName,
189 loadByNameAndHost,
190 listByFollowersUrls
191 ]
192 const instanceMethods = [
193 isOwned,
194 toActivityPubObject,
195 toFormattedJSON,
196 getFollowerSharedInboxUrls,
197 getFollowingUrl,
198 getFollowersUrl,
199 getPublicKeyUrl
200 ] 60 ]
201 addMethodsToModel(Account, classMethods, instanceMethods) 61})
202 62export class AccountModel extends Model<Account> {
203 return Account 63
204} 64 @AllowNull(false)
65 @Default(DataType.UUIDV4)
66 @IsUUID(4)
67 @Column(DataType.UUID)
68 uuid: string
69
70 @AllowNull(false)
71 @Is('AccountName', value => throwIfNotValid(value, isUserUsernameValid, 'account name'))
72 @Column
73 name: string
74
75 @AllowNull(false)
76 @Is('AccountUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
77 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
78 url: string
79
80 @AllowNull(true)
81 @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPublicKeyValid, 'public key'))
82 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PUBLIC_KEY.max))
83 publicKey: string
84
85 @AllowNull(true)
86 @Is('AccountPublicKey', value => throwIfNotValid(value, isAccountPrivateKeyValid, 'private key'))
87 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.PRIVATE_KEY.max))
88 privateKey: string
89
90 @AllowNull(false)
91 @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowersCountValid, 'followers count'))
92 @Column
93 followersCount: number
94
95 @AllowNull(false)
96 @Is('AccountFollowersCount', value => throwIfNotValid(value, isAccountFollowingCountValid, 'following count'))
97 @Column
98 followingCount: number
99
100 @AllowNull(false)
101 @Is('AccountInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'inbox url'))
102 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
103 inboxUrl: string
104
105 @AllowNull(false)
106 @Is('AccountOutboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'outbox url'))
107 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
108 outboxUrl: string
109
110 @AllowNull(false)
111 @Is('AccountSharedInboxUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'shared inbox url'))
112 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
113 sharedInboxUrl: string
114
115 @AllowNull(false)
116 @Is('AccountFollowersUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'followers url'))
117 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
118 followersUrl: string
119
120 @AllowNull(false)
121 @Is('AccountFollowingUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'following url'))
122 @Column(DataType.STRING(CONSTRAINTS_FIELDS.ACCOUNTS.URL.max))
123 followingUrl: string
124
125 @CreatedAt
126 createdAt: Date
127
128 @UpdatedAt
129 updatedAt: Date
130
131 @ForeignKey(() => AvatarModel)
132 @Column
133 avatarId: number
134
135 @BelongsTo(() => AvatarModel, {
136 foreignKey: {
137 allowNull: true
138 },
139 onDelete: 'cascade'
140 })
141 Avatar: AvatarModel
205 142
206// --------------------------------------------------------------------------- 143 @ForeignKey(() => ServerModel)
144 @Column
145 serverId: number
207 146
208function associate (models) { 147 @BelongsTo(() => ServerModel, {
209 Account.belongsTo(models.Server, {
210 foreignKey: { 148 foreignKey: {
211 name: 'serverId',
212 allowNull: true 149 allowNull: true
213 }, 150 },
214 onDelete: 'cascade' 151 onDelete: 'cascade'
215 }) 152 })
153 Server: ServerModel
216 154
217 Account.belongsTo(models.User, { 155 @ForeignKey(() => UserModel)
156 @Column
157 userId: number
158
159 @BelongsTo(() => UserModel, {
218 foreignKey: { 160 foreignKey: {
219 name: 'userId',
220 allowNull: true 161 allowNull: true
221 }, 162 },
222 onDelete: 'cascade' 163 onDelete: 'cascade'
223 }) 164 })
165 User: UserModel
166
167 @ForeignKey(() => ApplicationModel)
168 @Column
169 applicationId: number
224 170
225 Account.belongsTo(models.Application, { 171 @BelongsTo(() => ApplicationModel, {
226 foreignKey: { 172 foreignKey: {
227 name: 'applicationId',
228 allowNull: true 173 allowNull: true
229 }, 174 },
230 onDelete: 'cascade' 175 onDelete: 'cascade'
231 }) 176 })
177 Application: ApplicationModel
232 178
233 Account.hasMany(models.VideoChannel, { 179 @HasMany(() => VideoChannelModel, {
234 foreignKey: { 180 foreignKey: {
235 name: 'accountId',
236 allowNull: false 181 allowNull: false
237 }, 182 },
238 onDelete: 'cascade', 183 onDelete: 'cascade',
239 hooks: true 184 hooks: true
240 }) 185 })
186 VideoChannels: VideoChannelModel[]
241 187
242 Account.hasMany(models.AccountFollow, { 188 @HasMany(() => AccountFollowModel, {
243 foreignKey: { 189 foreignKey: {
244 name: 'accountId', 190 name: 'accountId',
245 allowNull: false 191 allowNull: false
246 }, 192 },
247 onDelete: 'cascade' 193 onDelete: 'cascade'
248 }) 194 })
195 AccountFollowing: AccountFollowModel[]
249 196
250 Account.hasMany(models.AccountFollow, { 197 @HasMany(() => AccountFollowModel, {
251 foreignKey: { 198 foreignKey: {
252 name: 'targetAccountId', 199 name: 'targetAccountId',
253 allowNull: false 200 allowNull: false
@@ -255,209 +202,199 @@ function associate (models) {
255 as: 'followers', 202 as: 'followers',
256 onDelete: 'cascade' 203 onDelete: 'cascade'
257 }) 204 })
205 AccountFollowers: AccountFollowModel[]
258 206
259 Account.hasOne(models.Avatar, { 207 @AfterDestroy
260 foreignKey: { 208 static sendDeleteIfOwned (instance: AccountModel) {
261 name: 'avatarId', 209 if (instance.isOwned()) {
262 allowNull: true 210 return sendDeleteAccount(instance, undefined)
263 }, 211 }
264 onDelete: 'cascade'
265 })
266}
267 212
268function afterDestroy (account: AccountInstance) { 213 return undefined
269 if (account.isOwned()) {
270 return sendDeleteAccount(account, undefined)
271 } 214 }
272 215
273 return undefined 216 static loadApplication () {
274} 217 return AccountModel.findOne({
218 include: [
219 {
220 model: ApplicationModel,
221 required: true
222 }
223 ]
224 })
225 }
275 226
276toFormattedJSON = function (this: AccountInstance) { 227 static load (id: number) {
277 let host = CONFIG.WEBSERVER.HOST 228 return AccountModel.findById(id)
278 let score: number 229 }
279 let avatar: Avatar = null
280 230
281 if (this.Avatar) { 231 static loadByUUID (uuid: string) {
282 avatar = { 232 const query = {
283 path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename), 233 where: {
284 createdAt: this.Avatar.createdAt, 234 uuid
285 updatedAt: this.Avatar.updatedAt 235 }
286 } 236 }
287 }
288 237
289 if (this.Server) { 238 return AccountModel.findOne(query)
290 host = this.Server.host
291 score = this.Server.score as number
292 } 239 }
293 240
294 const json = { 241 static loadLocalByName (name: string) {
295 id: this.id, 242 const query = {
296 uuid: this.uuid, 243 where: {
297 host, 244 name,
298 score, 245 [ Sequelize.Op.or ]: [
299 name: this.name, 246 {
300 followingCount: this.followingCount, 247 userId: {
301 followersCount: this.followersCount, 248 [ Sequelize.Op.ne ]: null
302 createdAt: this.createdAt, 249 }
303 updatedAt: this.updatedAt, 250 },
304 avatar 251 {
305 } 252 applicationId: {
253 [ Sequelize.Op.ne ]: null
254 }
255 }
256 ]
257 }
258 }
306 259
307 return json 260 return AccountModel.findOne(query)
308} 261 }
309 262
310toActivityPubObject = function (this: AccountInstance) { 263 static loadByNameAndHost (name: string, host: string) {
311 const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' 264 const query = {
312 265 where: {
313 const json = { 266 name
314 type, 267 },
315 id: this.url, 268 include: [
316 following: this.getFollowingUrl(), 269 {
317 followers: this.getFollowersUrl(), 270 model: ServerModel,
318 inbox: this.inboxUrl, 271 required: true,
319 outbox: this.outboxUrl, 272 where: {
320 preferredUsername: this.name, 273 host
321 url: this.url, 274 }
322 name: this.name, 275 }
323 endpoints: { 276 ]
324 sharedInbox: this.sharedInboxUrl
325 },
326 uuid: this.uuid,
327 publicKey: {
328 id: this.getPublicKeyUrl(),
329 owner: this.url,
330 publicKeyPem: this.publicKey
331 } 277 }
278
279 return AccountModel.findOne(query)
332 } 280 }
333 281
334 return activityPubContextify(json) 282 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
335} 283 const query = {
284 where: {
285 url
286 },
287 transaction
288 }
336 289
337isOwned = function (this: AccountInstance) { 290 return AccountModel.findOne(query)
338 return this.serverId === null 291 }
339}
340 292
341getFollowerSharedInboxUrls = function (this: AccountInstance, t: Sequelize.Transaction) { 293 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
342 const query: Sequelize.FindOptions<AccountAttributes> = { 294 const query = {
343 attributes: [ 'sharedInboxUrl' ], 295 where: {
344 include: [ 296 followersUrl: {
345 { 297 [ Sequelize.Op.in ]: followersUrls
346 model: Account['sequelize'].models.AccountFollow,
347 required: true,
348 as: 'followers',
349 where: {
350 targetAccountId: this.id
351 } 298 }
352 } 299 },
353 ], 300 transaction
354 transaction: t 301 }
355 }
356 302
357 return Account.findAll(query) 303 return AccountModel.findAll(query)
358 .then(accounts => accounts.map(a => a.sharedInboxUrl)) 304 }
359}
360 305
361getFollowingUrl = function (this: AccountInstance) { 306 toFormattedJSON () {
362 return this.url + '/following' 307 let host = CONFIG.WEBSERVER.HOST
363} 308 let score: number
309 let avatar: Avatar = null
364 310
365getFollowersUrl = function (this: AccountInstance) { 311 if (this.Avatar) {
366 return this.url + '/followers' 312 avatar = {
367} 313 path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename),
314 createdAt: this.Avatar.createdAt,
315 updatedAt: this.Avatar.updatedAt
316 }
317 }
368 318
369getPublicKeyUrl = function (this: AccountInstance) { 319 if (this.Server) {
370 return this.url + '#main-key' 320 host = this.Server.host
371} 321 score = this.Server.score
322 }
372 323
373// ------------------------------ STATICS ------------------------------ 324 return {
325 id: this.id,
326 uuid: this.uuid,
327 host,
328 score,
329 name: this.name,
330 followingCount: this.followingCount,
331 followersCount: this.followersCount,
332 createdAt: this.createdAt,
333 updatedAt: this.updatedAt,
334 avatar
335 }
336 }
374 337
375loadApplication = function () { 338 toActivityPubObject () {
376 return Account.findOne({ 339 const type = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person'
377 include: [ 340
378 { 341 const json = {
379 model: Account['sequelize'].models.Application, 342 type,
380 required: true 343 id: this.url,
344 following: this.getFollowingUrl(),
345 followers: this.getFollowersUrl(),
346 inbox: this.inboxUrl,
347 outbox: this.outboxUrl,
348 preferredUsername: this.name,
349 url: this.url,
350 name: this.name,
351 endpoints: {
352 sharedInbox: this.sharedInboxUrl
353 },
354 uuid: this.uuid,
355 publicKey: {
356 id: this.getPublicKeyUrl(),
357 owner: this.url,
358 publicKeyPem: this.publicKey
381 } 359 }
382 ]
383 })
384}
385
386load = function (id: number) {
387 return Account.findById(id)
388}
389
390loadByUUID = function (uuid: string) {
391 const query: Sequelize.FindOptions<AccountAttributes> = {
392 where: {
393 uuid
394 } 360 }
361
362 return activityPubContextify(json)
395 } 363 }
396 364
397 return Account.findOne(query) 365 isOwned () {
398} 366 return this.serverId === null
367 }
399 368
400loadLocalByName = function (name: string) { 369 getFollowerSharedInboxUrls (t: Sequelize.Transaction) {
401 const query: Sequelize.FindOptions<AccountAttributes> = { 370 const query = {
402 where: { 371 attributes: [ 'sharedInboxUrl' ],
403 name, 372 include: [
404 [Sequelize.Op.or]: [
405 {
406 userId: {
407 [Sequelize.Op.ne]: null
408 }
409 },
410 { 373 {
411 applicationId: { 374 model: AccountFollowModel,
412 [Sequelize.Op.ne]: null 375 required: true,
376 as: 'followers',
377 where: {
378 targetAccountId: this.id
413 } 379 }
414 } 380 }
415 ] 381 ],
382 transaction: t
416 } 383 }
417 }
418 384
419 return Account.findOne(query) 385 return AccountModel.findAll(query)
420} 386 .then(accounts => accounts.map(a => a.sharedInboxUrl))
421
422loadByNameAndHost = function (name: string, host: string) {
423 const query: Sequelize.FindOptions<AccountAttributes> = {
424 where: {
425 name
426 },
427 include: [
428 {
429 model: Account['sequelize'].models.Server,
430 required: true,
431 where: {
432 host
433 }
434 }
435 ]
436 } 387 }
437 388
438 return Account.findOne(query) 389 getFollowingUrl () {
439} 390 return this.url + '/following'
440
441loadByUrl = function (url: string, transaction?: Sequelize.Transaction) {
442 const query: Sequelize.FindOptions<AccountAttributes> = {
443 where: {
444 url
445 },
446 transaction
447 } 391 }
448 392
449 return Account.findOne(query) 393 getFollowersUrl () {
450} 394 return this.url + '/followers'
451
452listByFollowersUrls = function (followersUrls: string[], transaction?: Sequelize.Transaction) {
453 const query: Sequelize.FindOptions<AccountAttributes> = {
454 where: {
455 followersUrl: {
456 [Sequelize.Op.in]: followersUrls
457 }
458 },
459 transaction
460 } 395 }
461 396
462 return Account.findAll(query) 397 getPublicKeyUrl () {
398 return this.url + '#main-key'
399 }
463} 400}
diff --git a/server/models/account/index.ts b/server/models/account/index.ts
deleted file mode 100644
index 179f66974..000000000
--- a/server/models/account/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
1export * from './account-interface'
2export * from './account-follow-interface'
3export * from './account-video-rate-interface'
4export * from './user-interface'
diff --git a/server/models/account/user-interface.ts b/server/models/account/user-interface.ts
deleted file mode 100644
index 0f0b72063..000000000
--- a/server/models/account/user-interface.ts
+++ /dev/null
@@ -1,67 +0,0 @@
1import * as Bluebird from 'bluebird'
2import * as Sequelize from 'sequelize'
3import { ResultList } from '../../../shared/models/result-list.model'
4import { UserRight } from '../../../shared/models/users/user-right.enum'
5import { UserRole } from '../../../shared/models/users/user-role'
6import { User as FormattedUser } from '../../../shared/models/users/user.model'
7import { AccountInstance } from './account-interface'
8
9export namespace UserMethods {
10 export type HasRight = (this: UserInstance, right: UserRight) => boolean
11 export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean>
12
13 export type ToFormattedJSON = (this: UserInstance) => FormattedUser
14 export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
15
16 export type CountTotal = () => Bluebird<number>
17
18 export type GetByUsername = (username: string) => Bluebird<UserInstance>
19
20 export type ListForApi = (start: number, count: number, sort: string) => Bluebird< ResultList<UserInstance> >
21
22 export type LoadById = (id: number) => Bluebird<UserInstance>
23
24 export type LoadByUsername = (username: string) => Bluebird<UserInstance>
25 export type LoadByUsernameAndPopulateChannels = (username: string) => Bluebird<UserInstance>
26
27 export type LoadByUsernameOrEmail = (username: string, email: string) => Bluebird<UserInstance>
28}
29
30export interface UserClass {
31 isPasswordMatch: UserMethods.IsPasswordMatch,
32 toFormattedJSON: UserMethods.ToFormattedJSON,
33 hasRight: UserMethods.HasRight,
34 isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo,
35
36 countTotal: UserMethods.CountTotal,
37 getByUsername: UserMethods.GetByUsername,
38 listForApi: UserMethods.ListForApi,
39 loadById: UserMethods.LoadById,
40 loadByUsername: UserMethods.LoadByUsername,
41 loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels,
42 loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
43}
44
45export interface UserAttributes {
46 id?: number
47 password: string
48 username: string
49 email: string
50 displayNSFW?: boolean
51 role: UserRole
52 videoQuota: number
53
54 Account?: AccountInstance
55}
56
57export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance<UserAttributes> {
58 id: number
59 createdAt: Date
60 updatedAt: Date
61
62 isPasswordMatch: UserMethods.IsPasswordMatch
63 toFormattedJSON: UserMethods.ToFormattedJSON
64 hasRight: UserMethods.HasRight
65}
66
67export interface UserModel extends UserClass, Sequelize.Model<UserInstance, UserAttributes> {}
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 3705947c0..84adad96e 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -1,301 +1,251 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import {
3 AllowNull,
4 BeforeCreate,
5 BeforeUpdate,
6 Column, CreatedAt,
7 DataType,
8 Default,
9 HasMany,
10 HasOne,
11 Is,
12 IsEmail,
13 Model,
14 Table, UpdatedAt
15} from 'sequelize-typescript'
2import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' 16import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
3import { 17import {
4 comparePassword, 18 comparePassword,
5 cryptPassword, 19 cryptPassword
6 isUserDisplayNSFWValid,
7 isUserPasswordValid,
8 isUserRoleValid,
9 isUserUsernameValid,
10 isUserVideoQuotaValid
11} from '../../helpers' 20} from '../../helpers'
12import { addMethodsToModel, getSort } from '../utils' 21import {
13import { UserAttributes, UserInstance, UserMethods } from './user-interface' 22 isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
14 23 isUserVideoQuotaValid
15let User: Sequelize.Model<UserInstance, UserAttributes> 24} from '../../helpers/custom-validators/users'
16let isPasswordMatch: UserMethods.IsPasswordMatch 25import { OAuthTokenModel } from '../oauth/oauth-token'
17let hasRight: UserMethods.HasRight 26import { getSort, throwIfNotValid } from '../utils'
18let toFormattedJSON: UserMethods.ToFormattedJSON 27import { VideoChannelModel } from '../video/video-channel'
19let countTotal: UserMethods.CountTotal 28import { AccountModel } from './account'
20let getByUsername: UserMethods.GetByUsername 29
21let listForApi: UserMethods.ListForApi 30@Table({
22let loadById: UserMethods.LoadById 31 tableName: 'user',
23let loadByUsername: UserMethods.LoadByUsername 32 indexes: [
24let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels
25let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail
26let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo
27
28export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
29 User = sequelize.define<UserInstance, UserAttributes>('User',
30 { 33 {
31 password: { 34 fields: [ 'username' ],
32 type: DataTypes.STRING, 35 unique: true
33 allowNull: false,
34 validate: {
35 passwordValid: value => {
36 const res = isUserPasswordValid(value)
37 if (res === false) throw new Error('Password not valid.')
38 }
39 }
40 },
41 username: {
42 type: DataTypes.STRING,
43 allowNull: false,
44 validate: {
45 usernameValid: value => {
46 const res = isUserUsernameValid(value)
47 if (res === false) throw new Error('Username not valid.')
48 }
49 }
50 },
51 email: {
52 type: DataTypes.STRING(400),
53 allowNull: false,
54 validate: {
55 isEmail: true
56 }
57 },
58 displayNSFW: {
59 type: DataTypes.BOOLEAN,
60 allowNull: false,
61 defaultValue: false,
62 validate: {
63 nsfwValid: value => {
64 const res = isUserDisplayNSFWValid(value)
65 if (res === false) throw new Error('Display NSFW is not valid.')
66 }
67 }
68 },
69 role: {
70 type: DataTypes.INTEGER,
71 allowNull: false,
72 validate: {
73 roleValid: value => {
74 const res = isUserRoleValid(value)
75 if (res === false) throw new Error('Role is not valid.')
76 }
77 }
78 },
79 videoQuota: {
80 type: DataTypes.BIGINT,
81 allowNull: false,
82 validate: {
83 videoQuotaValid: value => {
84 const res = isUserVideoQuotaValid(value)
85 if (res === false) throw new Error('Video quota is not valid.')
86 }
87 }
88 }
89 }, 36 },
90 { 37 {
91 indexes: [ 38 fields: [ 'email' ],
92 { 39 unique: true
93 fields: [ 'username' ],
94 unique: true
95 },
96 {
97 fields: [ 'email' ],
98 unique: true
99 }
100 ],
101 hooks: {
102 beforeCreate: beforeCreateOrUpdate,
103 beforeUpdate: beforeCreateOrUpdate
104 }
105 } 40 }
106 )
107
108 const classMethods = [
109 associate,
110
111 countTotal,
112 getByUsername,
113 listForApi,
114 loadById,
115 loadByUsername,
116 loadByUsernameAndPopulateChannels,
117 loadByUsernameOrEmail
118 ] 41 ]
119 const instanceMethods = [ 42})
120 hasRight, 43export class UserModel extends Model<UserModel> {
121 isPasswordMatch, 44
122 toFormattedJSON, 45 @AllowNull(false)
123 isAbleToUploadVideo 46 @Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password'))
124 ] 47 @Column
125 addMethodsToModel(User, classMethods, instanceMethods) 48 password: string
126 49
127 return User 50 @AllowNull(false)
128} 51 @Is('UserPassword', value => throwIfNotValid(value, isUserUsernameValid, 'user name'))
52 @Column
53 username: string
54
55 @AllowNull(false)
56 @IsEmail
57 @Column(DataType.STRING(400))
58 email: string
59
60 @AllowNull(false)
61 @Default(false)
62 @Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean'))
63 @Column
64 displayNSFW: boolean
65
66 @AllowNull(false)
67 @Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
68 @Column
69 role: number
70
71 @AllowNull(false)
72 @Is('UserVideoQuota', value => throwIfNotValid(value, isUserVideoQuotaValid, 'video quota'))
73 @Column(DataType.BIGINT)
74 videoQuota: number
75
76 @CreatedAt
77 createdAt: Date
78
79 @UpdatedAt
80 updatedAt: Date
81
82 @HasOne(() => AccountModel, {
83 foreignKey: 'userId',
84 onDelete: 'cascade'
85 })
86 Account: AccountModel
129 87
130function beforeCreateOrUpdate (user: UserInstance) { 88 @HasMany(() => OAuthTokenModel, {
131 if (user.changed('password')) { 89 foreignKey: 'userId',
132 return cryptPassword(user.password) 90 onDelete: 'cascade'
133 .then(hash => { 91 })
134 user.password = hash 92 OAuthTokens: OAuthTokenModel[]
135 return undefined 93
136 }) 94 @BeforeCreate
95 @BeforeUpdate
96 static cryptPasswordIfNeeded (instance: UserModel) {
97 if (instance.changed('password')) {
98 return cryptPassword(instance.password)
99 .then(hash => {
100 instance.password = hash
101 return undefined
102 })
103 }
137 } 104 }
138}
139
140// ------------------------------ METHODS ------------------------------
141 105
142hasRight = function (this: UserInstance, right: UserRight) { 106 static countTotal () {
143 return hasUserRight(this.role, right) 107 return this.count()
144} 108 }
145 109
146isPasswordMatch = function (this: UserInstance, password: string) { 110 static getByUsername (username: string) {
147 return comparePassword(password, this.password) 111 const query = {
148} 112 where: {
113 username: username
114 },
115 include: [ { model: AccountModel, required: true } ]
116 }
149 117
150toFormattedJSON = function (this: UserInstance) { 118 return UserModel.findOne(query)
151 const json = {
152 id: this.id,
153 username: this.username,
154 email: this.email,
155 displayNSFW: this.displayNSFW,
156 role: this.role,
157 roleLabel: USER_ROLE_LABELS[this.role],
158 videoQuota: this.videoQuota,
159 createdAt: this.createdAt,
160 account: this.Account.toFormattedJSON()
161 } 119 }
162 120
163 if (Array.isArray(this.Account.VideoChannels) === true) { 121 static listForApi (start: number, count: number, sort: string) {
164 const videoChannels = this.Account.VideoChannels 122 const query = {
165 .map(c => c.toFormattedJSON()) 123 offset: start,
166 .sort((v1, v2) => { 124 limit: count,
167 if (v1.createdAt < v2.createdAt) return -1 125 order: [ getSort(sort) ],
168 if (v1.createdAt === v2.createdAt) return 0 126 include: [ { model: AccountModel, required: true } ]
127 }
169 128
170 return 1 129 return UserModel.findAndCountAll(query)
130 .then(({ rows, count }) => {
131 return {
132 data: rows,
133 total: count
134 }
171 }) 135 })
172
173 json['videoChannels'] = videoChannels
174 } 136 }
175 137
176 return json 138 static loadById (id: number) {
177} 139 const options = {
178 140 include: [ { model: AccountModel, required: true } ]
179isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) { 141 }
180 if (this.videoQuota === -1) return Promise.resolve(true)
181
182 return getOriginalVideoFileTotalFromUser(this).then(totalBytes => {
183 return (videoFile.size + totalBytes) < this.videoQuota
184 })
185}
186
187// ------------------------------ STATICS ------------------------------
188
189function associate (models) {
190 User.hasOne(models.Account, {
191 foreignKey: 'userId',
192 onDelete: 'cascade'
193 })
194 142
195 User.hasMany(models.OAuthToken, { 143 return UserModel.findById(id, options)
196 foreignKey: 'userId', 144 }
197 onDelete: 'cascade'
198 })
199}
200 145
201countTotal = function () { 146 static loadByUsername (username: string) {
202 return this.count() 147 const query = {
203} 148 where: {
149 username
150 },
151 include: [ { model: AccountModel, required: true } ]
152 }
204 153
205getByUsername = function (username: string) { 154 return UserModel.findOne(query)
206 const query = {
207 where: {
208 username: username
209 },
210 include: [ { model: User['sequelize'].models.Account, required: true } ]
211 } 155 }
212 156
213 return User.findOne(query) 157 static loadByUsernameAndPopulateChannels (username: string) {
214} 158 const query = {
159 where: {
160 username
161 },
162 include: [
163 {
164 model: AccountModel,
165 required: true,
166 include: [ VideoChannelModel ]
167 }
168 ]
169 }
215 170
216listForApi = function (start: number, count: number, sort: string) { 171 return UserModel.findOne(query)
217 const query = {
218 offset: start,
219 limit: count,
220 order: [ getSort(sort) ],
221 include: [ { model: User['sequelize'].models.Account, required: true } ]
222 } 172 }
223 173
224 return User.findAndCountAll(query).then(({ rows, count }) => { 174 static loadByUsernameOrEmail (username: string, email: string) {
225 return { 175 const query = {
226 data: rows, 176 include: [ { model: AccountModel, required: true } ],
227 total: count 177 where: {
178 [ Sequelize.Op.or ]: [ { username }, { email } ]
179 }
228 } 180 }
229 })
230}
231 181
232loadById = function (id: number) { 182 // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387
233 const options = { 183 return (UserModel as any).findOne(query)
234 include: [ { model: User['sequelize'].models.Account, required: true } ]
235 } 184 }
236 185
237 return User.findById(id, options) 186 private static getOriginalVideoFileTotalFromUser (user: UserModel) {
238} 187 // Don't use sequelize because we need to use a sub query
188 const query = 'SELECT SUM("size") AS "total" FROM ' +
189 '(SELECT MAX("videoFile"."size") AS "size" FROM "videoFile" ' +
190 'INNER JOIN "video" ON "videoFile"."videoId" = "video"."id" ' +
191 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
192 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
193 'INNER JOIN "user" ON "account"."userId" = "user"."id" ' +
194 'WHERE "user"."id" = $userId GROUP BY "video"."id") t'
195
196 const options = {
197 bind: { userId: user.id },
198 type: Sequelize.QueryTypes.SELECT
199 }
200 return UserModel.sequelize.query(query, options)
201 .then(([ { total } ]) => {
202 if (total === null) return 0
239 203
240loadByUsername = function (username: string) { 204 return parseInt(total, 10)
241 const query = { 205 })
242 where: {
243 username
244 },
245 include: [ { model: User['sequelize'].models.Account, required: true } ]
246 } 206 }
247 207
248 return User.findOne(query) 208 hasRight (right: UserRight) {
249} 209 return hasUserRight(this.role, right)
210 }
250 211
251loadByUsernameAndPopulateChannels = function (username: string) { 212 isPasswordMatch (password: string) {
252 const query = { 213 return comparePassword(password, this.password)
253 where: {
254 username
255 },
256 include: [
257 {
258 model: User['sequelize'].models.Account,
259 required: true,
260 include: [ User['sequelize'].models.VideoChannel ]
261 }
262 ]
263 } 214 }
264 215
265 return User.findOne(query) 216 toFormattedJSON () {
266} 217 const json = {
218 id: this.id,
219 username: this.username,
220 email: this.email,
221 displayNSFW: this.displayNSFW,
222 role: this.role,
223 roleLabel: USER_ROLE_LABELS[ this.role ],
224 videoQuota: this.videoQuota,
225 createdAt: this.createdAt,
226 account: this.Account.toFormattedJSON()
227 }
228
229 if (Array.isArray(this.Account.VideoChannels) === true) {
230 json['videoChannels'] = this.Account.VideoChannels
231 .map(c => c.toFormattedJSON())
232 .sort((v1, v2) => {
233 if (v1.createdAt < v2.createdAt) return -1
234 if (v1.createdAt === v2.createdAt) return 0
267 235
268loadByUsernameOrEmail = function (username: string, email: string) { 236 return 1
269 const query = { 237 })
270 include: [ { model: User['sequelize'].models.Account, required: true } ],
271 where: {
272 [Sequelize.Op.or]: [ { username }, { email } ]
273 } 238 }
239
240 return json
274 } 241 }
275 242
276 // FIXME: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18387 243 isAbleToUploadVideo (videoFile: Express.Multer.File) {
277 return (User as any).findOne(query) 244 if (this.videoQuota === -1) return Promise.resolve(true)
278}
279 245
280// --------------------------------------------------------------------------- 246 return UserModel.getOriginalVideoFileTotalFromUser(this)
281 247 .then(totalBytes => {
282function getOriginalVideoFileTotalFromUser (user: UserInstance) { 248 return (videoFile.size + totalBytes) < this.videoQuota
283 // Don't use sequelize because we need to use a sub query 249 })
284 const query = 'SELECT SUM("size") AS "total" FROM ' +
285 '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' +
286 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' +
287 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' +
288 'INNER JOIN "Accounts" ON "VideoChannels"."accountId" = "Accounts"."id" ' +
289 'INNER JOIN "Users" ON "Accounts"."userId" = "Users"."id" ' +
290 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t'
291
292 const options = {
293 bind: { userId: user.id },
294 type: Sequelize.QueryTypes.SELECT
295 } 250 }
296 return User['sequelize'].query(query, options).then(([ { total } ]) => {
297 if (total === null) return 0
298
299 return parseInt(total, 10)
300 })
301} 251}