11-tu-media-gallery

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 кешу не має.

Принцип: кожен 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-project if у фронті чи бекенді.
  • GetTuMediaGalleryListRequest / UploadTuMediaGalleryRequest / DeleteTuMediaGalleryRequest — завжди за одним type (і, якщо є, одним channel) за раз. Якщо джерело фізично віддає ширший набір за один виклик (Chathouse approved повертає 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 — глобальний enum approved | 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:

  1. User клікає на розділ по певному project.

  2. Відправляє запит на allows. Отримує налаштування по цьому розділу для цього проекту:

    • розбивка медіа по channel (якщо channels більше одного);
    • розбивка вікна по type медіа — photo, video, audio;
    • чи можна видаляти медіа кожного channel+type;
    • чи можна завантажувати медіа кожного channel+type, і на яку вкладку;
    • чи є розбивка по статусах і які саме (наприклад, declined відсутній на Golden).

    Ієрархія дозволів: channel → types → statuses. Підгрузка медіа реалізується по ланцюжку channel-type-status.

  3. Отримує списки TU, які за ним закріплені (operator, teamlead, top, client-manager?).

  4. Клік на TU.

  5. Підгружаємо порції медіа за дозволами: channel-type-status, а якщо channel немає — просто type-status.

Приклади трактування getAllows по проєктах:

Chathouse

  1. Один channel, без імені. Тому розбивки по chat/mail немає, відповідно одне вікно.
    • Розбивка по type медіа — photo, video.
  2. Видаляти можна.
  3. Завантаження фото/відео, без вибору категорії.
  4. Підгрузка медіа по type-status: approved, declined, on_moderation, курсором.

Prime

  1. Один channel, без імені — розбивки немає.
    • Розбивка по type медіа — photo, video, audio.
  2. Можна видаляти.
  3. Завантаження фото, відео.
  4. Підгрузка медіа по type-status: approved, declined, on_moderation, курсором.

Golden

  1. Два channel з іменем — розбивка chat / mail.
    • Розбивка по type медіа — photo, video.
  2. Можна видаляти chat / mail.
  3. Завантаження: chat — фото, відео; mail — фото.
  4. Підгрузка медіа по 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_erotic boolean, коди не розрізняють erotic-варіант.
  • 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_moderation
    trueapproved

    declined у 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) — адаптер сам перевіряє це і кидає помилку, щоб не змовчати провал.