service senders mail

MailSender

Мейл сендер

src/main/senders/mail-sender.ts

Автоматична розсилка листів per-анкета. Запускається в LadyRunner.runSenders(). Використовує офіційне API (officialApiService) — окремий ендпоінт від чат-сокету.


Два треки

ТрекСтанТип інвайтуКомуДжерело кандидатів
FAVstate.mailFAVNAFNOT_ACTIVE_FAVORITES — фаворити без активного контактуfavoriteService.NOT_ACTIVE_FAVORITES
NEWstate.mailNEWFANMОнлайн RU, не фаворитиmanOnlineService.menOnline

FAV має пріоритет — перевіряється першим, NEW відправляється тільки якщо FAV не знайшов кандидатів.

Ініціалізація

  1. resetState() — очищення всіх Map, Set, лічильників
  2. Завантажити mailFAV, mailNEW — pinned invite IDs зі стану сендера
  3. getMailBlackLists()BLACK_LIST_FAV: Set<number>, BLACK_LIST_NEW: Set<number>
  4. blockListService.init() — глобальний block list анкети
  5. initHistory():
    • getMailSenderHistory() → окремо sender[] (автоматичні) і user[] (ручні)
    • sender → заповнює HISTORY_FAV / HISTORY_NEW (останній timestamp per RU) і total (лічильники сьогодні)
    • user → тільки total.USER++ (враховується для статистики)
    • INITIALIZATION_TIME = Date.now()
  6. Завантажити тексти інвайтів за ID → invites: Map<id, IMailMessage>
  7. sendSendingProgress(null) — надіслати початковий прогрес у renderer

Цикл процесу (process, кожні 6 с)

process()
 ├── Умови входу: lastUseLimit > 5 с && isNormalTime() && networkConnected()
 ├── checkInvites()          ← перевірка терміну дії інвайтів
 ├── якщо є помилки або FULL_LIMIT → пропускаємо
 ├── isSending = true  → checkMailing()   ← опитуємо статус кампанії
 └── isSending = false → tryRun()         ← шукаємо нових кандидатів

Після виконання — ourSetTimeout(6 с) → наступний цикл.

waitForPrioritySend

Якщо від останнього tryRun пройшло менше 5 с → waitForPrioritySend() чекає залишок + 6 с додатково. Використовується при ручній відправці листа оператором щоб не перетинатись з кампанією.


checkInvites — термін дії інвайтів

Якщо INITIALIZATION_TIME < початок поточного дня → новий день (програма не перезапускалась):

  • скидає FULL_LIMIT = false
  • очищає total, HISTORY_NEW, HISTORY_FAV
  • викликає initHistory() повторно

Перевірка кожного інвайту:

ТипУмова скиданняДія
NAF (FAV)dateFirstSend < сьогодні 00:00Скидає інвайт → updateMailSenderState(null)
FANM (NEW)dateFirstSend > 168 год тому (7 днів)Скидає інвайт → updateMailSenderState(null)

Після скидання оператор повинен призначити новий інвайт.


Відбір кандидатів — tryRun

  1. getCandidates() → список manIds_api[] + тип (FAV / NEW)
  2. Якщо список порожній — пропускаємо
  3. Бере текст інвайту з invites Map
  4. officialApiService.send(...) — див. нижче
  5. Відповідь MailingStatus.STARTisSending = true, lastUseLimit = now

FAV — checkNotActiveFavorites

Джерело: favoriteService.NOT_ACTIVE_FAVORITES — фаворити без активного чату.

Фільтри:

ФільтрУмова
Не в BLACK_LIST_FAV
Не в blockListServiceГлобальний block list анкети
Не отримав листа сьогодніНе в todayHistory (timestamp > 00:00)
Не в DUPLICATED
contactDetails.status !== YesНемає реального контакту
Не blockedByTU
id !== 1Системний виняток

Захист від вчорашньої розсилки: якщо остання відправка FAV-треку була вчора і від неї пройшло менше 24 годин → повертає порожній список. Не спамити фаворитів якщо вчорашня розсилка ще «свіжа».

Захист від дедлайну (23:45):

minutesUntilDeadline = (23:45 - now) / 60000
maxMenCanBeProcessed = minutesUntilDeadline × 10

Якщо ids.length > maxMenCanBeProcessed → порожній список. Якщо не встигнемо обробити до кінця дня — краще не починати.

Батч: ids.slice(0, 10) — максимум 10 RU за один send.

NEW — checkOnlineCandidates

