allow limiting video-comments rss feeds to an account or video channel
[oweals/peertube.git] / server / models / utils.ts
1 import { Model, Sequelize } from 'sequelize-typescript'
2 import validator from 'validator'
3 import { Col } from 'sequelize/types/lib/utils'
4 import { literal, OrderItem, Op } from 'sequelize'
5
6 type SortType = { sortModel: string, sortValue: string }
7
8 // Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ]
9 function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
10   const { direction, field } = buildDirectionAndField(value)
11
12   let finalField: string | Col
13
14   if (field.toLowerCase() === 'match') { // Search
15     finalField = Sequelize.col('similarity')
16   } else if (field === 'videoQuotaUsed') { // Users list
17     finalField = Sequelize.col('videoQuotaUsed')
18   } else {
19     finalField = field
20   }
21
22   return [ [ finalField, direction ], lastSort ]
23 }
24
25 function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
26   const { direction, field } = buildDirectionAndField(value)
27
28   if (field === 'totalReplies') {
29     return [
30       [ Sequelize.literal('"totalReplies"'), direction ],
31       lastSort
32     ]
33   }
34
35   return getSort(value, lastSort)
36 }
37
38 function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
39   const { direction, field } = buildDirectionAndField(value)
40
41   if (field.toLowerCase() === 'trending') { // Sort by aggregation
42     return [
43       [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ],
44
45       [ Sequelize.col('VideoModel.views'), direction ],
46
47       lastSort
48     ]
49   }
50
51   let finalField: string | Col
52
53   // Alias
54   if (field.toLowerCase() === 'match') { // Search
55     finalField = Sequelize.col('similarity')
56   } else {
57     finalField = field
58   }
59
60   const firstSort = typeof finalField === 'string'
61     ? finalField.split('.').concat([ direction ]) as any // FIXME: sequelize typings
62     : [ finalField, direction ]
63
64   return [ firstSort, lastSort ]
65 }
66
67 function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
68   const [ firstSort ] = getSort(value)
69
70   if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as any[] // FIXME: typings
71   return [ firstSort, lastSort ]
72 }
73
74 function getFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
75   const { direction, field } = buildDirectionAndField(value)
76
77   if (field === 'redundancyAllowed') {
78     return [
79       [ 'ActorFollowing', 'Server', 'redundancyAllowed', direction ],
80       lastSort
81     ]
82   }
83
84   return getSort(value, lastSort)
85 }
86
87 function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
88   const now = Date.now()
89   const createdAtTime = model.createdAt.getTime()
90   const updatedAtTime = model.updatedAt.getTime()
91
92   return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval
93 }
94
95 function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) {
96   if (nullable && (value === null || value === undefined)) return
97
98   if (validator(value) === false) {
99     throw new Error(`"${value}" is not a valid ${fieldName}.`)
100   }
101 }
102
103 function buildTrigramSearchIndex (indexName: string, attribute: string) {
104   return {
105     name: indexName,
106     fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + '))') as any ],
107     using: 'gin',
108     operator: 'gin_trgm_ops'
109   }
110 }
111
112 function createSimilarityAttribute (col: string, value: string) {
113   return Sequelize.fn(
114     'similarity',
115
116     searchTrigramNormalizeCol(col),
117
118     searchTrigramNormalizeValue(value)
119   )
120 }
121
122 function buildBlockedAccountSQL (blockerIds: number[]) {
123   const blockerIdsString = blockerIds.join(', ')
124
125   return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
126     ' UNION ALL ' +
127     'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
128     'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
129     'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
130 }
131
132 function buildServerIdsFollowedBy (actorId: any) {
133   const actorIdNumber = parseInt(actorId + '', 10)
134
135   return '(' +
136     'SELECT "actor"."serverId" FROM "actorFollow" ' +
137     'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' +
138     'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
139     ')'
140 }
141
142 function buildWhereIdOrUUID (id: number | string) {
143   return validator.isInt('' + id) ? { id } : { uuid: id }
144 }
145
146 function parseAggregateResult (result: any) {
147   if (!result) return 0
148
149   const total = parseInt(result + '', 10)
150   if (isNaN(total)) return 0
151
152   return total
153 }
154
155 const createSafeIn = (model: typeof Model, stringArr: (string | number)[]) => {
156   return stringArr.map(t => {
157     return t === null
158       ? null
159       : model.sequelize.escape('' + t)
160   }).join(', ')
161 }
162
163 function buildLocalAccountIdsIn () {
164   return literal(
165     '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)'
166   )
167 }
168
169 function buildLocalActorIdsIn () {
170   return literal(
171     '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)'
172   )
173 }
174
175 function buildDirectionAndField (value: string) {
176   let field: string
177   let direction: 'ASC' | 'DESC'
178
179   if (value.substring(0, 1) === '-') {
180     direction = 'DESC'
181     field = value.substring(1)
182   } else {
183     direction = 'ASC'
184     field = value
185   }
186
187   return { direction, field }
188 }
189
190 function searchAttribute (sourceField?: string, targetField?: string) {
191   if (!sourceField) return {}
192
193   return {
194     [targetField]: {
195       [Op.iLike]: `%${sourceField}%`
196     }
197   }
198 }
199
200 // ---------------------------------------------------------------------------
201
202 export {
203   buildBlockedAccountSQL,
204   buildLocalActorIdsIn,
205   SortType,
206   buildLocalAccountIdsIn,
207   getSort,
208   getCommentSort,
209   getVideoSort,
210   getBlacklistSort,
211   createSimilarityAttribute,
212   throwIfNotValid,
213   buildServerIdsFollowedBy,
214   buildTrigramSearchIndex,
215   buildWhereIdOrUUID,
216   isOutdated,
217   parseAggregateResult,
218   getFollowsSort,
219   buildDirectionAndField,
220   createSafeIn,
221   searchAttribute
222 }
223
224 // ---------------------------------------------------------------------------
225
226 function searchTrigramNormalizeValue (value: string) {
227   return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value))
228 }
229
230 function searchTrigramNormalizeCol (col: string) {
231   return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col)))
232 }