diff options
Diffstat (limited to 'server/controllers')
-rw-r--r-- | server/controllers/api/accounts.ts | 18 | ||||
-rw-r--r-- | server/controllers/api/overviews.ts | 8 | ||||
-rw-r--r-- | server/controllers/api/search/search-videos.ts | 12 | ||||
-rw-r--r-- | server/controllers/api/users/my-subscriptions.ts | 9 | ||||
-rw-r--r-- | server/controllers/api/video-channel.ts | 17 | ||||
-rw-r--r-- | server/controllers/api/videos/index.ts | 10 | ||||
-rw-r--r-- | server/controllers/bots.ts | 10 | ||||
-rw-r--r-- | server/controllers/feeds.ts | 21 |
8 files changed, 79 insertions, 26 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 8eb880d59..44edffe38 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -2,6 +2,7 @@ import express from 'express' | |||
2 | import { pickCommonVideoQuery } from '@server/helpers/query' | 2 | import { pickCommonVideoQuery } from '@server/helpers/query' |
3 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | 3 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
4 | import { getServerActor } from '@server/models/application/application' | 4 | import { getServerActor } from '@server/models/application/application' |
5 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
5 | import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 6 | import { buildNSFWFilter, getCountVideos, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' |
6 | import { getFormattedObjects } from '../../helpers/utils' | 7 | import { getFormattedObjects } from '../../helpers/utils' |
7 | import { JobQueue } from '../../lib/job-queue' | 8 | import { JobQueue } from '../../lib/job-queue' |
@@ -169,17 +170,24 @@ async function listAccountPlaylists (req: express.Request, res: express.Response | |||
169 | } | 170 | } |
170 | 171 | ||
171 | async function listAccountVideos (req: express.Request, res: express.Response) { | 172 | async function listAccountVideos (req: express.Request, res: express.Response) { |
173 | const serverActor = await getServerActor() | ||
174 | |||
172 | const account = res.locals.account | 175 | const account = res.locals.account |
173 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | 176 | |
177 | const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res) | ||
178 | ? null | ||
179 | : { | ||
180 | actorId: serverActor.id, | ||
181 | orLocalVideos: true | ||
182 | } | ||
183 | |||
174 | const countVideos = getCountVideos(req) | 184 | const countVideos = getCountVideos(req) |
175 | const query = pickCommonVideoQuery(req.query) | 185 | const query = pickCommonVideoQuery(req.query) |
176 | 186 | ||
177 | const apiOptions = await Hooks.wrapObject({ | 187 | const apiOptions = await Hooks.wrapObject({ |
178 | ...query, | 188 | ...query, |
179 | 189 | ||
180 | followerActorId, | 190 | displayOnlyForFollower, |
181 | search: req.query.search, | ||
182 | includeLocalVideos: true, | ||
183 | nsfw: buildNSFWFilter(res, query.nsfw), | 191 | nsfw: buildNSFWFilter(res, query.nsfw), |
184 | withFiles: false, | 192 | withFiles: false, |
185 | accountId: account.id, | 193 | accountId: account.id, |
@@ -193,7 +201,7 @@ async function listAccountVideos (req: express.Request, res: express.Response) { | |||
193 | 'filter:api.accounts.videos.list.result' | 201 | 'filter:api.accounts.videos.list.result' |
194 | ) | 202 | ) |
195 | 203 | ||
196 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 204 | return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) |
197 | } | 205 | } |
198 | 206 | ||
199 | async function listAccountRatings (req: express.Request, res: express.Response) { | 207 | async function listAccountRatings (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/api/overviews.ts b/server/controllers/api/overviews.ts index 5b16232e2..68626a508 100644 --- a/server/controllers/api/overviews.ts +++ b/server/controllers/api/overviews.ts | |||
@@ -8,6 +8,7 @@ import { buildNSFWFilter } from '../../helpers/express-utils' | |||
8 | import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants' | 8 | import { MEMOIZE_TTL, OVERVIEWS } from '../../initializers/constants' |
9 | import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares' | 9 | import { asyncMiddleware, optionalAuthenticate, videosOverviewValidator } from '../../middlewares' |
10 | import { TagModel } from '../../models/video/tag' | 10 | import { TagModel } from '../../models/video/tag' |
11 | import { getServerActor } from '@server/models/application/application' | ||
11 | 12 | ||
12 | const overviewsRouter = express.Router() | 13 | const overviewsRouter = express.Router() |
13 | 14 | ||
@@ -109,11 +110,16 @@ async function getVideos ( | |||
109 | res: express.Response, | 110 | res: express.Response, |
110 | where: { videoChannelId?: number, tagsOneOf?: string[], categoryOneOf?: number[] } | 111 | where: { videoChannelId?: number, tagsOneOf?: string[], categoryOneOf?: number[] } |
111 | ) { | 112 | ) { |
113 | const serverActor = await getServerActor() | ||
114 | |||
112 | const query = await Hooks.wrapObject({ | 115 | const query = await Hooks.wrapObject({ |
113 | start: 0, | 116 | start: 0, |
114 | count: 12, | 117 | count: 12, |
115 | sort: '-createdAt', | 118 | sort: '-createdAt', |
116 | includeLocalVideos: true, | 119 | displayOnlyForFollower: { |
120 | actorId: serverActor.id, | ||
121 | orLocalVideos: true | ||
122 | }, | ||
117 | nsfw: buildNSFWFilter(res), | 123 | nsfw: buildNSFWFilter(res), |
118 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, | 124 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, |
119 | withFiles: false, | 125 | withFiles: false, |
diff --git a/server/controllers/api/search/search-videos.ts b/server/controllers/api/search/search-videos.ts index 90946cb74..6db70acdf 100644 --- a/server/controllers/api/search/search-videos.ts +++ b/server/controllers/api/search/search-videos.ts | |||
@@ -7,6 +7,8 @@ import { WEBSERVER } from '@server/initializers/constants' | |||
7 | import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' | 7 | import { getOrCreateAPVideo } from '@server/lib/activitypub/videos' |
8 | import { Hooks } from '@server/lib/plugins/hooks' | 8 | import { Hooks } from '@server/lib/plugins/hooks' |
9 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' | 9 | import { buildMutedForSearchIndex, isSearchIndexSearch, isURISearch } from '@server/lib/search' |
10 | import { getServerActor } from '@server/models/application/application' | ||
11 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
10 | import { HttpStatusCode, ResultList, Video } from '@shared/models' | 12 | import { HttpStatusCode, ResultList, Video } from '@shared/models' |
11 | import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search' | 13 | import { VideosSearchQueryAfterSanitize } from '../../../../shared/models/search' |
12 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' | 14 | import { buildNSFWFilter, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils' |
@@ -100,11 +102,15 @@ async function searchVideosIndex (query: VideosSearchQueryAfterSanitize, res: ex | |||
100 | } | 102 | } |
101 | 103 | ||
102 | async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: express.Response) { | 104 | async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: express.Response) { |
105 | const serverActor = await getServerActor() | ||
106 | |||
103 | const apiOptions = await Hooks.wrapObject({ | 107 | const apiOptions = await Hooks.wrapObject({ |
104 | ...query, | 108 | ...query, |
105 | 109 | ||
106 | includeLocalVideos: true, | 110 | displayOnlyForFollower: { |
107 | filter: query.filter, | 111 | actorId: serverActor.id, |
112 | orLocalVideos: true | ||
113 | }, | ||
108 | 114 | ||
109 | nsfw: buildNSFWFilter(res, query.nsfw), | 115 | nsfw: buildNSFWFilter(res, query.nsfw), |
110 | user: res.locals.oauth | 116 | user: res.locals.oauth |
@@ -118,7 +124,7 @@ async function searchVideosDB (query: VideosSearchQueryAfterSanitize, res: expre | |||
118 | 'filter:api.search.videos.local.list.result' | 124 | 'filter:api.search.videos.local.list.result' |
119 | ) | 125 | ) |
120 | 126 | ||
121 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 127 | return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) |
122 | } | 128 | } |
123 | 129 | ||
124 | async function searchVideoURI (url: string, res: express.Response) { | 130 | async function searchVideoURI (url: string, res: express.Response) { |
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts index b2b441673..d96378180 100644 --- a/server/controllers/api/users/my-subscriptions.ts +++ b/server/controllers/api/users/my-subscriptions.ts | |||
@@ -2,6 +2,7 @@ import 'multer' | |||
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { pickCommonVideoQuery } from '@server/helpers/query' | 3 | import { pickCommonVideoQuery } from '@server/helpers/query' |
4 | import { sendUndoFollow } from '@server/lib/activitypub/send' | 4 | import { sendUndoFollow } from '@server/lib/activitypub/send' |
5 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
5 | import { VideoChannelModel } from '@server/models/video/video-channel' | 6 | import { VideoChannelModel } from '@server/models/video/video-channel' |
6 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | 7 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' |
7 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' | 8 | import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils' |
@@ -175,13 +176,15 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res | |||
175 | const resultList = await VideoModel.listForApi({ | 176 | const resultList = await VideoModel.listForApi({ |
176 | ...query, | 177 | ...query, |
177 | 178 | ||
178 | includeLocalVideos: false, | 179 | displayOnlyForFollower: { |
180 | actorId: user.Account.Actor.id, | ||
181 | orLocalVideos: false | ||
182 | }, | ||
179 | nsfw: buildNSFWFilter(res, query.nsfw), | 183 | nsfw: buildNSFWFilter(res, query.nsfw), |
180 | withFiles: false, | 184 | withFiles: false, |
181 | followerActorId: user.Account.Actor.id, | ||
182 | user, | 185 | user, |
183 | countVideos | 186 | countVideos |
184 | }) | 187 | }) |
185 | 188 | ||
186 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 189 | return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) |
187 | } | 190 | } |
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 7bf7a68c9..f9c1a405d 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -3,6 +3,7 @@ import { pickCommonVideoQuery } from '@server/helpers/query' | |||
3 | import { Hooks } from '@server/lib/plugins/hooks' | 3 | import { Hooks } from '@server/lib/plugins/hooks' |
4 | import { ActorFollowModel } from '@server/models/actor/actor-follow' | 4 | import { ActorFollowModel } from '@server/models/actor/actor-follow' |
5 | import { getServerActor } from '@server/models/application/application' | 5 | import { getServerActor } from '@server/models/application/application' |
6 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
6 | import { MChannelBannerAccountDefault } from '@server/types/models' | 7 | import { MChannelBannerAccountDefault } from '@server/types/models' |
7 | import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' | 8 | import { ActorImageType, VideoChannelCreate, VideoChannelUpdate } from '../../../shared' |
8 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | 9 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' |
@@ -327,16 +328,24 @@ async function listVideoChannelPlaylists (req: express.Request, res: express.Res | |||
327 | } | 328 | } |
328 | 329 | ||
329 | async function listVideoChannelVideos (req: express.Request, res: express.Response) { | 330 | async function listVideoChannelVideos (req: express.Request, res: express.Response) { |
331 | const serverActor = await getServerActor() | ||
332 | |||
330 | const videoChannelInstance = res.locals.videoChannel | 333 | const videoChannelInstance = res.locals.videoChannel |
331 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | 334 | |
335 | const displayOnlyForFollower = isUserAbleToSearchRemoteURI(res) | ||
336 | ? null | ||
337 | : { | ||
338 | actorId: serverActor.id, | ||
339 | orLocalVideos: true | ||
340 | } | ||
341 | |||
332 | const countVideos = getCountVideos(req) | 342 | const countVideos = getCountVideos(req) |
333 | const query = pickCommonVideoQuery(req.query) | 343 | const query = pickCommonVideoQuery(req.query) |
334 | 344 | ||
335 | const apiOptions = await Hooks.wrapObject({ | 345 | const apiOptions = await Hooks.wrapObject({ |
336 | ...query, | 346 | ...query, |
337 | 347 | ||
338 | followerActorId, | 348 | displayOnlyForFollower, |
339 | includeLocalVideos: true, | ||
340 | nsfw: buildNSFWFilter(res, query.nsfw), | 349 | nsfw: buildNSFWFilter(res, query.nsfw), |
341 | withFiles: false, | 350 | withFiles: false, |
342 | videoChannelId: videoChannelInstance.id, | 351 | videoChannelId: videoChannelInstance.id, |
@@ -350,7 +359,7 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon | |||
350 | 'filter:api.video-channels.videos.list.result' | 359 | 'filter:api.video-channels.videos.list.result' |
351 | ) | 360 | ) |
352 | 361 | ||
353 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 362 | return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) |
354 | } | 363 | } |
355 | 364 | ||
356 | async function listVideoChannelFollowers (req: express.Request, res: express.Response) { | 365 | async function listVideoChannelFollowers (req: express.Request, res: express.Response) { |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index c0c77f3f7..821ed7ff3 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -5,6 +5,7 @@ import { doJSONRequest } from '@server/helpers/requests' | |||
5 | import { LiveManager } from '@server/lib/live' | 5 | import { LiveManager } from '@server/lib/live' |
6 | import { openapiOperationDoc } from '@server/middlewares/doc' | 6 | import { openapiOperationDoc } from '@server/middlewares/doc' |
7 | import { getServerActor } from '@server/models/application/application' | 7 | import { getServerActor } from '@server/models/application/application' |
8 | import { guessAdditionalAttributesFromQuery } from '@server/models/video/formatter/video-format-utils' | ||
8 | import { MVideoAccountLight } from '@server/types/models' | 9 | import { MVideoAccountLight } from '@server/types/models' |
9 | import { HttpStatusCode } from '../../../../shared/models' | 10 | import { HttpStatusCode } from '../../../../shared/models' |
10 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 11 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
@@ -211,13 +212,18 @@ async function getVideoFileMetadata (req: express.Request, res: express.Response | |||
211 | } | 212 | } |
212 | 213 | ||
213 | async function listVideos (req: express.Request, res: express.Response) { | 214 | async function listVideos (req: express.Request, res: express.Response) { |
215 | const serverActor = await getServerActor() | ||
216 | |||
214 | const query = pickCommonVideoQuery(req.query) | 217 | const query = pickCommonVideoQuery(req.query) |
215 | const countVideos = getCountVideos(req) | 218 | const countVideos = getCountVideos(req) |
216 | 219 | ||
217 | const apiOptions = await Hooks.wrapObject({ | 220 | const apiOptions = await Hooks.wrapObject({ |
218 | ...query, | 221 | ...query, |
219 | 222 | ||
220 | includeLocalVideos: true, | 223 | displayOnlyForFollower: { |
224 | actorId: serverActor.id, | ||
225 | orLocalVideos: true | ||
226 | }, | ||
221 | nsfw: buildNSFWFilter(res, query.nsfw), | 227 | nsfw: buildNSFWFilter(res, query.nsfw), |
222 | withFiles: false, | 228 | withFiles: false, |
223 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, | 229 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined, |
@@ -230,7 +236,7 @@ async function listVideos (req: express.Request, res: express.Response) { | |||
230 | 'filter:api.videos.list.result' | 236 | 'filter:api.videos.list.result' |
231 | ) | 237 | ) |
232 | 238 | ||
233 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 239 | return res.json(getFormattedObjects(resultList.data, resultList.total, guessAdditionalAttributesFromQuery(query))) |
234 | } | 240 | } |
235 | 241 | ||
236 | async function removeVideo (_req: express.Request, res: express.Response) { | 242 | async function removeVideo (_req: express.Request, res: express.Response) { |
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts index 63db345bf..9f03de7e8 100644 --- a/server/controllers/bots.ts +++ b/server/controllers/bots.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { getServerActor } from '@server/models/application/application' | ||
1 | import express from 'express' | 2 | import express from 'express' |
2 | import { truncate } from 'lodash' | 3 | import { truncate } from 'lodash' |
3 | import { SitemapStream, streamToPromise } from 'sitemap' | 4 | import { SitemapStream, streamToPromise } from 'sitemap' |
@@ -63,13 +64,18 @@ async function getSitemapAccountUrls () { | |||
63 | } | 64 | } |
64 | 65 | ||
65 | async function getSitemapLocalVideoUrls () { | 66 | async function getSitemapLocalVideoUrls () { |
67 | const serverActor = await getServerActor() | ||
68 | |||
66 | const { data } = await VideoModel.listForApi({ | 69 | const { data } = await VideoModel.listForApi({ |
67 | start: 0, | 70 | start: 0, |
68 | count: undefined, | 71 | count: undefined, |
69 | sort: 'createdAt', | 72 | sort: 'createdAt', |
70 | includeLocalVideos: true, | 73 | displayOnlyForFollower: { |
74 | actorId: serverActor.id, | ||
75 | orLocalVideos: true | ||
76 | }, | ||
77 | isLocal: true, | ||
71 | nsfw: buildNSFWFilter(), | 78 | nsfw: buildNSFWFilter(), |
72 | filter: 'local', | ||
73 | withFiles: false, | 79 | withFiles: false, |
74 | countVideos: false | 80 | countVideos: false |
75 | }) | 81 | }) |
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 5ac2e43a1..1f6aebac3 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import Feed from 'pfeed' | 2 | import Feed from 'pfeed' |
3 | import { getServerActor } from '@server/models/application/application' | ||
3 | import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' | 4 | import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' |
4 | import { VideoFilter } from '../../shared/models/videos/video-query.type' | ||
5 | import { buildNSFWFilter } from '../helpers/express-utils' | 5 | import { buildNSFWFilter } from '../helpers/express-utils' |
6 | import { CONFIG } from '../initializers/config' | 6 | import { CONFIG } from '../initializers/config' |
7 | import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' | 7 | import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' |
@@ -160,13 +160,18 @@ async function generateVideoFeed (req: express.Request, res: express.Response) { | |||
160 | videoChannelId: videoChannel ? videoChannel.id : null | 160 | videoChannelId: videoChannel ? videoChannel.id : null |
161 | } | 161 | } |
162 | 162 | ||
163 | const server = await getServerActor() | ||
163 | const { data } = await VideoModel.listForApi({ | 164 | const { data } = await VideoModel.listForApi({ |
164 | start, | 165 | start, |
165 | count: FEEDS.COUNT, | 166 | count: FEEDS.COUNT, |
166 | sort: req.query.sort, | 167 | sort: req.query.sort, |
167 | includeLocalVideos: true, | 168 | displayOnlyForFollower: { |
169 | actorId: server.id, | ||
170 | orLocalVideos: true | ||
171 | }, | ||
168 | nsfw, | 172 | nsfw, |
169 | filter: req.query.filter as VideoFilter, | 173 | isLocal: req.query.isLocal, |
174 | include: req.query.include, | ||
170 | withFiles: true, | 175 | withFiles: true, |
171 | countVideos: false, | 176 | countVideos: false, |
172 | ...options | 177 | ...options |
@@ -196,14 +201,18 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp | |||
196 | start, | 201 | start, |
197 | count: FEEDS.COUNT, | 202 | count: FEEDS.COUNT, |
198 | sort: req.query.sort, | 203 | sort: req.query.sort, |
199 | includeLocalVideos: false, | ||
200 | nsfw, | 204 | nsfw, |
201 | filter: req.query.filter as VideoFilter, | 205 | |
206 | isLocal: req.query.isLocal, | ||
207 | include: req.query.include, | ||
202 | 208 | ||
203 | withFiles: true, | 209 | withFiles: true, |
204 | countVideos: false, | 210 | countVideos: false, |
205 | 211 | ||
206 | followerActorId: res.locals.user.Account.Actor.id, | 212 | displayOnlyForFollower: { |
213 | actorId: res.locals.user.Account.Actor.id, | ||
214 | orLocalVideos: false | ||
215 | }, | ||
207 | user: res.locals.user | 216 | user: res.locals.user |
208 | }) | 217 | }) |
209 | 218 | ||