diff options
author | Chocobozzz <me@florianbigard.com> | 2018-08-23 17:58:39 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2018-08-27 09:41:54 +0200 |
commit | f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9 (patch) | |
tree | 2050443febcdb2a3eec68b7bbf9687e26dcb24dc /server/models | |
parent | 240085d0056fd97ac3c7fa8fa4ce9bc32afc4d6e (diff) | |
download | PeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.tar.gz PeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.tar.zst PeerTube-f37dc0dd14d9ce0b59c454c2c1b935fcbe9727e9.zip |
Add ability to search video channels
Diffstat (limited to 'server/models')
-rw-r--r-- | server/models/account/account.ts | 14 | ||||
-rw-r--r-- | server/models/activitypub/actor-follow.ts | 103 | ||||
-rw-r--r-- | server/models/activitypub/actor.ts | 10 | ||||
-rw-r--r-- | server/models/video/video-channel.ts | 163 |
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' | |||
26 | import { FOLLOW_STATES } from '../../initializers/constants' | 26 | import { FOLLOW_STATES } from '../../initializers/constants' |
27 | import { ServerModel } from '../server/server' | 27 | import { ServerModel } from '../server/server' |
28 | import { getSort } from '../utils' | 28 | import { getSort } from '../utils' |
29 | import { ActorModel } from './actor' | 29 | import { ActorModel, unusedActorAttributesForAPI } from './actor' |
30 | import { VideoChannelModel } from '../video/video-channel' | 30 | import { VideoChannelModel } from '../video/video-channel' |
31 | import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions' | 31 | import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions' |
32 | import { AccountModel } from '../account/account' | 32 | import { 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 | ||
45 | export 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' |
25 | import { sendDeleteActor } from '../../lib/activitypub/send' | 26 | import { sendDeleteActor } from '../../lib/activitypub/send' |
26 | import { AccountModel } from '../account/account' | 27 | import { AccountModel } from '../account/account' |
27 | import { ActorModel } from '../activitypub/actor' | 28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
28 | import { getSort, throwIfNotValid } from '../utils' | 29 | import { buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
29 | import { VideoModel } from './video' | 30 | import { VideoModel } from './video' |
30 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 31 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
31 | import { AvatarModel } from '../avatar/avatar' | ||
32 | import { ServerModel } from '../server/server' | 32 | import { ServerModel } from '../server/server' |
33 | import { 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 | ||
36 | const indexes: DefineIndexesOptions[] = [ | ||
37 | buildTrigramSearchIndex('video_channel_name_trigram', 'name'), | ||
38 | |||
39 | { | ||
40 | fields: [ 'accountId' ] | ||
41 | }, | ||
42 | { | ||
43 | fields: [ 'actorId' ] | ||
44 | } | ||
45 | ] | ||
33 | 46 | ||
34 | enum ScopeNames { | 47 | enum 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 | ||
54 | type 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 | }) |
91 | export class VideoChannelModel extends Model<VideoChannelModel> { | 136 | export 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 | ||