Мейл сендер
src/main/senders/mail-sender.ts
Автоматична розсилка листів per-анкета. Запускається в LadyRunner.runSenders(). Використовує офіційне API (officialApiService) — окремий ендпоінт від чат-сокету.
Два треки
| Трек | Стан | Тип інвайту | Кому | Джерело кандидатів |
|---|---|---|---|---|
| FAV | state.mailFAV | NAF | NOT_ACTIVE_FAVORITES — фаворити без активного контакту | favoriteService.NOT_ACTIVE_FAVORITES |
| NEW | state.mailNEW | FANM | Онлайн RU, не фаворити | manOnlineService.menOnline |
FAV має пріоритет — перевіряється першим, NEW відправляється тільки якщо FAV не знайшов кандидатів.
Ініціалізація
resetState()— очищення всіх Map, Set, лічильників- Завантажити
mailFAV,mailNEW— pinned invite IDs зі стану сендера getMailBlackLists()→BLACK_LIST_FAV: Set<number>,BLACK_LIST_NEW: Set<number>blockListService.init()— глобальний block list анкетиinitHistory():getMailSenderHistory()→ окремоsender[](автоматичні) іuser[](ручні)sender→ заповнюєHISTORY_FAV/HISTORY_NEW(останній timestamp per RU) іtotal(лічильники сьогодні)user→ тількиtotal.USER++(враховується для статистики)INITIALIZATION_TIME = Date.now()
- Завантажити тексти інвайтів за ID →
invites: Map<id, IMailMessage> 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
getCandidates()→ списокmanIds_api[]+ тип (FAV / NEW)- Якщо список порожній — пропускаємо
- Бере текст інвайту з
invitesMap officialApiService.send(...)— див. нижче- Відповідь
MailingStatus.START→isSending = 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.START → isSending = true.
До 2 вкладень (attach1, attach2) — беруться з state.mailFAV.imageId1/imageId2 (конфігурує оператор через Enabled invites).
Помилки tryRun
| Помилка | Дія |
|---|---|
'your limit is end' або '7000' | FULL_LIMIT = true |
Bad status: 404 + total < 150 | ERROR = 'No access to API!' |
| Інша помилка | updateError(type, message) |
Опитування статусу — checkMailing
officialApiService.status(ladyId_api) → { status, count, id, list[] }.
Опитується кожні 6 с поки isSending = true.
Результати per RU (MailingResult)
| Enum | API-значення | Значення | Дія |
|---|---|---|---|
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' | Є непрочитаний лист від RU | ERROR_FAV/NEW + логує manId |
ERR4 | 'limit 500' | Ліміт 500 листів per RU | ERROR_FAV/NEW = повідомлення |
UNREAD | 'unread letters' | Непрочитані листи є | getMail() → відкриває листи, додає в DUPLICATED |
exception | 'exception' | Виняток API | Додає в BLACK_LIST_FAV/NEW назавжди |
Duplicated | 'Duplicated' | Можливий дублікат | Додає в DUPLICATED, ігнорується далі |
Статуси кампанії (MailingStatus)
| Enum | API-значення | Дія |
|---|---|---|
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.length → itWasLastCheck = true.
Стоп-листи і ліміти
| Ліміт | Тип | Час | Опис |
|---|---|---|---|
FULL_LIMIT | boolean | До опівночі | Денний API-ліміт. Повністю зупиняє сендер. |
BLACK_LIST_FAV / BLACK_LIST_NEW | Set<number> | Постійно | Завантажується при init, поповнюється при exception |
DUPLICATED | number[] | Сесія | RU з дублікатами або unread — ігноруються. Очищається при resetState |
lastUseLimit | timestamp | 5 с | Мінімальна пауза між tryRun викликами |
ERROR_FAV / ERROR_NEW
Текст помилки конкретного треку. Зупиняє тільки цей трек, інший продовжує.
Скидається коли:
- Оператор оновлює інвайт (
updateState()) updateError(type, null)при успіху
updateError — збереження помилки
- Записує в
criticalLogsRepository(для DataSync) - Встановлює
ERROR_FAVабоERROR_NEW sendErrorStatus()→reactService.updateMailSenderErrors()saveErrorToServer()→apiSenderService.updateSenderError()— зберігає на сервері
Збереження результатів
Кожна успішна відправка → saveResultToDatabase() → mailSenderRepository.create() → SQLite → синк через DataSyncService.
Поле reason у результаті Date — містить дату відправки від сервера. Timestamp розраховується: Date.parse(reason) - 7 * oneDayMs.
Зв’язки
- Ручна відправка листа: Mail
- Block list (глобальний per анкета): BlockListService
- Blacklist per інвайт: Blacklist
- Онлайн RU: ManOnlineService
- Фаворити: FavoriteService
- Синк даних: DataSyncService
- UI керування: Sender screen