Джерело: manOnlineService.menOnline — Set поточних онлайн.

Фільтри:

ФільтрУмова
Не в BLACK_LIST_NEW
Не в blockListService
Не є фаворитом!favoriteService.existsFavorite(id)
Не в HISTORY_NEWНе отримував листа за останній тиждень
Не в DUPLICATED
id !== 1Системний виняток

Захист квоти для FAV:

якщо NOT_ACTIVE_FAVORITES.length >= 300 - candidateIds.length - todayNewHistory.length
  → порожній список

Резервує «місце» в денному ліміті для фаворитів. Якщо накопичилось багато FAV — NEW не відправляється.

Батч: ids.slice(0, 10).


Відправка — officialApiService.send

POST /usermodule/services/agencyhelper/v2

{
  "command": "send",
  "login": "ladyId_api",
  "pass": "password",
  "list": "[manId1, manId2, ...]",
  "text": "текст інвайту",
  "attach1": "imageId1",   // опційно
  "attach2": "imageId2"    // опційно
}

Відповідь: { status: 'start' }MailingStatus.STARTisSending = true.

До 2 вкладень (attach1, attach2) — беруться з state.mailFAV.imageId1/imageId2 (конфігурує оператор через Enabled invites).

Помилки tryRun

ПомилкаДія
'your limit is end' або '7000'FULL_LIMIT = true
Bad status: 404 + total < 150ERROR = 'No access to API!'
Інша помилкаupdateError(type, message)

Опитування статусу — checkMailing

officialApiService.status(ladyId_api){ status, count, id, list[] }.

Опитується кожні 6 с поки isSending = true.

Результати per RU (MailingResult)

EnumAPI-значенняЗначенняДія
SUCCESS'true'УспіхЗберігає в БД, оновлює HISTORY + total, sendProgress
Date'Date'Успіх з датоюТе саме; timestamp з reason
ERR1'many white spaces'Забагато пробілівERROR_FAV/NEW = повідомлення, зупинка треку
ERR2'unacceptable words'Заборонені словаERROR_FAV/NEW = повідомлення, зупинка треку
ERR3'unanswered letter'Є непрочитаний лист від RUERROR_FAV/NEW + логує manId
ERR4'limit 500'Ліміт 500 листів per RUERROR_FAV/NEW = повідомлення
UNREAD'unread letters'Непрочитані листи єgetMail() → відкриває листи, додає в DUPLICATED
exception'exception'Виняток APIДодає в BLACK_LIST_FAV/NEW назавжди
Duplicated'Duplicated'Можливий дублікатДодає в DUPLICATED, ігнорується далі

Статуси кампанії (MailingStatus)

EnumAPI-значенняДія
START'start'Кампанія прийнята
PROCESSING''Чекаємо, продовжуємо опитувати
RESTART'start new'Нова кампанія поверх
LIMIT'limit'FULL_LIMIT = true, завершуємо
END'end'Завершена, isSending = false, currentCandidates = null
STOP'Stop'Зупинена вручну
TERMINATED'terminated'Примусове завершення

Логіка завершення: якщо currentCandidates.manIds_api.length === currentFinished.lengthitWasLastCheck = true.


Стоп-листи і ліміти

ЛімітТипЧасОпис
FULL_LIMITbooleanДо опівночіДенний API-ліміт. Повністю зупиняє сендер.
BLACK_LIST_FAV / BLACK_LIST_NEWSet<number>ПостійноЗавантажується при init, поповнюється при exception
DUPLICATEDnumber[]СесіяRU з дублікатами або unread — ігноруються. Очищається при resetState
lastUseLimittimestamp5 сМінімальна пауза між tryRun викликами

ERROR_FAV / ERROR_NEW

Текст помилки конкретного треку. Зупиняє тільки цей трек, інший продовжує.

Скидається коли:

  • Оператор оновлює інвайт (updateState())
  • updateError(type, null) при успіху

updateError — збереження помилки

  1. Записує в criticalLogsRepository (для DataSync)
  2. Встановлює ERROR_FAV або ERROR_NEW
  3. sendErrorStatus()reactService.updateMailSenderErrors()
  4. saveErrorToServer()apiSenderService.updateSenderError() — зберігає на сервері

Збереження результатів

Кожна успішна відправка → saveResultToDatabase()mailSenderRepository.create() → SQLite → синк через DataSyncService.

Поле reason у результаті Date — містить дату відправки від сервера. Timestamp розраховується: Date.parse(reason) - 7 * oneDayMs.


Зв’язки