aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
Diffstat (limited to 'server/models')
-rw-r--r--server/models/account/account.ts14
-rw-r--r--server/models/activitypub/actor-follow.ts103
-rw-r--r--server/models/activitypub/actor.ts10
-rw-r--r--server/models/video/video-channel.ts163
4 files changed, 242 insertions, 48 deletions
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 07539a04e..6bbfc6f4e 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -29,18 +29,8 @@ import { UserModel } from './user'
29@DefaultScope({ 29@DefaultScope({
30 include: [ 30 include: [
31 { 31 {
32 model: () => ActorModel, 32 model: () => ActorModel, // Default scope includes avatar and server
33 required: true, 33 required: true
34 include: [
35 {
36 model: () => ServerModel,
37 required: false
38 },
39 {
40 model: () => AvatarModel,
41 required: false
42 }
43 ]
44 } 34 }
45 ] 35 ]
46}) 36})
diff --git a/server/models/activitypub/actor-follow.ts b/server/models/activitypub/actor-follow.ts
index b2d7ace66..81fcf7001 100644
--- a/server/models/activitypub/actor-follow.ts
+++ b/server/models/activitypub/actor-follow.ts
@@ -26,7 +26,7 @@ import { ACTOR_FOLLOW_SCORE } from '../../initializers'
26import { FOLLOW_STATES } from '../../initializers/constants' 26import { FOLLOW_STATES } from '../../initializers/constants'
27import { ServerModel } from '../server/server' 27import { ServerModel } from '../server/server'
28import { getSort } from '../utils' 28import { getSort } from '../utils'
29import { ActorModel } from './actor' 29import { ActorModel, unusedActorAttributesForAPI } from './actor'
30import { VideoChannelModel } from '../video/video-channel' 30import { VideoChannelModel } from '../video/video-channel'
31import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions' 31import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions'
32import { AccountModel } from '../account/account' 32import { AccountModel } from '../account/account'
@@ -167,8 +167,11 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
167 return ActorFollowModel.findOne(query) 167 return ActorFollowModel.findOne(query)
168 } 168 }
169 169
170 static loadByActorAndTargetNameAndHost (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) { 170 static loadByActorAndTargetNameAndHostForAPI (actorId: number, targetName: string, targetHost: string, t?: Sequelize.Transaction) {
171 const actorFollowingPartInclude: IIncludeOptions = { 171 const actorFollowingPartInclude: IIncludeOptions = {
172 attributes: {
173 exclude: unusedActorAttributesForAPI
174 },
172 model: ActorModel, 175 model: ActorModel,
173 required: true, 176 required: true,
174 as: 'ActorFollowing', 177 as: 'ActorFollowing',
@@ -177,7 +180,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
177 }, 180 },
178 include: [ 181 include: [
179 { 182 {
180 model: VideoChannelModel, 183 model: VideoChannelModel.unscoped(),
181 required: false 184 required: false
182 } 185 }
183 ] 186 ]
@@ -200,17 +203,79 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
200 actorId 203 actorId
201 }, 204 },
202 include: [ 205 include: [
203 {
204 model: ActorModel,
205 required: true,
206 as: 'ActorFollower'
207 },
208 actorFollowingPartInclude 206 actorFollowingPartInclude
209 ], 207 ],
210 transaction: t 208 transaction: t
211 } 209 }
212 210
213 return ActorFollowModel.findOne(query) 211 return ActorFollowModel.findOne(query)
212 .then(result => {
213 if (result && result.ActorFollowing.VideoChannel) {
214 result.ActorFollowing.VideoChannel.Actor = result.ActorFollowing
215 }
216
217 return result
218 })
219 }
220
221 static listSubscribedIn (actorId: number, targets: { name: string, host?: string }[]) {
222 const whereTab = targets
223 .map(t => {
224 if (t.host) {
225 return {
226 [ Sequelize.Op.and ]: [
227 {
228 '$preferredUsername$': t.name
229 },
230 {
231 '$host$': t.host
232 }
233 ]
234 }
235 }
236
237 return {
238 [ Sequelize.Op.and ]: [
239 {
240 '$preferredUsername$': t.name
241 },
242 {
243 '$serverId$': null
244 }
245 ]
246 }
247 })
248
249 const query = {
250 attributes: [],
251 where: {
252 [ Sequelize.Op.and ]: [
253 {
254 [ Sequelize.Op.or ]: whereTab
255 },
256 {
257 actorId
258 }
259 ]
260 },
261 include: [
262 {
263 attributes: [ 'preferredUsername' ],
264 model: ActorModel.unscoped(),
265 required: true,
266 as: 'ActorFollowing',
267 include: [
268 {
269 attributes: [ 'host' ],
270 model: ServerModel.unscoped(),
271 required: false
272 }
273 ]
274 }
275 ]
276 }
277
278 return ActorFollowModel.findAll(query)
214 } 279 }
215 280
216 static listFollowingForApi (id: number, start: number, count: number, sort: string) { 281 static listFollowingForApi (id: number, start: number, count: number, sort: string) {
@@ -248,6 +313,7 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
248 313
249 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) { 314 static listSubscriptionsForApi (id: number, start: number, count: number, sort: string) {
250 const query = { 315 const query = {
316 attributes: [],
251 distinct: true, 317 distinct: true,
252 offset: start, 318 offset: start,
253 limit: count, 319 limit: count,
@@ -257,6 +323,9 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
257 }, 323 },
258 include: [ 324 include: [
259 { 325 {
326 attributes: {
327 exclude: unusedActorAttributesForAPI
328 },
260 model: ActorModel, 329 model: ActorModel,
261 as: 'ActorFollowing', 330 as: 'ActorFollowing',
262 required: true, 331 required: true,
@@ -266,8 +335,24 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
266 required: true, 335 required: true,
267 include: [ 336 include: [
268 { 337 {
269 model: AccountModel, 338 attributes: {
339 exclude: unusedActorAttributesForAPI
340 },
341 model: ActorModel,
270 required: true 342 required: true
343 },
344 {
345 model: AccountModel,
346 required: true,
347 include: [
348 {
349 attributes: {
350 exclude: unusedActorAttributesForAPI
351 },
352 model: ActorModel,
353 required: true
354 }
355 ]
271 } 356 }
272 ] 357 ]
273 } 358 }
diff --git a/server/models/activitypub/actor.ts b/server/models/activitypub/actor.ts
index 2abf40713..ec0b4b2d9 100644
--- a/server/models/activitypub/actor.ts
+++ b/server/models/activitypub/actor.ts
@@ -42,6 +42,16 @@ enum ScopeNames {
42 FULL = 'FULL' 42 FULL = 'FULL'
43} 43}
44 44
45export const unusedActorAttributesForAPI = [
46 'publicKey',
47 'privateKey',
48 'inboxUrl',
49 'outboxUrl',
50 'sharedInboxUrl',
51 'followersUrl',
52 'followingUrl'
53]
54
45@DefaultScope({ 55@DefaultScope({
46 include: [ 56 include: [
47 { 57 {
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 9f80e0b8d..7d717fc68 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -12,6 +12,7 @@ import {
12 Is, 12 Is,
13 Model, 13 Model,
14 Scopes, 14 Scopes,
15 Sequelize,
15 Table, 16 Table,
16 UpdatedAt 17 UpdatedAt
17} from 'sequelize-typescript' 18} from 'sequelize-typescript'
@@ -24,19 +25,36 @@ import {
24} from '../../helpers/custom-validators/video-channels' 25} from '../../helpers/custom-validators/video-channels'
25import { sendDeleteActor } from '../../lib/activitypub/send' 26import { sendDeleteActor } from '../../lib/activitypub/send'
26import { AccountModel } from '../account/account' 27import { AccountModel } from '../account/account'
27import { ActorModel } from '../activitypub/actor' 28import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
28import { getSort, throwIfNotValid } from '../utils' 29import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
29import { VideoModel } from './video' 30import { VideoModel } from './video'
30import { CONSTRAINTS_FIELDS } from '../../initializers' 31import { CONSTRAINTS_FIELDS } from '../../initializers'
31import { AvatarModel } from '../avatar/avatar'
32import { ServerModel } from '../server/server' 32import { ServerModel } from '../server/server'
33import { DefineIndexesOptions } from 'sequelize'
34
35// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
36const indexes: DefineIndexesOptions[] = [
37 buildTrigramSearchIndex('video_channel_name_trigram', 'name'),
38
39 {
40 fields: [ 'accountId' ]
41 },
42 {
43 fields: [ 'actorId' ]
44 }
45]
33 46
34enum ScopeNames { 47enum ScopeNames {
48 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
35 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
36 WITH_ACTOR = 'WITH_ACTOR', 50 WITH_ACTOR = 'WITH_ACTOR',
37 WITH_VIDEOS = 'WITH_VIDEOS' 51 WITH_VIDEOS = 'WITH_VIDEOS'
38} 52}
39 53
54type AvailableForListOptions = {
55 actorId: number
56}
57
40@DefaultScope({ 58@DefaultScope({
41 include: [ 59 include: [
42 { 60 {
@@ -46,23 +64,57 @@ enum ScopeNames {
46 ] 64 ]
47}) 65})
48@Scopes({ 66@Scopes({
49 [ScopeNames.WITH_ACCOUNT]: { 67 [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
50 include: [ 68 const actorIdNumber = parseInt(options.actorId + '', 10)
51 { 69
52 model: () => AccountModel.unscoped(), 70 // Only list local channels OR channels that are on an instance followed by actorId
53 required: true, 71 const inQueryInstanceFollow = '(' +
54 include: [ 72 'SELECT "actor"."serverId" FROM "actor" ' +
55 { 73 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = actor.id ' +
56 model: () => ActorModel.unscoped(), 74 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
57 required: true, 75 ')'
58 include: [ 76
77 return {
78 include: [
79 {
80 attributes: {
81 exclude: unusedActorAttributesForAPI
82 },
83 model: ActorModel,
84 where: {
85 [Sequelize.Op.or]: [
86 {
87 serverId: null
88 },
59 { 89 {
60 model: () => AvatarModel.unscoped(), 90 serverId: {
61 required: false 91 [ Sequelize.Op.in ]: Sequelize.literal(inQueryInstanceFollow)
92 }
62 } 93 }
63 ] 94 ]
64 } 95 }
65 ] 96 },
97 {
98 model: AccountModel,
99 required: true,
100 include: [
101 {
102 attributes: {
103 exclude: unusedActorAttributesForAPI
104 },
105 model: ActorModel, // Default scope includes avatar and server
106 required: true
107 }
108 ]
109 }
110 ]
111 }
112 },
113 [ScopeNames.WITH_ACCOUNT]: {
114 include: [
115 {
116 model: () => AccountModel,
117 required: true
66 } 118 }
67 ] 119 ]
68 }, 120 },
@@ -79,14 +131,7 @@ enum ScopeNames {
79}) 131})
80@Table({ 132@Table({
81 tableName: 'videoChannel', 133 tableName: 'videoChannel',
82 indexes: [ 134 indexes
83 {
84 fields: [ 'accountId' ]
85 },
86 {
87 fields: [ 'actorId' ]
88 }
89 ]
90}) 135})
91export class VideoChannelModel extends Model<VideoChannelModel> { 136export class VideoChannelModel extends Model<VideoChannelModel> {
92 137
@@ -170,15 +215,61 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
170 return VideoChannelModel.count(query) 215 return VideoChannelModel.count(query)
171 } 216 }
172 217
173 static listForApi (start: number, count: number, sort: string) { 218 static listForApi (actorId: number, start: number, count: number, sort: string) {
174 const query = { 219 const query = {
175 offset: start, 220 offset: start,
176 limit: count, 221 limit: count,
177 order: getSort(sort) 222 order: getSort(sort)
178 } 223 }
179 224
225 const scopes = {
226 method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId } as AvailableForListOptions ]
227 }
180 return VideoChannelModel 228 return VideoChannelModel
181 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 229 .scope(scopes)
230 .findAndCountAll(query)
231 .then(({ rows, count }) => {
232 return { total: count, data: rows }
233 })
234 }
235
236 static searchForApi (options: {
237 actorId: number
238 search: string
239 start: number
240 count: number
241 sort: string
242 }) {
243 const attributesInclude = []
244 const escapedSearch = VideoModel.sequelize.escape(options.search)
245 const escapedLikeSearch = VideoModel.sequelize.escape('%' + options.search + '%')
246 attributesInclude.push(createSimilarityAttribute('VideoChannelModel.name', options.search))
247
248 const query = {
249 attributes: {
250 include: attributesInclude
251 },
252 offset: options.start,
253 limit: options.count,
254 order: getSort(options.sort),
255 where: {
256 id: {
257 [ Sequelize.Op.in ]: Sequelize.literal(
258 '(' +
259 'SELECT id FROM "videoChannel" WHERE ' +
260 'lower(immutable_unaccent("name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
261 'lower(immutable_unaccent("name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
262 ')'
263 )
264 }
265 }
266 }
267
268 const scopes = {
269 method: [ ScopeNames.AVAILABLE_FOR_LIST, { actorId: options.actorId } as AvailableForListOptions ]
270 }
271 return VideoChannelModel
272 .scope(scopes)
182 .findAndCountAll(query) 273 .findAndCountAll(query)
183 .then(({ rows, count }) => { 274 .then(({ rows, count }) => {
184 return { total: count, data: rows } 275 return { total: count, data: rows }
@@ -239,7 +330,25 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
239 } 330 }
240 331
241 return VideoChannelModel 332 return VideoChannelModel
242 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 333 .scope([ ScopeNames.WITH_ACCOUNT ])
334 .findOne(query)
335 }
336
337 static loadByUrlAndPopulateAccount (url: string) {
338 const query = {
339 include: [
340 {
341 model: ActorModel,
342 required: true,
343 where: {
344 url
345 }
346 }
347 ]
348 }
349
350 return VideoChannelModel
351 .scope([ ScopeNames.WITH_ACCOUNT ])
243 .findOne(query) 352 .findOne(query)
244 } 353 }
245 354