aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account-follow.ts228
-rw-r--r--server/models/account/account.ts111
-rw-r--r--server/models/account/user.ts23
-rw-r--r--server/models/activitypub/actor-follow.ts260
-rw-r--r--server/models/activitypub/actor.ts165
-rw-r--r--server/models/application/application.ts23
-rw-r--r--server/models/video/video-abuse.ts13
-rw-r--r--server/models/video/video-channel-share.ts96
-rw-r--r--server/models/video/video-channel.ts164
-rw-r--r--server/models/video/video-share.ts32
-rw-r--r--server/models/video/video.ts78
11 files changed, 599 insertions, 594 deletions
diff --git a/server/models/account/account-follow.ts b/server/models/account/account-follow.ts
deleted file mode 100644
index 975e7ee7d..000000000
--- a/server/models/account/account-follow.ts
+++ /dev/null
@@ -1,228 +0,0 @@
1import * as Bluebird from 'bluebird'
2import { values } from 'lodash'
3import * as Sequelize from 'sequelize'
4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
5import { FollowState } from '../../../shared/models/accounts'
6import { FOLLOW_STATES } from '../../initializers/constants'
7import { ServerModel } from '../server/server'
8import { getSort } from '../utils'
9import { AccountModel } from './account'
10
11@Table({
12 tableName: 'accountFollow',
13 indexes: [
14 {
15 fields: [ 'accountId' ]
16 },
17 {
18 fields: [ 'targetAccountId' ]
19 },
20 {
21 fields: [ 'accountId', 'targetAccountId' ],
22 unique: true
23 }
24 ]
25})
26export class AccountFollowModel extends Model<AccountFollowModel> {
27
28 @AllowNull(false)
29 @Column(DataType.ENUM(values(FOLLOW_STATES)))
30 state: FollowState
31
32 @CreatedAt
33 createdAt: Date
34
35 @UpdatedAt
36 updatedAt: Date
37
38 @ForeignKey(() => AccountModel)
39 @Column
40 accountId: number
41
42 @BelongsTo(() => AccountModel, {
43 foreignKey: {
44 name: 'accountId',
45 allowNull: false
46 },
47 as: 'AccountFollower',
48 onDelete: 'CASCADE'
49 })
50 AccountFollower: AccountModel
51
52 @ForeignKey(() => AccountModel)
53 @Column
54 targetAccountId: number
55
56 @BelongsTo(() => AccountModel, {
57 foreignKey: {
58 name: 'targetAccountId',
59 allowNull: false
60 },
61 as: 'AccountFollowing',
62 onDelete: 'CASCADE'
63 })
64 AccountFollowing: AccountModel
65
66 static loadByAccountAndTarget (accountId: number, targetAccountId: number, t?: Sequelize.Transaction) {
67 const query = {
68 where: {
69 accountId,
70 targetAccountId
71 },
72 include: [
73 {
74 model: AccountModel,
75 required: true,
76 as: 'AccountFollower'
77 },
78 {
79 model: AccountModel,
80 required: true,
81 as: 'AccountFollowing'
82 }
83 ],
84 transaction: t
85 }
86
87 return AccountFollowModel.findOne(query)
88 }
89
90 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
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 }
113
114 return AccountFollowModel.findAndCountAll(query)
115 .then(({ rows, count }) => {
116 return {
117 data: rows,
118 total: count
119 }
120 })
121 }
122
123 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
124 const query = {
125 distinct: true,
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 ]
145 }
146
147 return AccountFollowModel.findAndCountAll(query)
148 .then(({ rows, count }) => {
149 return {
150 data: rows,
151 total: count
152 }
153 })
154 }
155
156 static listAcceptedFollowerUrlsForApi (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
157 return AccountFollowModel.createListAcceptedFollowForApiQuery('followers', accountIds, t, start, count)
158 }
159
160 static listAcceptedFollowerSharedInboxUrls (accountIds: number[], t: Sequelize.Transaction) {
161 return AccountFollowModel.createListAcceptedFollowForApiQuery('followers', accountIds, t, undefined, undefined, 'sharedInboxUrl')
162 }
163
164 static listAcceptedFollowingUrlsForApi (accountIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
165 return AccountFollowModel.createListAcceptedFollowForApiQuery('following', accountIds, t, start, count)
166 }
167
168 private static async createListAcceptedFollowForApiQuery (type: 'followers' | 'following',
169 accountIds: number[],
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 }
184
185 const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ]
186 const tasks: Bluebird<any>[] = []
187
188 for (const selection of selections) {
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\' '
193
194 if (count !== undefined) query += 'LIMIT ' + count
195 if (start !== undefined) query += ' OFFSET ' + start
196
197 const options = {
198 bind: { accountIds },
199 type: Sequelize.QueryTypes.SELECT,
200 transaction: t
201 }
202 tasks.push(AccountFollowModel.sequelize.query(query, options))
203 }
204
205 const [ followers, [ { total } ] ] = await
206 Promise.all(tasks)
207 const urls: string[] = followers.map(f => f.url)
208
209 return {
210 data: urls,
211 total: parseInt(total, 10)
212 }
213 }
214
215 toFormattedJSON () {
216 const follower = this.AccountFollower.toFormattedJSON()
217 const following = this.AccountFollowing.toFormattedJSON()
218
219 return {
220 id: this.id,
221 follower,
222 following,
223 state: this.state,
224 createdAt: this.createdAt,
225 updatedAt: this.updatedAt
226 }
227 }
228}
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index b26395fd4..1ee232537 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -5,18 +5,16 @@ import {
5 BelongsTo, 5 BelongsTo,
6 Column, 6 Column,
7 CreatedAt, 7 CreatedAt,
8 DataType, 8 DefaultScope,
9 Default,
10 ForeignKey, 9 ForeignKey,
11 HasMany, 10 HasMany,
12 Is, 11 Is,
13 IsUUID,
14 Model, 12 Model,
15 Table, 13 Table,
16 UpdatedAt 14 UpdatedAt
17} from 'sequelize-typescript' 15} from 'sequelize-typescript'
18import { isUserUsernameValid } from '../../helpers/custom-validators/users' 16import { isUserUsernameValid } from '../../helpers/custom-validators/users'
19import { sendDeleteAccount } from '../../lib/activitypub/send' 17import { sendDeleteActor } from '../../lib/activitypub/send'
20import { ActorModel } from '../activitypub/actor' 18import { ActorModel } from '../activitypub/actor'
21import { ApplicationModel } from '../application/application' 19import { ApplicationModel } from '../application/application'
22import { ServerModel } from '../server/server' 20import { ServerModel } from '../server/server'
@@ -24,31 +22,30 @@ import { throwIfNotValid } from '../utils'
24import { VideoChannelModel } from '../video/video-channel' 22import { VideoChannelModel } from '../video/video-channel'
25import { UserModel } from './user' 23import { UserModel } from './user'
26 24
27@Table({ 25@DefaultScope({
28 tableName: 'account', 26 include: [
29 indexes: [
30 {
31 fields: [ 'name' ]
32 },
33 {
34 fields: [ 'serverId' ]
35 },
36 {
37 fields: [ 'userId' ],
38 unique: true
39 },
40 {
41 fields: [ 'applicationId' ],
42 unique: true
43 },
44 { 27 {
45 fields: [ 'name', 'serverId', 'applicationId' ], 28 model: () => ActorModel,
46 unique: true 29 required: true,
30 include: [
31 {
32 model: () => ServerModel,
33 required: false
34 }
35 ]
47 } 36 }
48 ] 37 ]
49}) 38})
39@Table({
40 tableName: 'account'
41})
50export class AccountModel extends Model<AccountModel> { 42export class AccountModel extends Model<AccountModel> {
51 43
44 @AllowNull(false)
45 @Is('AccountName', value => throwIfNotValid(value, isUserUsernameValid, 'account name'))
46 @Column
47 name: string
48
52 @CreatedAt 49 @CreatedAt
53 createdAt: Date 50 createdAt: Date
54 51
@@ -89,7 +86,7 @@ export class AccountModel extends Model<AccountModel> {
89 }, 86 },
90 onDelete: 'cascade' 87 onDelete: 'cascade'
91 }) 88 })
92 Application: ApplicationModel 89 Account: ApplicationModel
93 90
94 @HasMany(() => VideoChannelModel, { 91 @HasMany(() => VideoChannelModel, {
95 foreignKey: { 92 foreignKey: {
@@ -103,32 +100,27 @@ export class AccountModel extends Model<AccountModel> {
103 @AfterDestroy 100 @AfterDestroy
104 static sendDeleteIfOwned (instance: AccountModel) { 101 static sendDeleteIfOwned (instance: AccountModel) {
105 if (instance.isOwned()) { 102 if (instance.isOwned()) {
106 return sendDeleteAccount(instance, undefined) 103 return sendDeleteActor(instance.Actor, undefined)
107 } 104 }
108 105
109 return undefined 106 return undefined
110 } 107 }
111 108
112 static loadApplication () {
113 return AccountModel.findOne({
114 include: [
115 {
116 model: ApplicationModel,
117 required: true
118 }
119 ]
120 })
121 }
122
123 static load (id: number) { 109 static load (id: number) {
124 return AccountModel.findById(id) 110 return AccountModel.findById(id)
125 } 111 }
126 112
127 static loadByUUID (uuid: string) { 113 static loadByUUID (uuid: string) {
128 const query = { 114 const query = {
129 where: { 115 include: [
130 uuid 116 {
131 } 117 model: ActorModel,
118 required: true,
119 where: {
120 uuid
121 }
122 }
123 ]
132 } 124 }
133 125
134 return AccountModel.findOne(query) 126 return AccountModel.findOne(query)
@@ -156,25 +148,6 @@ export class AccountModel extends Model<AccountModel> {
156 return AccountModel.findOne(query) 148 return AccountModel.findOne(query)
157 } 149 }
158 150
159 static loadByNameAndHost (name: string, host: string) {
160 const query = {
161 where: {
162 name
163 },
164 include: [
165 {
166 model: ServerModel,
167 required: true,
168 where: {
169 host
170 }
171 }
172 ]
173 }
174
175 return AccountModel.findOne(query)
176 }
177
178 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 151 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
179 const query = { 152 const query = {
180 include: [ 153 include: [
@@ -192,29 +165,11 @@ export class AccountModel extends Model<AccountModel> {
192 return AccountModel.findOne(query) 165 return AccountModel.findOne(query)
193 } 166 }
194 167
195 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
196 const query = {
197 include: [
198 {
199 model: ActorModel,
200 required: true,
201 where: {
202 followersUrl: {
203 [ Sequelize.Op.in ]: followersUrls
204 }
205 }
206 }
207 ],
208 transaction
209 }
210
211 return AccountModel.findAll(query)
212 }
213
214 toFormattedJSON () { 168 toFormattedJSON () {
215 const actor = this.Actor.toFormattedJSON() 169 const actor = this.Actor.toFormattedJSON()
216 const account = { 170 const account = {
217 id: this.id, 171 id: this.id,
172 name: this.name,
218 createdAt: this.createdAt, 173 createdAt: this.createdAt,
219 updatedAt: this.updatedAt 174 updatedAt: this.updatedAt
220 } 175 }
@@ -223,7 +178,7 @@ export class AccountModel extends Model<AccountModel> {
223 } 178 }
224 179
225 toActivityPubObject () { 180 toActivityPubObject () {
226 return this.Actor.toActivityPubObject(this.name, this.uuid, 'Account') 181 return this.Actor.toActivityPubObject(this.name, 'Account')
227 } 182 }
228 183
229 isOwned () { 184 isOwned () {
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 70ed61e07..1d5759ea3 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -1,26 +1,13 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { 2import {
3 AllowNull, 3 AllowNull, BeforeCreate, BeforeUpdate, Column, CreatedAt, DataType, Default, DefaultScope, HasMany, HasOne, Is, IsEmail, Model,
4 BeforeCreate, 4 Scopes, Table, UpdatedAt
5 BeforeUpdate,
6 Column, CreatedAt,
7 DataType,
8 Default, DefaultScope,
9 HasMany,
10 HasOne,
11 Is,
12 IsEmail,
13 Model, Scopes,
14 Table, UpdatedAt
15} from 'sequelize-typescript' 5} from 'sequelize-typescript'
16import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' 6import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
7import { comparePassword, cryptPassword } from '../../helpers'
17import { 8import {
18 comparePassword, 9 isUserAutoPlayVideoValid, isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
19 cryptPassword 10 isUserVideoQuotaValid
20} from '../../helpers'
21import {
22 isUserDisplayNSFWValid, isUserPasswordValid, isUserRoleValid, isUserUsernameValid,
23 isUserVideoQuotaValid, isUserAutoPlayVideoValid
24} from '../../helpers/custom-validators/users' 11} from '../../helpers/custom-validators/users'
25import { OAuthTokenModel } from '../oauth/oauth-token' 12import { OAuthTokenModel } from '../oauth/oauth-token'
26import { getSort, throwIfNotValid } from '../utils' 13import { getSort, throwIfNotValid } from '../utils'
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
new file mode 100644
index 000000000..4cba05e95
--- /dev/null
+++ b/server/models/activitypub/actor-follow.ts
@@ -0,0 +1,260 @@
1import * as Bluebird from 'bluebird'
2import { values } from 'lodash'
3import * as Sequelize from 'sequelize'
4import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
5import { FollowState } from '../../../shared/models/actors'
6import { FOLLOW_STATES } from '../../initializers/constants'
7import { ServerModel } from '../server/server'
8import { getSort } from '../utils'
9import { ActorModel } from './actor'
10
11@Table({
12 tableName: 'actorFollow',
13 indexes: [
14 {
15 fields: [ 'actorId' ]
16 },
17 {
18 fields: [ 'targetActorId' ]
19 },
20 {
21 fields: [ 'actorId', 'targetActorId' ],
22 unique: true
23 }
24 ]
25})
26export class ActorFollowModel extends Model<ActorFollowModel> {
27
28 @AllowNull(false)
29 @Column(DataType.ENUM(values(FOLLOW_STATES)))
30 state: FollowState
31
32 @CreatedAt
33 createdAt: Date
34
35 @UpdatedAt
36 updatedAt: Date
37
38 @ForeignKey(() => ActorModel)
39 @Column
40 actorId: number
41
42 @BelongsTo(() => ActorModel, {
43 foreignKey: {
44 name: 'actorId',
45 allowNull: false
46 },
47 as: 'ActorFollower',
48 onDelete: 'CASCADE'
49 })
50 ActorFollower: ActorModel
51
52 @ForeignKey(() => ActorModel)
53 @Column
54 targetActorId: number
55
56 @BelongsTo(() => ActorModel, {
57 foreignKey: {
58 name: 'targetActorId',
59 allowNull: false
60 },
61 as: 'ActorFollowing',
62 onDelete: 'CASCADE'
63 })
64 ActorFollowing: ActorModel
65
66 static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Sequelize.Transaction) {
67 const query = {
68 where: {
69 actorId,
70 targetActorId: targetActorId
71 },
72 include: [
73 {
74 model: ActorModel,
75 required: true,
76 as: 'ActorFollower'
77 },
78 {
79 model: ActorModel,
80 required: true,
81 as: 'ActorFollowing'
82 }
83 ],
84 transaction: t
85 }
86
87 return ActorFollowModel.findOne(query)
88 }
89
90 static loadByActorAndTargetHost (actorId: number, targetHost: string, t?: Sequelize.Transaction) {
91 const query = {
92 where: {
93 actorId
94 },
95 include: [
96 {
97 model: ActorModel,
98 required: true,
99 as: 'ActorFollower'
100 },
101 {
102 model: ActorModel,
103 required: true,
104 as: 'ActorFollowing',
105 include: [
106 {
107 model: ServerModel,
108 required: true,
109 where: {
110 host: targetHost
111 }
112 }
113 ]
114 }
115 ],
116 transaction: t
117 }
118
119 return ActorFollowModel.findOne(query)
120 }
121
122 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
123 const query = {
124 distinct: true,
125 offset: start,
126 limit: count,
127 order: [ getSort(sort) ],
128 include: [
129 {
130 model: ActorModel,
131 required: true,
132 as: 'ActorFollower',
133 where: {
134 id
135 }
136 },
137 {
138 model: ActorModel,
139 as: 'ActorFollowing',
140 required: true,
141 include: [ ServerModel ]
142 }
143 ]
144 }
145
146 return ActorFollowModel.findAndCountAll(query)
147 .then(({ rows, count }) => {
148 return {
149 data: rows,
150 total: count
151 }
152 })
153 }
154
155 static listFollowersForApi (id: number, start: number, count: number, sort: string) {
156 const query = {
157 distinct: true,
158 offset: start,
159 limit: count,
160 order: [ getSort(sort) ],
161 include: [
162 {
163 model: ActorModel,
164 required: true,
165 as: 'ActorFollower',
166 include: [ ServerModel ]
167 },
168 {
169 model: ActorModel,
170 as: 'ActorFollowing',
171 required: true,
172 where: {
173 id
174 }
175 }
176 ]
177 }
178
179 return ActorFollowModel.findAndCountAll(query)
180 .then(({ rows, count }) => {
181 return {
182 data: rows,
183 total: count
184 }
185 })
186 }
187
188 static listAcceptedFollowerUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
189 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, start, count)
190 }
191
192 static listAcceptedFollowerSharedInboxUrls (actorIds: number[], t: Sequelize.Transaction) {
193 return ActorFollowModel.createListAcceptedFollowForApiQuery('followers', actorIds, t, undefined, undefined, 'sharedInboxUrl')
194 }
195
196 static listAcceptedFollowingUrlsForApi (actorIds: number[], t: Sequelize.Transaction, start?: number, count?: number) {
197 return ActorFollowModel.createListAcceptedFollowForApiQuery('following', actorIds, t, start, count)
198 }
199
200 private static async createListAcceptedFollowForApiQuery (type: 'followers' | 'following',
201 actorIds: number[],
202 t: Sequelize.Transaction,
203 start?: number,
204 count?: number,
205 columnUrl = 'url') {
206 let firstJoin: string
207 let secondJoin: string
208
209 if (type === 'followers') {
210 firstJoin = 'targetActorId'
211 secondJoin = 'actorId'
212 } else {
213 firstJoin = 'actorId'
214 secondJoin = 'targetActorId'
215 }
216
217 const selections = [ '"Follows"."' + columnUrl + '" AS "url"', 'COUNT(*) AS "total"' ]
218 const tasks: Bluebird<any>[] = []
219
220 for (const selection of selections) {
221 let query = 'SELECT ' + selection + ' FROM "actor" ' +
222 'INNER JOIN "actorFollow" ON "actorFollow"."' + firstJoin + '" = "actor"."id" ' +
223 'INNER JOIN "actor" AS "Follows" ON "actorFollow"."' + secondJoin + '" = "Follows"."id" ' +
224 'WHERE "actor"."id" = ANY ($actorIds) AND "actorFollow"."state" = \'accepted\' '
225
226 if (count !== undefined) query += 'LIMIT ' + count
227 if (start !== undefined) query += ' OFFSET ' + start
228
229 const options = {
230 bind: { actorIds },
231 type: Sequelize.QueryTypes.SELECT,
232 transaction: t
233 }
234 tasks.push(ActorFollowModel.sequelize.query(query, options))
235 }
236
237 const [ followers, [ { total } ] ] = await
238 Promise.all(tasks)
239 const urls: string[] = followers.map(f => f.url)
240
241 return {
242 data: urls,
243 total: parseInt(total, 10)
244 }
245 }
246
247 toFormattedJSON () {
248 const follower = this.ActorFollower.toFormattedJSON()
249 const following = this.ActorFollowing.toFormattedJSON()
250
251 return {
252 id: this.id,
253 follower,
254 following,
255 state: this.state,
256 createdAt: this.createdAt,
257 updatedAt: this.updatedAt
258 }
259 }
260}
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 4cae6a6ec..ecaa43dcf 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -1,38 +1,83 @@
1import { values } from 'lodash'
1import { join } from 'path' 2import { join } from 'path'
2import * as Sequelize from 'sequelize' 3import * as Sequelize from 'sequelize'
3import { 4import {
4 AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, HasMany, Is, IsUUID, Model, Table, 5 AllowNull,
6 BelongsTo,
7 Column,
8 CreatedAt,
9 DataType,
10 Default,
11 ForeignKey,
12 HasMany,
13 HasOne,
14 Is,
15 IsUUID,
16 Model,
17 Scopes,
18 Table,
5 UpdatedAt 19 UpdatedAt
6} from 'sequelize-typescript' 20} from 'sequelize-typescript'
21import { ActivityPubActorType } from '../../../shared/models/activitypub'
7import { Avatar } from '../../../shared/models/avatars/avatar.model' 22import { Avatar } from '../../../shared/models/avatars/avatar.model'
8import { activityPubContextify } from '../../helpers' 23import { activityPubContextify } from '../../helpers'
9import { 24import {
10 isActivityPubUrlValid, 25 isActivityPubUrlValid,
11 isActorFollowersCountValid, 26 isActorFollowersCountValid,
12 isActorFollowingCountValid, isActorPreferredUsernameValid, 27 isActorFollowingCountValid,
28 isActorNameValid,
13 isActorPrivateKeyValid, 29 isActorPrivateKeyValid,
14 isActorPublicKeyValid 30 isActorPublicKeyValid
15} from '../../helpers/custom-validators/activitypub' 31} from '../../helpers/custom-validators/activitypub'
16import { isUserUsernameValid } from '../../helpers/custom-validators/users' 32import { ACTIVITY_PUB_ACTOR_TYPES, AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers'
17import { AVATARS_DIR, CONFIG, CONSTRAINTS_FIELDS } from '../../initializers' 33import { AccountModel } from '../account/account'
18import { AccountFollowModel } from '../account/account-follow'
19import { AvatarModel } from '../avatar/avatar' 34import { AvatarModel } from '../avatar/avatar'
20import { ServerModel } from '../server/server' 35import { ServerModel } from '../server/server'
21import { throwIfNotValid } from '../utils' 36import { throwIfNotValid } from '../utils'
37import { VideoChannelModel } from '../video/video-channel'
38import { ActorFollowModel } from './actor-follow'
22 39
40enum ScopeNames {
41 FULL = 'FULL'
42}
43
44@Scopes({
45 [ScopeNames.FULL]: {
46 include: [
47 {
48 model: () => AccountModel,
49 required: false
50 },
51 {
52 model: () => VideoChannelModel,
53 required: false
54 }
55 ]
56 }
57})
23@Table({ 58@Table({
24 tableName: 'actor' 59 tableName: 'actor',
60 indexes: [
61 {
62 fields: [ 'name', 'serverId' ],
63 unique: true
64 }
65 ]
25}) 66})
26export class ActorModel extends Model<ActorModel> { 67export class ActorModel extends Model<ActorModel> {
27 68
28 @AllowNull(false) 69 @AllowNull(false)
70 @Column(DataType.ENUM(values(ACTIVITY_PUB_ACTOR_TYPES)))
71 type: ActivityPubActorType
72
73 @AllowNull(false)
29 @Default(DataType.UUIDV4) 74 @Default(DataType.UUIDV4)
30 @IsUUID(4) 75 @IsUUID(4)
31 @Column(DataType.UUID) 76 @Column(DataType.UUID)
32 uuid: string 77 uuid: string
33 78
34 @AllowNull(false) 79 @AllowNull(false)
35 @Is('ActorName', value => throwIfNotValid(value, isActorPreferredUsernameValid, 'actor name')) 80 @Is('ActorName', value => throwIfNotValid(value, isActorNameValid, 'actor name'))
36 @Column 81 @Column
37 name: string 82 name: string
38 83
@@ -104,24 +149,24 @@ export class ActorModel extends Model<ActorModel> {
104 }) 149 })
105 Avatar: AvatarModel 150 Avatar: AvatarModel
106 151
107 @HasMany(() => AccountFollowModel, { 152 @HasMany(() => ActorFollowModel, {
108 foreignKey: { 153 foreignKey: {
109 name: 'accountId', 154 name: 'actorId',
110 allowNull: false 155 allowNull: false
111 }, 156 },
112 onDelete: 'cascade' 157 onDelete: 'cascade'
113 }) 158 })
114 AccountFollowing: AccountFollowModel[] 159 AccountFollowing: ActorFollowModel[]
115 160
116 @HasMany(() => AccountFollowModel, { 161 @HasMany(() => ActorFollowModel, {
117 foreignKey: { 162 foreignKey: {
118 name: 'targetAccountId', 163 name: 'targetActorId',
119 allowNull: false 164 allowNull: false
120 }, 165 },
121 as: 'followers', 166 as: 'followers',
122 onDelete: 'cascade' 167 onDelete: 'cascade'
123 }) 168 })
124 AccountFollowers: AccountFollowModel[] 169 AccountFollowers: ActorFollowModel[]
125 170
126 @ForeignKey(() => ServerModel) 171 @ForeignKey(() => ServerModel)
127 @Column 172 @Column
@@ -135,6 +180,36 @@ export class ActorModel extends Model<ActorModel> {
135 }) 180 })
136 Server: ServerModel 181 Server: ServerModel
137 182
183 @HasOne(() => AccountModel, {
184 foreignKey: {
185 allowNull: true
186 },
187 onDelete: 'cascade'
188 })
189 Account: AccountModel
190
191 @HasOne(() => VideoChannelModel, {
192 foreignKey: {
193 allowNull: true
194 },
195 onDelete: 'cascade'
196 })
197 VideoChannel: VideoChannelModel
198
199 static load (id: number) {
200 return ActorModel.scope(ScopeNames.FULL).findById(id)
201 }
202
203 static loadByUUID (uuid: string) {
204 const query = {
205 where: {
206 uuid
207 }
208 }
209
210 return ActorModel.scope(ScopeNames.FULL).findOne(query)
211 }
212
138 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) { 213 static listByFollowersUrls (followersUrls: string[], transaction?: Sequelize.Transaction) {
139 const query = { 214 const query = {
140 where: { 215 where: {
@@ -145,7 +220,48 @@ export class ActorModel extends Model<ActorModel> {
145 transaction 220 transaction
146 } 221 }
147 222
148 return ActorModel.findAll(query) 223 return ActorModel.scope(ScopeNames.FULL).findAll(query)
224 }
225
226 static loadLocalByName (name: string) {
227 const query = {
228 where: {
229 name,
230 serverId: null
231 }
232 }
233
234 return ActorModel.scope(ScopeNames.FULL).findOne(query)
235 }
236
237 static loadByNameAndHost (name: string, host: string) {
238 const query = {
239 where: {
240 name
241 },
242 include: [
243 {
244 model: ServerModel,
245 required: true,
246 where: {
247 host
248 }
249 }
250 ]
251 }
252
253 return ActorModel.scope(ScopeNames.FULL).findOne(query)
254 }
255
256 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
257 const query = {
258 where: {
259 url
260 },
261 transaction
262 }
263
264 return ActorModel.scope(ScopeNames.FULL).findOne(query)
149 } 265 }
150 266
151 toFormattedJSON () { 267 toFormattedJSON () {
@@ -167,6 +283,7 @@ export class ActorModel extends Model<ActorModel> {
167 283
168 return { 284 return {
169 id: this.id, 285 id: this.id,
286 uuid: this.uuid,
170 host, 287 host,
171 score, 288 score,
172 followingCount: this.followingCount, 289 followingCount: this.followingCount,
@@ -175,28 +292,30 @@ export class ActorModel extends Model<ActorModel> {
175 } 292 }
176 } 293 }
177 294
178 toActivityPubObject (name: string, uuid: string, type: 'Account' | 'VideoChannel') { 295 toActivityPubObject (preferredUsername: string, type: 'Account' | 'Application' | 'VideoChannel') {
179 let activityPubType 296 let activityPubType
180 if (type === 'Account') { 297 if (type === 'Account') {
181 activityPubType = this.serverId ? 'Application' as 'Application' : 'Person' as 'Person' 298 activityPubType = 'Person' as 'Person'
299 } else if (type === 'Application') {
300 activityPubType = 'Application' as 'Application'
182 } else { // VideoChannel 301 } else { // VideoChannel
183 activityPubType = 'Group' 302 activityPubType = 'Group' as 'Group'
184 } 303 }
185 304
186 const json = { 305 const json = {
187 type, 306 type: activityPubType,
188 id: this.url, 307 id: this.url,
189 following: this.getFollowingUrl(), 308 following: this.getFollowingUrl(),
190 followers: this.getFollowersUrl(), 309 followers: this.getFollowersUrl(),
191 inbox: this.inboxUrl, 310 inbox: this.inboxUrl,
192 outbox: this.outboxUrl, 311 outbox: this.outboxUrl,
193 preferredUsername: name, 312 preferredUsername,
194 url: this.url, 313 url: this.url,
195 name, 314 name: this.name,
196 endpoints: { 315 endpoints: {
197 sharedInbox: this.sharedInboxUrl 316 sharedInbox: this.sharedInboxUrl
198 }, 317 },
199 uuid, 318 uuid: this.uuid,
200 publicKey: { 319 publicKey: {
201 id: this.getPublicKeyUrl(), 320 id: this.getPublicKeyUrl(),
202 owner: this.url, 321 owner: this.url,
@@ -212,11 +331,11 @@ export class ActorModel extends Model<ActorModel> {
212 attributes: [ 'sharedInboxUrl' ], 331 attributes: [ 'sharedInboxUrl' ],
213 include: [ 332 include: [
214 { 333 {
215 model: AccountFollowModel, 334 model: ActorFollowModel,
216 required: true, 335 required: true,
217 as: 'followers', 336 as: 'followers',
218 where: { 337 where: {
219 targetAccountId: this.id 338 targetActorId: this.id
220 } 339 }
221 } 340 }
222 ], 341 ],
diff --git a/server/models/application/application.ts b/server/models/application/application.ts
index 9fc07e850..854a5fb36 100644
--- a/server/models/application/application.ts
+++ b/server/models/application/application.ts
@@ -1,5 +1,14 @@
1import { AllowNull, Column, Default, IsInt, Model, Table } from 'sequelize-typescript' 1import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript'
2import { AccountModel } from '../account/account'
2 3
4@DefaultScope({
5 include: [
6 {
7 model: () => AccountModel,
8 required: true
9 }
10 ]
11})
3@Table({ 12@Table({
4 tableName: 'application' 13 tableName: 'application'
5}) 14})
@@ -11,7 +20,19 @@ export class ApplicationModel extends Model<ApplicationModel> {
11 @Column 20 @Column
12 migrationVersion: number 21 migrationVersion: number
13 22
23 @HasOne(() => AccountModel, {
24 foreignKey: {
25 allowNull: true
26 },
27 onDelete: 'cascade'
28 })
29 Account: AccountModel
30
14 static countTotal () { 31 static countTotal () {
15 return ApplicationModel.count() 32 return ApplicationModel.count()
16 } 33 }
34
35 static load () {
36 return ApplicationModel.findOne()
37 }
17} 38}
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index d0ee969fb..182971c4e 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -3,7 +3,6 @@ import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
3import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos' 3import { isVideoAbuseReasonValid } from '../../helpers/custom-validators/videos'
4import { CONFIG } from '../../initializers' 4import { CONFIG } from '../../initializers'
5import { AccountModel } from '../account/account' 5import { AccountModel } from '../account/account'
6import { ServerModel } from '../server/server'
7import { getSort, throwIfNotValid } from '../utils' 6import { getSort, throwIfNotValid } from '../utils'
8import { VideoModel } from './video' 7import { VideoModel } from './video'
9 8
@@ -63,13 +62,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
63 include: [ 62 include: [
64 { 63 {
65 model: AccountModel, 64 model: AccountModel,
66 required: true, 65 required: true
67 include: [
68 {
69 model: ServerModel,
70 required: false
71 }
72 ]
73 }, 66 },
74 { 67 {
75 model: VideoModel, 68 model: VideoModel,
@@ -87,8 +80,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
87 toFormattedJSON () { 80 toFormattedJSON () {
88 let reporterServerHost 81 let reporterServerHost
89 82
90 if (this.Account.Server) { 83 if (this.Account.Actor.Server) {
91 reporterServerHost = this.Account.Server.host 84 reporterServerHost = this.Account.Actor.Server.host
92 } else { 85 } else {
93 // It means it's our video 86 // It means it's our video
94 reporterServerHost = CONFIG.WEBSERVER.HOST 87 reporterServerHost = CONFIG.WEBSERVER.HOST
diff --git a/server/models/video/video-channel-share.ts b/server/models/video/video-channel-share.ts
deleted file mode 100644
index f5b7a7cd5..000000000
--- a/server/models/video/video-channel-share.ts
+++ /dev/null
@@ -1,96 +0,0 @@
1import * as Sequelize from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { AccountModel } from '../account/account'
4import { VideoChannelModel } from './video-channel'
5
6enum ScopeNames {
7 FULL = 'FULL',
8 WITH_ACCOUNT = 'WITH_ACCOUNT'
9}
10
11@Scopes({
12 [ScopeNames.FULL]: {
13 include: [
14 {
15 model: () => AccountModel,
16 required: true
17 },
18 {
19 model: () => VideoChannelModel,
20 required: true
21 }
22 ]
23 },
24 [ScopeNames.WITH_ACCOUNT]: {
25 include: [
26 {
27 model: () => AccountModel,
28 required: true
29 }
30 ]
31 }
32})
33@Table({
34 tableName: 'videoChannelShare',
35 indexes: [
36 {
37 fields: [ 'accountId' ]
38 },
39 {
40 fields: [ 'videoChannelId' ]
41 }
42 ]
43})
44export class VideoChannelShareModel extends Model<VideoChannelShareModel> {
45 @CreatedAt
46 createdAt: Date
47
48 @UpdatedAt
49 updatedAt: Date
50
51 @ForeignKey(() => AccountModel)
52 @Column
53 accountId: number
54
55 @BelongsTo(() => AccountModel, {
56 foreignKey: {
57 allowNull: false
58 },
59 onDelete: 'cascade'
60 })
61 Account: AccountModel
62
63 @ForeignKey(() => VideoChannelModel)
64 @Column
65 videoChannelId: number
66
67 @BelongsTo(() => VideoChannelModel, {
68 foreignKey: {
69 allowNull: false
70 },
71 onDelete: 'cascade'
72 })
73 VideoChannel: VideoChannelModel
74
75 static load (accountId: number, videoChannelId: number, t: Sequelize.Transaction) {
76 return VideoChannelShareModel.scope(ScopeNames.FULL).findOne({
77 where: {
78 accountId,
79 videoChannelId
80 },
81 transaction: t
82 })
83 }
84
85 static loadAccountsByShare (videoChannelId: number, t: Sequelize.Transaction) {
86 const query = {
87 where: {
88 videoChannelId
89 },
90 transaction: t
91 }
92
93 return VideoChannelShareModel.scope(ScopeNames.WITH_ACCOUNT).findAll(query)
94 .then(res => res.map(r => r.Account))
95 }
96}
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index fe44d3d53..acc2486b3 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -1,42 +1,52 @@
1import * as Sequelize from 'sequelize'
2import { 1import {
3 AfterDestroy, 2 AfterDestroy,
4 AllowNull, 3 AllowNull,
5 BelongsTo, 4 BelongsTo,
6 Column, 5 Column,
7 CreatedAt, 6 CreatedAt,
8 DataType, 7 DefaultScope,
9 Default,
10 ForeignKey, 8 ForeignKey,
11 HasMany, 9 HasMany,
12 Is, 10 Is,
13 IsUUID,
14 Model, 11 Model,
15 Scopes, 12 Scopes,
16 Table, 13 Table,
17 UpdatedAt 14 UpdatedAt
18} from 'sequelize-typescript' 15} from 'sequelize-typescript'
19import { IFindOptions } from 'sequelize-typescript/lib/interfaces/IFindOptions' 16import { ActivityPubActor } from '../../../shared/models/activitypub'
20import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels' 17import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../../helpers/custom-validators/video-channels'
21import { sendDeleteVideoChannel } from '../../lib/activitypub/send' 18import { sendDeleteActor } from '../../lib/activitypub/send'
22import { AccountModel } from '../account/account' 19import { AccountModel } from '../account/account'
23import { ActorModel } from '../activitypub/actor' 20import { ActorModel } from '../activitypub/actor'
24import { ServerModel } from '../server/server'
25import { getSort, throwIfNotValid } from '../utils' 21import { getSort, throwIfNotValid } from '../utils'
26import { VideoModel } from './video' 22import { VideoModel } from './video'
27import { VideoChannelShareModel } from './video-channel-share'
28 23
29enum ScopeNames { 24enum ScopeNames {
30 WITH_ACCOUNT = 'WITH_ACCOUNT', 25 WITH_ACCOUNT = 'WITH_ACCOUNT',
26 WITH_ACTOR = 'WITH_ACTOR',
31 WITH_VIDEOS = 'WITH_VIDEOS' 27 WITH_VIDEOS = 'WITH_VIDEOS'
32} 28}
33 29
30@DefaultScope({
31 include: [
32 {
33 model: () => ActorModel,
34 required: true
35 }
36 ]
37})
34@Scopes({ 38@Scopes({
35 [ScopeNames.WITH_ACCOUNT]: { 39 [ScopeNames.WITH_ACCOUNT]: {
36 include: [ 40 include: [
37 { 41 {
38 model: () => AccountModel, 42 model: () => AccountModel,
39 include: [ { model: () => ServerModel, required: false } ] 43 required: true,
44 include: [
45 {
46 model: () => ActorModel,
47 required: true
48 }
49 ]
40 } 50 }
41 ] 51 ]
42 }, 52 },
@@ -44,6 +54,11 @@ enum ScopeNames {
44 include: [ 54 include: [
45 () => VideoModel 55 () => VideoModel
46 ] 56 ]
57 },
58 [ScopeNames.WITH_ACTOR]: {
59 include: [
60 () => ActorModel
61 ]
47 } 62 }
48}) 63})
49@Table({ 64@Table({
@@ -57,12 +72,6 @@ enum ScopeNames {
57export class VideoChannelModel extends Model<VideoChannelModel> { 72export class VideoChannelModel extends Model<VideoChannelModel> {
58 73
59 @AllowNull(false) 74 @AllowNull(false)
60 @Default(DataType.UUIDV4)
61 @IsUUID(4)
62 @Column(DataType.UUID)
63 uuid: string
64
65 @AllowNull(false)
66 @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name')) 75 @Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
67 @Column 76 @Column
68 name: string 77 name: string
@@ -72,10 +81,6 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
72 @Column 81 @Column
73 description: string 82 description: string
74 83
75 @AllowNull(false)
76 @Column
77 remote: boolean
78
79 @CreatedAt 84 @CreatedAt
80 createdAt: Date 85 createdAt: Date
81 86
@@ -115,19 +120,10 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
115 }) 120 })
116 Videos: VideoModel[] 121 Videos: VideoModel[]
117 122
118 @HasMany(() => VideoChannelShareModel, {
119 foreignKey: {
120 name: 'channelId',
121 allowNull: false
122 },
123 onDelete: 'CASCADE'
124 })
125 VideoChannelShares: VideoChannelShareModel[]
126
127 @AfterDestroy 123 @AfterDestroy
128 static sendDeleteIfOwned (instance: VideoChannelModel) { 124 static sendDeleteIfOwned (instance: VideoChannelModel) {
129 if (instance.isOwned()) { 125 if (instance.Actor.isOwned()) {
130 return sendDeleteVideoChannel(instance, undefined) 126 return sendDeleteActor(instance.Actor, undefined)
131 } 127 }
132 128
133 return undefined 129 return undefined
@@ -150,7 +146,9 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
150 order: [ getSort(sort) ] 146 order: [ getSort(sort) ]
151 } 147 }
152 148
153 return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findAndCountAll(query) 149 return VideoChannelModel
150 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
151 .findAndCountAll(query)
154 .then(({ rows, count }) => { 152 .then(({ rows, count }) => {
155 return { total: count, data: rows } 153 return { total: count, data: rows }
156 }) 154 })
@@ -165,51 +163,18 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
165 where: { 163 where: {
166 id: accountId 164 id: accountId
167 }, 165 },
168 required: true, 166 required: true
169 include: [ { model: ServerModel, required: false } ]
170 } 167 }
171 ] 168 ]
172 } 169 }
173 170
174 return VideoChannelModel.findAndCountAll(query) 171 return VideoChannelModel
172 .findAndCountAll(query)
175 .then(({ rows, count }) => { 173 .then(({ rows, count }) => {
176 return { total: count, data: rows } 174 return { total: count, data: rows }
177 }) 175 })
178 } 176 }
179 177
180 static loadByUrl (url: string, t?: Sequelize.Transaction) {
181 const query: IFindOptions<VideoChannelModel> = {
182 include: [
183 {
184 model: ActorModel,
185 required: true,
186 where: {
187 url
188 }
189 }
190 ]
191 }
192
193 if (t !== undefined) query.transaction = t
194
195 return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query)
196 }
197
198 static loadByUUIDOrUrl (uuid: string, url: string, t?: Sequelize.Transaction) {
199 const query: IFindOptions<VideoChannelModel> = {
200 where: {
201 [ Sequelize.Op.or ]: [
202 { uuid },
203 { url }
204 ]
205 }
206 }
207
208 if (t !== undefined) query.transaction = t
209
210 return VideoChannelModel.findOne(query)
211 }
212
213 static loadByIdAndAccount (id: number, accountId: number) { 178 static loadByIdAndAccount (id: number, accountId: number) {
214 const options = { 179 const options = {
215 where: { 180 where: {
@@ -218,21 +183,33 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
218 } 183 }
219 } 184 }
220 185
221 return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findOne(options) 186 return VideoChannelModel
187 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
188 .findOne(options)
222 } 189 }
223 190
224 static loadAndPopulateAccount (id: number) { 191 static loadAndPopulateAccount (id: number) {
225 return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findById(id) 192 return VideoChannelModel
193 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
194 .findById(id)
226 } 195 }
227 196
228 static loadByUUIDAndPopulateAccount (uuid: string) { 197 static loadByUUIDAndPopulateAccount (uuid: string) {
229 const options = { 198 const options = {
230 where: { 199 include: [
231 uuid 200 {
232 } 201 model: ActorModel,
202 required: true,
203 where: {
204 uuid
205 }
206 }
207 ]
233 } 208 }
234 209
235 return VideoChannelModel.scope(ScopeNames.WITH_ACCOUNT).findOne(options) 210 return VideoChannelModel
211 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
212 .findOne(options)
236 } 213 }
237 214
238 static loadAndPopulateAccountAndVideos (id: number) { 215 static loadAndPopulateAccountAndVideos (id: number) {
@@ -242,39 +219,36 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
242 ] 219 ]
243 } 220 }
244 221
245 return VideoChannelModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ]).findById(id, options) 222 return VideoChannelModel
246 } 223 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
247 224 .findById(id, options)
248 isOwned () {
249 return this.remote === false
250 } 225 }
251 226
252 toFormattedJSON () { 227 toFormattedJSON () {
253 const json = { 228 const actor = this.Actor.toFormattedJSON()
229 const account = {
254 id: this.id, 230 id: this.id,
255 uuid: this.uuid,
256 name: this.name, 231 name: this.name,
257 description: this.description, 232 description: this.description,
258 isLocal: this.isOwned(), 233 isLocal: this.Actor.isOwned(),
259 createdAt: this.createdAt, 234 createdAt: this.createdAt,
260 updatedAt: this.updatedAt 235 updatedAt: this.updatedAt
261 } 236 }
262 237
263 if (this.Account !== undefined) { 238 return Object.assign(actor, account)
264 json[ 'owner' ] = {
265 name: this.Account.name,
266 uuid: this.Account.uuid
267 }
268 }
269
270 if (Array.isArray(this.Videos)) {
271 json[ 'videos' ] = this.Videos.map(v => v.toFormattedJSON())
272 }
273
274 return json
275 } 239 }
276 240
277 toActivityPubObject () { 241 toActivityPubObject (): ActivityPubActor {
278 return this.Actor.toActivityPubObject(this.name, this.uuid, 'VideoChannel') 242 const obj = this.Actor.toActivityPubObject(this.name, 'VideoChannel')
243
244 return Object.assign(obj, {
245 summary: this.description,
246 attributedTo: [
247 {
248 type: 'Person' as 'Person',
249 id: this.Account.Actor.url
250 }
251 ]
252 })
279 } 253 }
280} 254}
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts
index e1733b3a7..c252fd646 100644
--- a/server/models/video/video-share.ts
+++ b/server/models/video/video-share.ts
@@ -1,18 +1,18 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript' 2import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
3import { AccountModel } from '../account/account' 3import { ActorModel } from '../activitypub/actor'
4import { VideoModel } from './video' 4import { VideoModel } from './video'
5 5
6enum ScopeNames { 6enum ScopeNames {
7 FULL = 'FULL', 7 FULL = 'FULL',
8 WITH_ACCOUNT = 'WITH_ACCOUNT' 8 WITH_ACTOR = 'WITH_ACTOR'
9} 9}
10 10
11@Scopes({ 11@Scopes({
12 [ScopeNames.FULL]: { 12 [ScopeNames.FULL]: {
13 include: [ 13 include: [
14 { 14 {
15 model: () => AccountModel, 15 model: () => ActorModel,
16 required: true 16 required: true
17 }, 17 },
18 { 18 {
@@ -21,10 +21,10 @@ enum ScopeNames {
21 } 21 }
22 ] 22 ]
23 }, 23 },
24 [ScopeNames.WITH_ACCOUNT]: { 24 [ScopeNames.WITH_ACTOR]: {
25 include: [ 25 include: [
26 { 26 {
27 model: () => AccountModel, 27 model: () => ActorModel,
28 required: true 28 required: true
29 } 29 }
30 ] 30 ]
@@ -34,7 +34,7 @@ enum ScopeNames {
34 tableName: 'videoShare', 34 tableName: 'videoShare',
35 indexes: [ 35 indexes: [
36 { 36 {
37 fields: [ 'accountId' ] 37 fields: [ 'actorId' ]
38 }, 38 },
39 { 39 {
40 fields: [ 'videoId' ] 40 fields: [ 'videoId' ]
@@ -48,17 +48,17 @@ export class VideoShareModel extends Model<VideoShareModel> {
48 @UpdatedAt 48 @UpdatedAt
49 updatedAt: Date 49 updatedAt: Date
50 50
51 @ForeignKey(() => AccountModel) 51 @ForeignKey(() => ActorModel)
52 @Column 52 @Column
53 accountId: number 53 actorId: number
54 54
55 @BelongsTo(() => AccountModel, { 55 @BelongsTo(() => ActorModel, {
56 foreignKey: { 56 foreignKey: {
57 allowNull: false 57 allowNull: false
58 }, 58 },
59 onDelete: 'cascade' 59 onDelete: 'cascade'
60 }) 60 })
61 Account: AccountModel 61 Actor: ActorModel
62 62
63 @ForeignKey(() => VideoModel) 63 @ForeignKey(() => VideoModel)
64 @Column 64 @Column
@@ -72,24 +72,24 @@ export class VideoShareModel extends Model<VideoShareModel> {
72 }) 72 })
73 Video: VideoModel 73 Video: VideoModel
74 74
75 static load (accountId: number, videoId: number, t: Sequelize.Transaction) { 75 static load (actorId: number, videoId: number, t: Sequelize.Transaction) {
76 return VideoShareModel.scope(ScopeNames.WITH_ACCOUNT).findOne({ 76 return VideoShareModel.scope(ScopeNames.WITH_ACTOR).findOne({
77 where: { 77 where: {
78 accountId, 78 actorId,
79 videoId 79 videoId
80 }, 80 },
81 transaction: t 81 transaction: t
82 }) 82 })
83 } 83 }
84 84
85 static loadAccountsByShare (videoId: number, t: Sequelize.Transaction) { 85 static loadActorsByShare (videoId: number, t: Sequelize.Transaction) {
86 const query = { 86 const query = {
87 where: { 87 where: {
88 videoId 88 videoId
89 }, 89 },
90 include: [ 90 include: [
91 { 91 {
92 model: AccountModel, 92 model: ActorModel,
93 required: true 93 required: true
94 } 94 }
95 ], 95 ],
@@ -97,6 +97,6 @@ export class VideoShareModel extends Model<VideoShareModel> {
97 } 97 }
98 98
99 return VideoShareModel.scope(ScopeNames.FULL).findAll(query) 99 return VideoShareModel.scope(ScopeNames.FULL).findAll(query)
100 .then(res => res.map(r => r.Account)) 100 .then(res => res.map(r => r.Actor))
101 } 101 }
102} 102}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 1f940a50d..97fdbc8ef 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -66,9 +66,10 @@ import {
66 VIDEO_PRIVACIES 66 VIDEO_PRIVACIES
67} from '../../initializers' 67} from '../../initializers'
68import { getAnnounceActivityPubUrl } from '../../lib/activitypub' 68import { getAnnounceActivityPubUrl } from '../../lib/activitypub'
69import { sendDeleteVideo } from '../../lib/index' 69import { sendDeleteVideo } from '../../lib/activitypub/send'
70import { AccountModel } from '../account/account' 70import { AccountModel } from '../account/account'
71import { AccountVideoRateModel } from '../account/account-video-rate' 71import { AccountVideoRateModel } from '../account/account-video-rate'
72import { ActorModel } from '../activitypub/actor'
72import { ServerModel } from '../server/server' 73import { ServerModel } from '../server/server'
73import { getSort, throwIfNotValid } from '../utils' 74import { getSort, throwIfNotValid } from '../utils'
74import { TagModel } from './tag' 75import { TagModel } from './tag'
@@ -79,8 +80,7 @@ import { VideoShareModel } from './video-share'
79import { VideoTagModel } from './video-tag' 80import { VideoTagModel } from './video-tag'
80 81
81enum ScopeNames { 82enum ScopeNames {
82 NOT_IN_BLACKLIST = 'NOT_IN_BLACKLIST', 83 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
83 PUBLIC = 'PUBLIC',
84 WITH_ACCOUNT = 'WITH_ACCOUNT', 84 WITH_ACCOUNT = 'WITH_ACCOUNT',
85 WITH_TAGS = 'WITH_TAGS', 85 WITH_TAGS = 'WITH_TAGS',
86 WITH_FILES = 'WITH_FILES', 86 WITH_FILES = 'WITH_FILES',
@@ -89,17 +89,13 @@ enum ScopeNames {
89} 89}
90 90
91@Scopes({ 91@Scopes({
92 [ScopeNames.NOT_IN_BLACKLIST]: { 92 [ScopeNames.AVAILABLE_FOR_LIST]: {
93 where: { 93 where: {
94 id: { 94 id: {
95 [Sequelize.Op.notIn]: Sequelize.literal( 95 [Sequelize.Op.notIn]: Sequelize.literal(
96 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' 96 '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
97 ) 97 )
98 } 98 },
99 }
100 },
101 [ScopeNames.PUBLIC]: {
102 where: {
103 privacy: VideoPrivacy.PUBLIC 99 privacy: VideoPrivacy.PUBLIC
104 } 100 }
105 }, 101 },
@@ -114,8 +110,14 @@ enum ScopeNames {
114 required: true, 110 required: true,
115 include: [ 111 include: [
116 { 112 {
117 model: () => ServerModel, 113 model: () => ActorModel,
118 required: false 114 required: true,
115 include: [
116 {
117 model: () => ServerModel,
118 required: false
119 }
120 ]
119 } 121 }
120 ] 122 ]
121 } 123 }
@@ -138,7 +140,7 @@ enum ScopeNames {
138 include: [ 140 include: [
139 { 141 {
140 model: () => VideoShareModel, 142 model: () => VideoShareModel,
141 include: [ () => AccountModel ] 143 include: [ () => ActorModel ]
142 } 144 }
143 ] 145 ]
144 }, 146 },
@@ -271,7 +273,7 @@ export class VideoModel extends Model<VideoModel> {
271 273
272 @BelongsTo(() => VideoChannelModel, { 274 @BelongsTo(() => VideoChannelModel, {
273 foreignKey: { 275 foreignKey: {
274 allowNull: false 276 allowNull: true
275 }, 277 },
276 onDelete: 'cascade' 278 onDelete: 'cascade'
277 }) 279 })
@@ -351,14 +353,15 @@ export class VideoModel extends Model<VideoModel> {
351 return VideoModel.scope(ScopeNames.WITH_FILES).findAll() 353 return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
352 } 354 }
353 355
354 static listAllAndSharedByAccountForOutbox (accountId: number, start: number, count: number) { 356 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
355 function getRawQuery (select: string) { 357 function getRawQuery (select: string) {
356 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' + 358 const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
357 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' + 359 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
358 'WHERE "VideoChannel"."accountId" = ' + accountId 360 'INNER JOIN "account" AS "Account" ON "Account"."id" = "VideoChannel"."accountId" ' +
361 'WHERE "Account"."actorId" = ' + actorId
359 const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' + 362 const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
360 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' + 363 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
361 'WHERE "VideoShare"."accountId" = ' + accountId 364 'WHERE "VideoShare"."actorId" = ' + actorId
362 365
363 return `(${queryVideo}) UNION (${queryVideoShare})` 366 return `(${queryVideo}) UNION (${queryVideoShare})`
364 } 367 }
@@ -388,11 +391,16 @@ export class VideoModel extends Model<VideoModel> {
388 } 391 }
389 }, 392 },
390 { 393 {
391 accountId 394 actorId
392 } 395 }
393 ] 396 ]
394 }, 397 },
395 include: [ AccountModel ] 398 include: [
399 {
400 model: ActorModel,
401 required: true
402 }
403 ]
396 }, 404 },
397 { 405 {
398 model: VideoChannelModel, 406 model: VideoChannelModel,
@@ -469,7 +477,7 @@ export class VideoModel extends Model<VideoModel> {
469 order: [ getSort(sort) ] 477 order: [ getSort(sort) ]
470 } 478 }
471 479
472 return VideoModel.scope([ ScopeNames.NOT_IN_BLACKLIST, ScopeNames.PUBLIC, ScopeNames.WITH_ACCOUNT ]) 480 return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST, ScopeNames.WITH_ACCOUNT ])
473 .findAndCountAll(query) 481 .findAndCountAll(query)
474 .then(({ rows, count }) => { 482 .then(({ rows, count }) => {
475 return { 483 return {
@@ -541,7 +549,13 @@ export class VideoModel extends Model<VideoModel> {
541 549
542 const accountInclude: IIncludeOptions = { 550 const accountInclude: IIncludeOptions = {
543 model: AccountModel, 551 model: AccountModel,
544 include: [ serverInclude ] 552 include: [
553 {
554 model: ActorModel,
555 required: true,
556 include: [ serverInclude ]
557 }
558 ]
545 } 559 }
546 560
547 const videoChannelInclude: IIncludeOptions = { 561 const videoChannelInclude: IIncludeOptions = {
@@ -586,7 +600,7 @@ export class VideoModel extends Model<VideoModel> {
586 videoChannelInclude, tagInclude 600 videoChannelInclude, tagInclude
587 ] 601 ]
588 602
589 return VideoModel.scope([ ScopeNames.NOT_IN_BLACKLIST, ScopeNames.PUBLIC ]) 603 return VideoModel.scope([ ScopeNames.AVAILABLE_FOR_LIST ])
590 .findAndCountAll(query).then(({ rows, count }) => { 604 .findAndCountAll(query).then(({ rows, count }) => {
591 return { 605 return {
592 data: rows, 606 data: rows,
@@ -688,8 +702,8 @@ export class VideoModel extends Model<VideoModel> {
688 toFormattedJSON () { 702 toFormattedJSON () {
689 let serverHost 703 let serverHost
690 704
691 if (this.VideoChannel.Account.Server) { 705 if (this.VideoChannel.Account.Actor.Server) {
692 serverHost = this.VideoChannel.Account.Server.host 706 serverHost = this.VideoChannel.Account.Actor.Server.host
693 } else { 707 } else {
694 // It means it's our video 708 // It means it's our video
695 serverHost = CONFIG.WEBSERVER.HOST 709 serverHost = CONFIG.WEBSERVER.HOST
@@ -805,9 +819,9 @@ export class VideoModel extends Model<VideoModel> {
805 819
806 for (const rate of this.AccountVideoRates) { 820 for (const rate of this.AccountVideoRates) {
807 if (rate.type === 'like') { 821 if (rate.type === 'like') {
808 likes.push(rate.Account.url) 822 likes.push(rate.Account.Actor.url)
809 } else if (rate.type === 'dislike') { 823 } else if (rate.type === 'dislike') {
810 dislikes.push(rate.Account.url) 824 dislikes.push(rate.Account.Actor.url)
811 } 825 }
812 } 826 }
813 827
@@ -820,7 +834,7 @@ export class VideoModel extends Model<VideoModel> {
820 const shares: string[] = [] 834 const shares: string[] = []
821 835
822 for (const videoShare of this.VideoShares) { 836 for (const videoShare of this.VideoShares) {
823 const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Account) 837 const shareUrl = getAnnounceActivityPubUrl(this.url, videoShare.Actor)
824 shares.push(shareUrl) 838 shares.push(shareUrl)
825 } 839 }
826 840
@@ -886,7 +900,13 @@ export class VideoModel extends Model<VideoModel> {
886 url, 900 url,
887 likes: likesObject, 901 likes: likesObject,
888 dislikes: dislikesObject, 902 dislikes: dislikesObject,
889 shares: sharesObject 903 shares: sharesObject,
904 attributedTo: [
905 {
906 type: 'Group',
907 id: this.VideoChannel.Actor.url
908 }
909 ]
890 } 910 }
891 } 911 }
892 912
@@ -1030,8 +1050,8 @@ export class VideoModel extends Model<VideoModel> {
1030 baseUrlHttp = CONFIG.WEBSERVER.URL 1050 baseUrlHttp = CONFIG.WEBSERVER.URL
1031 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT 1051 baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
1032 } else { 1052 } else {
1033 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Server.host 1053 baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host
1034 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Server.host 1054 baseUrlWs = REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
1035 } 1055 }
1036 1056
1037 return { baseUrlHttp, baseUrlWs } 1057 return { baseUrlHttp, baseUrlWs }