TU Media Gallery
src/tu-media-gallery/ — один спільний бекенд-сервіс, який дає фронтенду уніфікований API медіа-галереї TU (фото/відео/аудіо) над трьома різними партнерськими API (Chathouse, Prime, Golden), не змушуючи фронт і жоден інший код знати про відмінності кожного джерела.
Глобальний опис фічі (суть, ролі, різниця по проєктах) — Project Platform (stack-docs). Що саме бачить і робить фронтенд (поля, getAllows, user cases) — Для frontend. Мапінг кожного project (Chathouse/Prime/Golden) на спільну модель — Мапінг адаптерів.
Архітектура
Ланцюжок одного виклику: TuMediaGalleryController → TuMediaGalleryService → TuMediaGalleryGateway → TuMediaGalleryAdapterRegistry → конкретний XxxTuMediaGalleryAdapter → XxxClient (project-services/) → HTTP-запит до project-сервісу.
Кожен шар відповідає рівно за одну річ і не знає про шари через один:
- Controller — HTTP-контракт для фронта (
getAllows/getMediaList/createMedia/deleteMedia), guard по ролях, парсинг multipart для upload. Не знає, що таке Chathouse чи Prime. - Service — тонкий делегат до Gateway плюс читання статичної конфігурації
TU_MEDIA_GALLERY_ALLOWSдляgetAllows. Жодної project-специфічної логіки. - Gateway — резолвить
projectу конкретний адаптер через Registry і викликає на ньому той самий метод. Сам нічого не конвертує. - AdapterRegistry —
Map<Projects, TuMediaGalleryAdapter>; кидаєNotFoundException, якщо адаптер для project не зареєстрований (зараз — Chathouse/Prime/Golden; Udate ще не реалізований). - Adapter (по одному класу на project) — єдине місце, яке знає формат конкретного project API: перетворює спільний запит у project-специфічний виклик і сиру відповідь назад у спільну модель. Детальний мапінг кожного — Мапінг адаптерів.
- Client (
project-services/*.client.ts) — лише HTTP-виклик до project-сервісу (ProjectServiceClient.postLegacy/postFormLegacy), без жодної конвертації. Частина клієнтів обгорнута Redis-кеш-декоратором (Decorator Pattern) — потрібно лише джерелам без нативної пагінації (Chathouse, Golden); Prime кешу не має.
Спільні інтерфейси (@it-monkeys/contracts, домен tu-media-gallery)
Принцип: кожен project API моделює медіа по-своєму (статуси, типи, категорії, пагінація, поля відповіді) — контракт визначає ОДНУ спільну форму, а адаптер відповідає за приведення до неї. Ніхто, крім свого адаптера, не бачить полів апстріму.
TuMediaGalleryItem— канонічний медіа-запис:mediaId,tuId,project,type,status, опційніcategory/channel/name/createdBy/moderatedBy, і фіксований набір розмірівurl/previewUrl/mediumUrl. Усі project-специфічні поля (числові коди, теги,isModerate) звужуються рівно в ці кілька уніфікованих значень.TuMediaGalleryPage<T>— опаковий курсор{ result: T[], cursor: string | null }. Викликачу байдуже, чи джерело має нативний курсор (Prime) чи курсор емулюється на нашій стороні (Chathouse, Golden).TuMediaGalleryAllows— ієрархія дозволівproject → channels[] → types[] → { statuses, delete, upload }. Описує, що саме дозволено на конкретному project, замість того щоб хардкодити per-projectifу фронті чи бекенді.GetTuMediaGalleryListRequest/UploadTuMediaGalleryRequest/DeleteTuMediaGalleryRequest— завжди за однимtype(і, якщо є, однимchannel) за раз. Якщо джерело фізично віддає ширший набір за один виклик (Chathouseapprovedповертає photo+video разом), звуження до потрібногоtypeробить сам адаптер, а не спільний шар.TuMediaGalleryFile— уніфікована форма файлу для upload (data/name/mimetype/size), не залежить від Express/Multer типів — адаптер і project-клієнт працюють з нею однаково незалежно від того, як контролер отримав файл.
Звʼязок спільного сервісу з кожним project API
Спільний сервіс (Controller/Service/Gateway) ніколи не звертається до project API напряму і не знає жодного project-специфічного поля чи ендпоінта. Єдина точка дотику — інтерфейс TuMediaGalleryAdapter (getMediaList / createMedia / deleteMedia), який реалізує рівно один клас на кожен project і який Registry віддає Gateway за ключем project.
Наразі внутрішні ендпоінти, які мали б приймати ці виклики (internal/... роути на стороні stack-chathouse/stack-golden/stack-prime), ще не реалізовані в цих репозиторіях — контракти (getFullPath() кожного namespace) вже визначені, і адаптери вже готові їх викликати, але приймаючих контролерів на project-сервісах поки немає.
Для frontend
Розділ реалізований так, що для кожного проекту реалізоване одне апі.
Swagger: тег TU Media Gallery у /v2/docs.
В Swagger вказано всі доступні запити та всі доступні поля. Нижче вказані нюанси та пояснення до важливих полів.
Нюанси
channel— окреме поле, де використовується медіа:undefined | chat | mail. Наприклад, на Golden буде розбивка по каналах. Якщо в allows будуть приходити доступні channel. Якщо їх більше одного, то потрібно буде робити розбивку і підгрузку медіа по різним каналам.type— тип файлу:photo | video | audio. В allows будуть приходити доступні types. Потрібно робити підгрузку медіа по різним types.status— глобальний enumapproved | on_moderation | declined. В allows будуть приходити доступні statuses. Потрібно підгружати медіа по channel-type-status.category— окремий enum еротичності/особливості контенту:undefined | erotic | pre_special | special | special_plus. Біля фоток відображається іконка, що відповідає категорії (наприклад,erotic— вогник).pre_special/special/special_plus— категорії з Prime. Якщо категорії немає — іконки немає.- Розміри — фіксований набір
url(оригінал) /previewUrl(мала версія) /mediumUrl(середня версія, якщо джерело її має). Для відео додатковоpreviewUrl(статичний кадр) таurl. Всюди є previewUrl, url. На деяких проектах немає mediumUrl. - Пагінація — курсорна
{ result: T[], cursor: string | null }(«логіка підгрузки по курсору — реалізуємо на нашій стороні»), незалежно від того, як пагінує джерело. name—string, опціональне поле; використовується в аудіо, щоб розрізняти audio між собою.
Приклади реалізації
User cases:
-
User клікає на розділ по певному project.
-
Відправляє запит на allows. Отримує налаштування по цьому розділу для цього проекту:
- розбивка медіа по
channel(якщо channels більше одного); - розбивка вікна по
typeмедіа — photo, video, audio; - чи можна видаляти медіа кожного
channel+type; - чи можна завантажувати медіа кожного
channel+type, і на яку вкладку; - чи є розбивка по статусах і які саме (наприклад,
declinedвідсутній на Golden).
Ієрархія дозволів:
channel→types→statuses. Підгрузка медіа реалізується по ланцюжкуchannel-type-status. - розбивка медіа по
-
Отримує списки TU, які за ним закріплені (operator, teamlead, top, client-manager?).
-
Клік на TU.
-
Підгружаємо порції медіа за дозволами:
channel-type-status, а якщоchannelнемає — простоtype-status.
Приклади трактування getAllows по проєктах:
Chathouse
- Один
channel, без імені. Тому розбивки по chat/mail немає, відповідно одне вікно.- Розбивка по
typeмедіа — photo, video.
- Розбивка по
- Видаляти можна.
- Завантаження фото/відео, без вибору категорії.
- Підгрузка медіа по
type-status: approved, declined, on_moderation, курсором.
Prime
- Один
channel, без імені — розбивки немає.- Розбивка по
typeмедіа — photo, video, audio.
- Розбивка по
- Можна видаляти.
- Завантаження фото, відео.
- Підгрузка медіа по
type-status: approved, declined, on_moderation, курсором.
Golden
- Два
channelз іменем — розбивка chat / mail.- Розбивка по
typeмедіа — photo, video.
- Розбивка по
- Можна видаляти chat / mail.
- Завантаження: chat — фото, відео; mail — фото.
- Підгрузка медіа по
channel-type-status: approved, declined, on_moderation, курсором.
Мапінг адаптерів
Мапінг з апстріму кожного project на спільну модель TuMediaGalleryItem — конкретно, як кожен адаптер (src/tu-media-gallery/adapters/*.ts) визначає type/status/category/channel, і як улаштована пагінація/upload/delete. Спільна логіка (Gateway/Registry, getAllows, keyset-пагінація) — у розділах вище.
Chathouse
- Джерела — три запити за статусом:
approved(окремий namespace, повертає photo+video разом),on_moderationіdeclined(спільний namespace, різний шлях, повертає обидва типи одним змішаним масивом). Сам статус визначається тим, який запит викликали, — у відповіді його немає. type/category— мапляться з числового коду, і код різний для approved і для moderation/declined/upload:- approved використовує
ChathouseMediaType:photo=14,eroticPhoto=10,video=13,eroticVideo=19— категоріяeroticвизначається самим кодом; для звичайного відео додатково враховується окремийis_erotic. - moderation/declined/upload використовують вужчий
ChathouseLibraryMediaType:photo=5,video=6— тут еротичність завжди лише через окремийis_eroticboolean, коди не розрізняють erotic-варіант.
- approved використовує
channel— відсутній (Chathouse не має розбивки).- Пагінація — джерело не пагінує, віддає весь список одразу; курсор емулює адаптер (
paginateArray, keyset заmediaId). Оскількиapprovedзавжди повертає photo+video разом, адаптер додатково звужує результат під потрібнийtypeвже після конвертації, перед пагінацією. - Upload — приймає лише photo/video; категорію на завантаженні не передаємо — еротичність Chathouse проставляє сам під час модерації.
- Delete — у Chathouse API немає ендпоінта видалення. «Видалення» реалізоване лише на нашій стороні:
mediaIdприховується черезTuMediaGalleryHideRepository, і приховані id виключаються зі списку ще до конвертації в наступних запитах. - Ліміт файлу — 100 МБ на завантаження.
Prime
- Джерела — три незалежні запити (photo/video/audio), кожен з власним нативним курсором апстріму — на відміну від Chathouse/Golden, адаптер прокидає цей курсор як є, без власної пагінації.
- Status — photo/video мають структурований
status.code(PrimeMediaStatus: approved / approved_by_ai / on_moderation / declined / declined_by_ai — AI-варіанти згортаються в той самий глобальний статус). Audio має інший, простіший рядковий статус (PrimeAudioStatus: approved / declined / uploading — проміжнийuploadingмапиться вon_moderation, оскільки апстрім не має окремого moderation-стану для audio). - Category — визначається тегами (
PrimeMediaTag), тільки для photo/video:erotic/adult→erotic;special_plus→special_plus;special→special;text_on_photoабо відсутність тегів → категорія відсутня. Audio тегів не має — категорія завжди відсутня. channel— відсутній.- Upload — процес різний за типом: photo синхронний (Prime одразу повертає
idPhotoі urls); video й audio — двофазні (presigned S3-форма від Prime → прямий POST файлу в S3, в обхід Prime); audio додатково має третій, non-blocking крок — фіналізація метаданих окремим запитом після завантаження в S3. - Delete — реальний ендпоінт на кожен тип (photo/video/audio).
- Ліміт файлу — не задокументовано на боці Prime.
Golden
-
Джерела — два незалежні
channel:chat(photo+video одним списком) іmail(лише photo, окремий список вкладень листів). Безchannelу запиті адаптер одразу відхиляє виклик — інакше незрозуміло, який upstream-ендпоінт кликати. -
Status — власного enum немає, лише булеве
isModerate:Golden isModerateГлобальний статус falseon_moderationtrueapproveddeclinedу Golden ніде не моделюється — запит з цим статусом завжди повертає порожній список. -
Category — Golden не має тегів/категорій взагалі, поле
categoryзавжди відсутнє. -
Розміри — у
chatкожен елемент має набірurls(blur/poster— прев’ю фото/відео,preview— середній розмір,url— оригінал/плейбек); уmail—imageUrls.smallSizeUrl/originalSizeUrl. -
Пагінація — жоден з двох каналів не має нативного курсору, обидва повертаються апстрімом одним масивом — курсор емулюється тим самим keyset-механізмом (
paginateArray), що й для Chathouse. -
Upload —
chatприймає photo і video;mail— лише photo (.jpg/.jpeg/.png). -
Delete — реальний ендпоінт на кожен канал. Апстрім не кидає HTTP-помилку при невдалому видаленні (повертає
results[].success:false) — адаптер сам перевіряє це і кидає помилку, щоб не змовчати провал.