aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/video-abuse.ts479
-rw-r--r--server/models/video/video.ts84
2 files changed, 42 insertions, 521 deletions
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
deleted file mode 100644
index 1319332f0..000000000
--- a/server/models/video/video-abuse.ts
+++ /dev/null
@@ -1,479 +0,0 @@
1import * as Bluebird from 'bluebird'
2import { literal, Op } from 'sequelize'
3import {
4 AllowNull,
5 BelongsTo,
6 Column,
7 CreatedAt,
8 DataType,
9 Default,
10 ForeignKey,
11 Is,
12 Model,
13 Scopes,
14 Table,
15 UpdatedAt
16} from 'sequelize-typescript'
17import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
18import {
19 VideoAbuseState,
20 VideoDetails,
21 VideoAbusePredefinedReasons,
22 VideoAbusePredefinedReasonsString,
23 videoAbusePredefinedReasonsMap
24} from '../../../shared'
25import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
26import { VideoAbuse } from '../../../shared/models/videos'
27import {
28 isVideoAbuseModerationCommentValid,
29 isVideoAbuseReasonValid,
30 isVideoAbuseStateValid
31} from '../../helpers/custom-validators/video-abuses'
32import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
33import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models'
34import { AccountModel } from '../account/account'
35import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
36import { ThumbnailModel } from './thumbnail'
37import { VideoModel } from './video'
38import { VideoBlacklistModel } from './video-blacklist'
39import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
40import { invert } from 'lodash'
41
42export enum ScopeNames {
43 FOR_API = 'FOR_API'
44}
45
46@Scopes(() => ({
47 [ScopeNames.FOR_API]: (options: {
48 // search
49 search?: string
50 searchReporter?: string
51 searchReportee?: string
52 searchVideo?: string
53 searchVideoChannel?: string
54
55 // filters
56 id?: number
57 predefinedReasonId?: number
58
59 state?: VideoAbuseState
60 videoIs?: VideoAbuseVideoIs
61
62 // accountIds
63 serverAccountId: number
64 userAccountId: number
65 }) => {
66 const where = {
67 reporterAccountId: {
68 [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
69 }
70 }
71
72 if (options.search) {
73 Object.assign(where, {
74 [Op.or]: [
75 {
76 [Op.and]: [
77 { videoId: { [Op.not]: null } },
78 searchAttribute(options.search, '$Video.name$')
79 ]
80 },
81 {
82 [Op.and]: [
83 { videoId: { [Op.not]: null } },
84 searchAttribute(options.search, '$Video.VideoChannel.name$')
85 ]
86 },
87 {
88 [Op.and]: [
89 { deletedVideo: { [Op.not]: null } },
90 { deletedVideo: searchAttribute(options.search, 'name') }
91 ]
92 },
93 {
94 [Op.and]: [
95 { deletedVideo: { [Op.not]: null } },
96 { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } }
97 ]
98 },
99 searchAttribute(options.search, '$Account.name$')
100 ]
101 })
102 }
103
104 if (options.id) Object.assign(where, { id: options.id })
105 if (options.state) Object.assign(where, { state: options.state })
106
107 if (options.videoIs === 'deleted') {
108 Object.assign(where, {
109 deletedVideo: {
110 [Op.not]: null
111 }
112 })
113 }
114
115 if (options.predefinedReasonId) {
116 Object.assign(where, {
117 predefinedReasons: {
118 [Op.contains]: [ options.predefinedReasonId ]
119 }
120 })
121 }
122
123 const onlyBlacklisted = options.videoIs === 'blacklisted'
124
125 return {
126 attributes: {
127 include: [
128 [
129 // we don't care about this count for deleted videos, so there are not included
130 literal(
131 '(' +
132 'SELECT count(*) ' +
133 'FROM "videoAbuse" ' +
134 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' +
135 ')'
136 ),
137 'countReportsForVideo'
138 ],
139 [
140 // we don't care about this count for deleted videos, so there are not included
141 literal(
142 '(' +
143 'SELECT t.nth ' +
144 'FROM ( ' +
145 'SELECT id, ' +
146 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
147 'FROM "videoAbuse" ' +
148 ') t ' +
149 'WHERE t.id = "VideoAbuseModel".id ' +
150 ')'
151 ),
152 'nthReportForVideo'
153 ],
154 [
155 literal(
156 '(' +
157 'SELECT count("videoAbuse"."id") ' +
158 'FROM "videoAbuse" ' +
159 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
160 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
161 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
162 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
163 ')'
164 ),
165 'countReportsForReporter__video'
166 ],
167 [
168 literal(
169 '(' +
170 'SELECT count(DISTINCT "videoAbuse"."id") ' +
171 'FROM "videoAbuse" ' +
172 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` +
173 ')'
174 ),
175 'countReportsForReporter__deletedVideo'
176 ],
177 [
178 literal(
179 '(' +
180 'SELECT count(DISTINCT "videoAbuse"."id") ' +
181 'FROM "videoAbuse" ' +
182 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
183 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
184 'INNER JOIN "account" ON ' +
185 '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' +
186 `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
187 ')'
188 ),
189 'countReportsForReportee__video'
190 ],
191 [
192 literal(
193 '(' +
194 'SELECT count(DISTINCT "videoAbuse"."id") ' +
195 'FROM "videoAbuse" ' +
196 `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` +
197 `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
198 `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
199 ')'
200 ),
201 'countReportsForReportee__deletedVideo'
202 ]
203 ]
204 },
205 include: [
206 {
207 model: AccountModel,
208 required: true,
209 where: searchAttribute(options.searchReporter, 'name')
210 },
211 {
212 model: VideoModel,
213 required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel),
214 where: searchAttribute(options.searchVideo, 'name'),
215 include: [
216 {
217 model: ThumbnailModel
218 },
219 {
220 model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
221 where: searchAttribute(options.searchVideoChannel, 'name'),
222 include: [
223 {
224 model: AccountModel,
225 where: searchAttribute(options.searchReportee, 'name')
226 }
227 ]
228 },
229 {
230 attributes: [ 'id', 'reason', 'unfederated' ],
231 model: VideoBlacklistModel,
232 required: onlyBlacklisted
233 }
234 ]
235 }
236 ],
237 where
238 }
239 }
240}))
241@Table({
242 tableName: 'videoAbuse',
243 indexes: [
244 {
245 fields: [ 'videoId' ]
246 },
247 {
248 fields: [ 'reporterAccountId' ]
249 }
250 ]
251})
252export class VideoAbuseModel extends Model<VideoAbuseModel> {
253
254 @AllowNull(false)
255 @Default(null)
256 @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
257 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
258 reason: string
259
260 @AllowNull(false)
261 @Default(null)
262 @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
263 @Column
264 state: VideoAbuseState
265
266 @AllowNull(true)
267 @Default(null)
268 @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true))
269 @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
270 moderationComment: string
271
272 @AllowNull(true)
273 @Default(null)
274 @Column(DataType.JSONB)
275 deletedVideo: VideoDetails
276
277 @AllowNull(true)
278 @Default(null)
279 @Column(DataType.ARRAY(DataType.INTEGER))
280 predefinedReasons: VideoAbusePredefinedReasons[]
281
282 @AllowNull(true)
283 @Default(null)
284 @Column
285 startAt: number
286
287 @AllowNull(true)
288 @Default(null)
289 @Column
290 endAt: number
291
292 @CreatedAt
293 createdAt: Date
294
295 @UpdatedAt
296 updatedAt: Date
297
298 @ForeignKey(() => AccountModel)
299 @Column
300 reporterAccountId: number
301
302 @BelongsTo(() => AccountModel, {
303 foreignKey: {
304 allowNull: true
305 },
306 onDelete: 'set null'
307 })
308 Account: AccountModel
309
310 @ForeignKey(() => VideoModel)
311 @Column
312 videoId: number
313
314 @BelongsTo(() => VideoModel, {
315 foreignKey: {
316 allowNull: true
317 },
318 onDelete: 'set null'
319 })
320 Video: VideoModel
321
322 static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
323 const videoAttributes = {}
324 if (videoId) videoAttributes['videoId'] = videoId
325 if (uuid) videoAttributes['deletedVideo'] = { uuid }
326
327 const query = {
328 where: {
329 id,
330 ...videoAttributes
331 }
332 }
333 return VideoAbuseModel.findOne(query)
334 }
335
336 static listForApi (parameters: {
337 start: number
338 count: number
339 sort: string
340
341 serverAccountId: number
342 user?: MUserAccountId
343
344 id?: number
345 predefinedReason?: VideoAbusePredefinedReasonsString
346 state?: VideoAbuseState
347 videoIs?: VideoAbuseVideoIs
348
349 search?: string
350 searchReporter?: string
351 searchReportee?: string
352 searchVideo?: string
353 searchVideoChannel?: string
354 }) {
355 const {
356 start,
357 count,
358 sort,
359 search,
360 user,
361 serverAccountId,
362 state,
363 videoIs,
364 predefinedReason,
365 searchReportee,
366 searchVideo,
367 searchVideoChannel,
368 searchReporter,
369 id
370 } = parameters
371
372 const userAccountId = user ? user.Account.id : undefined
373 const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined
374
375 const query = {
376 offset: start,
377 limit: count,
378 order: getSort(sort),
379 col: 'VideoAbuseModel.id',
380 distinct: true
381 }
382
383 const filters = {
384 id,
385 predefinedReasonId,
386 search,
387 state,
388 videoIs,
389 searchReportee,
390 searchVideo,
391 searchVideoChannel,
392 searchReporter,
393 serverAccountId,
394 userAccountId
395 }
396
397 return VideoAbuseModel
398 .scope([
399 { method: [ ScopeNames.FOR_API, filters ] }
400 ])
401 .findAndCountAll(query)
402 .then(({ rows, count }) => {
403 return { total: count, data: rows }
404 })
405 }
406
407 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
408 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
409 const countReportsForVideo = this.get('countReportsForVideo') as number
410 const nthReportForVideo = this.get('nthReportForVideo') as number
411 const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
412 const countReportsForReporterDeletedVideo = this.get('countReportsForReporter__deletedVideo') as number
413 const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
414 const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
415
416 const video = this.Video
417 ? this.Video
418 : this.deletedVideo
419
420 return {
421 id: this.id,
422 reason: this.reason,
423 predefinedReasons,
424 reporterAccount: this.Account.toFormattedJSON(),
425 state: {
426 id: this.state,
427 label: VideoAbuseModel.getStateLabel(this.state)
428 },
429 moderationComment: this.moderationComment,
430 video: {
431 id: video.id,
432 uuid: video.uuid,
433 name: video.name,
434 nsfw: video.nsfw,
435 deleted: !this.Video,
436 blacklisted: this.Video?.isBlacklisted() || false,
437 thumbnailPath: this.Video?.getMiniatureStaticPath(),
438 channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
439 },
440 createdAt: this.createdAt,
441 updatedAt: this.updatedAt,
442 startAt: this.startAt,
443 endAt: this.endAt,
444 count: countReportsForVideo || 0,
445 nth: nthReportForVideo || 0,
446 countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
447 countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0)
448 }
449 }
450
451 toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
452 const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
453
454 const startAt = this.startAt
455 const endAt = this.endAt
456
457 return {
458 type: 'Flag' as 'Flag',
459 content: this.reason,
460 object: this.Video.url,
461 tag: predefinedReasons.map(r => ({
462 type: 'Hashtag' as 'Hashtag',
463 name: r
464 })),
465 startAt,
466 endAt
467 }
468 }
469
470 private static getStateLabel (id: number) {
471 return VIDEO_ABUSE_STATES[id] || 'Unknown'
472 }
473
474 private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] {
475 return (predefinedReasons || [])
476 .filter(r => r in VideoAbusePredefinedReasons)
477 .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString)
478 }
479}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index e2718300e..272bba0e1 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,4 +1,5 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { remove } from 'fs-extra'
2import { maxBy, minBy, pick } from 'lodash' 3import { maxBy, minBy, pick } from 'lodash'
3import { join } from 'path' 4import { join } from 'path'
4import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' 5import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
@@ -23,10 +24,18 @@ import {
23 Table, 24 Table,
24 UpdatedAt 25 UpdatedAt
25} from 'sequelize-typescript' 26} from 'sequelize-typescript'
26import { UserRight, VideoPrivacy, VideoState, ResultList } from '../../../shared' 27import { buildNSFWFilter } from '@server/helpers/express-utils'
28import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video'
29import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
30import { getServerActor } from '@server/models/application/application'
31import { ModelCache } from '@server/models/model-cache'
32import { VideoFile } from '@shared/models/videos/video-file.model'
33import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
27import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 34import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
28import { Video, VideoDetails } from '../../../shared/models/videos' 35import { Video, VideoDetails } from '../../../shared/models/videos'
36import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
29import { VideoFilter } from '../../../shared/models/videos/video-query.type' 37import { VideoFilter } from '../../../shared/models/videos/video-query.type'
38import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
30import { peertubeTruncate } from '../../helpers/core-utils' 39import { peertubeTruncate } from '../../helpers/core-utils'
31import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' 40import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
32import { isBooleanValid } from '../../helpers/custom-validators/misc' 41import { isBooleanValid } from '../../helpers/custom-validators/misc'
@@ -43,6 +52,7 @@ import {
43} from '../../helpers/custom-validators/videos' 52} from '../../helpers/custom-validators/videos'
44import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' 53import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
45import { logger } from '../../helpers/logger' 54import { logger } from '../../helpers/logger'
55import { CONFIG } from '../../initializers/config'
46import { 56import {
47 ACTIVITY_PUB, 57 ACTIVITY_PUB,
48 API_VERSION, 58 API_VERSION,
@@ -59,40 +69,6 @@ import {
59 WEBSERVER 69 WEBSERVER
60} from '../../initializers/constants' 70} from '../../initializers/constants'
61import { sendDeleteVideo } from '../../lib/activitypub/send' 71import { sendDeleteVideo } from '../../lib/activitypub/send'
62import { AccountModel } from '../account/account'
63import { AccountVideoRateModel } from '../account/account-video-rate'
64import { ActorModel } from '../activitypub/actor'
65import { AvatarModel } from '../avatar/avatar'
66import { ServerModel } from '../server/server'
67import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
68import { TagModel } from './tag'
69import { VideoAbuseModel } from './video-abuse'
70import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
71import { VideoCommentModel } from './video-comment'
72import { VideoFileModel } from './video-file'
73import { VideoShareModel } from './video-share'
74import { VideoTagModel } from './video-tag'
75import { ScheduleVideoUpdateModel } from './schedule-video-update'
76import { VideoCaptionModel } from './video-caption'
77import { VideoBlacklistModel } from './video-blacklist'
78import { remove } from 'fs-extra'
79import { VideoViewModel } from './video-view'
80import { VideoRedundancyModel } from '../redundancy/video-redundancy'
81import {
82 videoFilesModelToFormattedJSON,
83 VideoFormattingJSONOptions,
84 videoModelToActivityPubObject,
85 videoModelToFormattedDetailsJSON,
86 videoModelToFormattedJSON
87} from './video-format-utils'
88import { UserVideoHistoryModel } from '../account/user-video-history'
89import { VideoImportModel } from './video-import'
90import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
91import { VideoPlaylistElementModel } from './video-playlist-element'
92import { CONFIG } from '../../initializers/config'
93import { ThumbnailModel } from './thumbnail'
94import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
95import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
96import { 72import {
97 MChannel, 73 MChannel,
98 MChannelAccountDefault, 74 MChannelAccountDefault,
@@ -118,15 +94,39 @@ import {
118 MVideoWithFile, 94 MVideoWithFile,
119 MVideoWithRights 95 MVideoWithRights
120} from '../../types/models' 96} from '../../types/models'
121import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
122import { MThumbnail } from '../../types/models/video/thumbnail' 97import { MThumbnail } from '../../types/models/video/thumbnail'
123import { VideoFile } from '@shared/models/videos/video-file.model' 98import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
124import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' 99import { VideoAbuseModel } from '../abuse/video-abuse'
125import { ModelCache } from '@server/models/model-cache' 100import { AccountModel } from '../account/account'
101import { AccountVideoRateModel } from '../account/account-video-rate'
102import { UserVideoHistoryModel } from '../account/user-video-history'
103import { ActorModel } from '../activitypub/actor'
104import { AvatarModel } from '../avatar/avatar'
105import { VideoRedundancyModel } from '../redundancy/video-redundancy'
106import { ServerModel } from '../server/server'
107import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
108import { ScheduleVideoUpdateModel } from './schedule-video-update'
109import { TagModel } from './tag'
110import { ThumbnailModel } from './thumbnail'
111import { VideoBlacklistModel } from './video-blacklist'
112import { VideoCaptionModel } from './video-caption'
113import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
114import { VideoCommentModel } from './video-comment'
115import { VideoFileModel } from './video-file'
116import {
117 videoFilesModelToFormattedJSON,
118 VideoFormattingJSONOptions,
119 videoModelToActivityPubObject,
120 videoModelToFormattedDetailsJSON,
121 videoModelToFormattedJSON
122} from './video-format-utils'
123import { VideoImportModel } from './video-import'
124import { VideoPlaylistElementModel } from './video-playlist-element'
126import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' 125import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
127import { buildNSFWFilter } from '@server/helpers/express-utils' 126import { VideoShareModel } from './video-share'
128import { getServerActor } from '@server/models/application/application' 127import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
129import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video" 128import { VideoTagModel } from './video-tag'
129import { VideoViewModel } from './video-view'
130 130
131export enum ScopeNames { 131export enum ScopeNames {
132 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', 132 AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',