#include "irc_service.h" #include #include #include "storage_paths.h" namespace roro { namespace { constexpr uint32_t kQuietPingMs = 240000; // no traffic for 4 min: ping the server constexpr uint32_t kDeadMs = 330000; // and give up if it still says nothing std::string hex(const uint8_t* bytes, size_t n) { std::string out; char buf[3]; for (size_t i = 0; i < n; i++) { std::snprintf(buf, sizeof(buf), "%02x", bytes[i]); out += buf; } return out; } } // namespace IrcService::IrcService(KeyValueStore& store, const std::string& defaultNick, WifiService& wifi, StorageService& storage, ClockService& clock, EventBus& bus) : config_(store), wifi_(wifi), storage_(storage), clock_(clock), bus_(bus) { config_.load(defaultNick); session_.reset(new IrcSession(config_)); } void IrcService::start() { if (task_) return; lock_ = xSemaphoreCreateMutex(); xTaskCreate(taskEntry, "irc", 6144, this, 1, &task_); // peak 4.0 KB (TLS handshake, M2) } void IrcService::connect() { stoppedByUser_ = false; if (wanted_) return; retryAtMs_ = millis(); backoff_.reset(); wanted_ = true; } void IrcService::disconnect() { stoppedByUser_ = true; stopRequested_ = true; // the IRC task says QUIT and closes; nothing to do if it never started if (!task_) wanted_ = false; } int IrcService::totalUnread() { Lock l(lock_); return session_->totalUnread(); } IrcConfig IrcService::draftConfig() { Lock l(lock_); return config_; } std::string IrcService::applyConfig(const IrcConfig& draft) { std::string error = draft.validate(); if (!error.empty()) return error; Lock l(lock_); config_.copySettingsFrom(draft); config_.save(); if (wanted_) restart_ = true; // the task reconnects with a fresh session else session_.reset(new IrcSession(config_)); return ""; } void IrcService::taskEntry(void* self) { static_cast(self)->loop(); } void IrcService::scheduleRetry(const std::string& why) { uint32_t delay = backoff_.nextDelayMs(); retryAtMs_ = millis() + delay; status_ = Status::Retrying; Lock l(lock_); session_->disconnected(clock_.utcNow(), why + ", retrying in " + std::to_string(delay / 1000) + " s"); } bool IrcService::open() { status_ = Status::Connecting; IrcConfig settings = draftConfig(); // a snapshot: the UI may apply new settings meanwhile bool pinning = settings.tls && settings.allowSelfSigned; conn_ = settings.tls ? static_cast(&tlsClient_) : &plainClient_; if (settings.tls) { tlsClient_.setTimeout(15); // seconds, for the handshake if (pinning) tlsClient_.setInsecure(); else tlsClient_.useBuiltinCACertBundle(); } if (!conn_->connect(settings.host.c_str(), settings.port)) return false; if (pinning) { uint8_t sha[32]; if (!tlsClient_.getFingerprintSHA256(sha)) { tlsClient_.stop(); return false; } std::string fingerprint = hex(sha, sizeof(sha)); if (settings.pinnedSha256.empty()) { Lock l(lock_); config_.pinnedSha256 = fingerprint; // trust on first use config_.save(); } else if (settings.pinnedSha256 != fingerprint) { tlsClient_.stop(); Lock l(lock_); session_->disconnected(clock_.utcNow(), "server certificate changed: not connecting"); return false; } } open_ = true; partial_.clear(); lastRxMs_ = millis(); pingSent_ = false; status_ = Status::Registering; Lock l(lock_); session_->connected(clock_.utcNow()); return true; } void IrcService::close(const std::string& reason) { if (open_) conn_->stop(); open_ = false; if (!reason.empty()) { Lock l(lock_); session_->disconnected(clock_.utcNow(), reason); } } void IrcService::readLines() { while (conn_->available()) { char c = static_cast(conn_->read()); if (c == '\n') { lastRxMs_ = millis(); pingSent_ = false; Lock l(lock_); session_->receive(partial_, clock_.utcNow()); partial_.clear(); } else if (c != '\r' && partial_.size() < 1024) { partial_ += c; } } } void IrcService::flushEffects() { IrcEffects fx; bool registered, quit; { Lock l(lock_); fx = session_->takeEffects(); registered = session_->registered(); quit = session_->quitRequested(); } for (auto& line : fx.send) if (open_) conn_->print((line + "\r\n").c_str()); std::string date = clock_.localDate(); std::string host; { Lock l(lock_); host = config_.host; } for (auto& entry : fx.logs) { const IrcLine& l = entry.line; std::string when = l.utc >= 0 ? ClockModel::formatLocalTime(l.utc) : "--:--"; std::string text; switch (l.kind) { case IrcLine::Kind::Action: case IrcLine::Kind::OwnAction: text = "* " + l.nick + " " + l.text; break; case IrcLine::Kind::Notice: text = "-" + l.nick + "- " + l.text; break; case IrcLine::Kind::Info: text = l.text; break; default: text = "<" + l.nick + "> " + l.text; break; } storage_.appendLine(storage::dailyLogPath({"irc", host, entry.buffer}, date), when + " " + text); } for (auto& n : fx.notifications) bus_.publish(Event::withText(EventType::Notification, n.c_str(), static_cast(NotificationLevel::Message))); if (registered && status_ == Status::Registering) { status_ = Status::Online; backoff_.reset(); } if (quit) { // /quit: QUIT is already on its way if connected; stop in any state stoppedByUser_ = true; stopRequested_ = true; quitSent_ = true; } } void IrcService::loop() { for (;;) { uint32_t now = millis(); bool wifiUp = wifi_.state() == WifiController::State::Connected; bool monitoring = wifi_.state() == WifiController::State::Monitoring; if (stopRequested_) { stopRequested_ = false; if (open_) { if (!quitSent_) conn_->print("QUIT :roro9stack\r\n"); vTaskDelay(pdMS_TO_TICKS(300)); // let QUIT reach the server close(""); } quitSent_ = false; wanted_ = false; status_ = Status::Stopped; Lock l(lock_); session_->disconnected(clock_.utcNow(), "disconnected (Enter reconnects)"); } if (restart_) { restart_ = false; if (open_) { conn_->print("QUIT :reconfiguring\r\n"); vTaskDelay(pdMS_TO_TICKS(300)); close(""); } Lock l(lock_); session_.reset(new IrcSession(config_)); retryAtMs_ = now; backoff_.reset(); } if (!wanted_) { if (open_) close("disconnected"); status_ = Status::Stopped; } else if (held_) { if (open_) { conn_->print("QUIT :updating\r\n"); vTaskDelay(pdMS_TO_TICKS(300)); close("paused while the device updates"); } status_ = Status::Paused; retryAtMs_ = now; // back at once when released } else if (!wifiUp) { if (open_) close(monitoring ? "paused for Wi-Fi monitoring" : "Wi-Fi lost"); status_ = monitoring ? Status::Paused : Status::WaitingForWifi; retryAtMs_ = now; // reconnect as soon as Wi-Fi is back } else if (!open_) { if (static_cast(now - retryAtMs_) >= 0) { if (!open()) scheduleRetry("could not connect to " + draftConfig().host); } } else if (!conn_->connected()) { close(""); scheduleRetry("connection lost"); } else { readLines(); uint32_t quiet = millis() - lastRxMs_; if (quiet > kDeadMs) { close(""); scheduleRetry("server stopped answering"); } else if (quiet > kQuietPingMs && !pingSent_) { conn_->print("PING :roro9stack\r\n"); pingSent_ = true; } } { Lock l(lock_); session_->tick(millis()); } flushEffects(); vTaskDelay(pdMS_TO_TICKS(open_ ? 30 : 250)); } } } // namespace roro