diff --git a/lib/irc/src/irc_config.cpp b/lib/irc/src/irc_config.cpp index 6a380f1..ff8d7a4 100644 --- a/lib/irc/src/irc_config.cpp +++ b/lib/irc/src/irc_config.cpp @@ -36,7 +36,8 @@ std::string IrcConfig::validate() const { if (port < 1 || port > 65535) return "Port must be 1 to 65535"; if (!validNick(nick)) return "Nick: letters, digits and []\\`_^{|}- only, not starting with a digit"; for (auto& c : autojoin) - if (c.size() < 2 || (c[0] != '#' && c[0] != '&')) return "IRC channels start with # or &"; + if (c.channel.size() < 2 || (c.channel[0] != '#' && c.channel[0] != '&')) + return "Auto-join: IRC channels start with # or &, each followed by its key if it has one"; return ""; } @@ -56,23 +57,27 @@ std::string IrcConfig::save() { return ""; } -std::vector IrcConfig::parseChannels(const std::string& text) { - std::vector out; - std::string current; - for (char c : text + " ") { - if (c == ' ' || c == ',') { - if (!current.empty()) out.push_back(current); - current.clear(); - } else { - current += c; - } +std::vector IrcConfig::parseChannels(const std::string& text) { + std::vector out; + std::string word; + auto take = [&]() { + if (word.empty()) return; + bool channel = word[0] == '#' || word[0] == '&'; + if (!channel && !out.empty() && out.back().key.empty()) out.back().key = word; + else out.push_back({word, ""}); // a stray word fails validation as a channel + word.clear(); + }; + for (char c : text) { + if (c == ' ' || c == ',') take(); + else word += c; } + take(); return out; } -std::string IrcConfig::formatChannels(const std::vector& channels) { +std::string IrcConfig::formatChannels(const std::vector& channels) { std::string out; - for (auto& c : channels) out += (out.empty() ? "" : " ") + c; + for (auto& c : channels) out += (out.empty() ? "" : ", ") + c.channel + (c.key.empty() ? "" : " " + c.key); return out; } diff --git a/lib/irc/src/irc_config.h b/lib/irc/src/irc_config.h index 144e130..3b48b57 100644 --- a/lib/irc/src/irc_config.h +++ b/lib/irc/src/irc_config.h @@ -7,6 +7,11 @@ namespace roro { +struct IrcChannel { + std::string channel; // "#roro" + std::string key; // empty when the IRC channel has none +}; + // The one IRC server the IRC Service connects to, persisted in internal flash. class IrcConfig { public: @@ -20,7 +25,7 @@ class IrcConfig { std::string nick; std::string saslUser, saslPassword; // SASL PLAIN when both are set std::string nickservPassword; // otherwise IDENTIFY with NickServ, if set - std::vector autojoin; + std::vector autojoin; // A copy is a draft the UI can edit freely; copySettingsFrom() applies one. IrcConfig(const IrcConfig&) = default; @@ -41,8 +46,10 @@ class IrcConfig { std::string save(); // empty on success, otherwise why it was refused std::string validate() const; - static std::vector parseChannels(const std::string& text); - static std::string formatChannels(const std::vector& channels); + // "#private key, #public": a word starting with # or & is an IRC channel, the word after it + // (if it doesn't) its key. Spaces or commas separate entries, so "#a #b" also reads. + static std::vector parseChannels(const std::string& text); + static std::string formatChannels(const std::vector& channels); private: KeyValueStore& store_; diff --git a/lib/irc/src/irc_session.cpp b/lib/irc/src/irc_session.cpp index b74f5c7..3b3f9b2 100644 --- a/lib/irc/src/irc_session.cpp +++ b/lib/irc/src/irc_session.cpp @@ -37,6 +37,13 @@ std::pair firstWord(const std::string& text) { IrcSession::IrcSession(const IrcConfig& config) : config_(config), nick_(config.nick) { buffers_.push_back({config.host, IrcBuffer::Type::Server, {}, 0, false, true, ""}); + for (auto& c : config.autojoin) + if (!c.key.empty()) keys_[lower(c.channel)] = c.key; +} + +void IrcSession::tick(uint32_t nowMs) { + nowMs_ = nowMs; + if (joinsHeld_ && nowMs - heldSinceMs_ >= kNickservWaitMs) joinChannels(); } int IrcSession::totalUnread() const { @@ -108,6 +115,8 @@ void IrcSession::add(int b, IrcLine::Kind kind, const std::string& nick, const s void IrcSession::connected(int64_t) { registered_ = false; + joinsHeld_ = false; + saslFailed_ = false; quit_ = false; nick_ = config_.nick; if (!config_.saslUser.empty() && !config_.saslPassword.empty()) send("CAP REQ :sasl"); @@ -130,18 +139,49 @@ void IrcSession::onWelcome(const IrcMessage& m, int64_t utc) { registered_ = true; nick_ = m.param(0); info(0, "Connected to " + config_.host + " as " + nick_, utc); - if (!config_.nickservPassword.empty() && config_.saslUser.empty()) - send(IrcMessage::serialize("PRIVMSG", {"NickServ", "IDENTIFY " + config_.nickservPassword})); - - std::vector channels = config_.autojoin; - for (auto& c : rejoin_) - if (std::find(channels.begin(), channels.end(), c) == channels.end()) channels.push_back(c); - rejoin_.clear(); - if (!channels.empty()) { - std::string list; - for (auto& c : channels) list += (list.empty() ? "" : ",") + c; - send(IrcMessage::serialize("JOIN", {list})); + // NickServ when it's the configured login, or as the fallback when SASL failed. + // The account is named explicitly: we may be on a fallback nick if a stale session holds ours. + bool sasl = !config_.saslUser.empty() && !config_.saslPassword.empty(); + std::string account = sasl ? config_.saslUser : config_.nick; + std::string identify; + if (!config_.nickservPassword.empty() && (!sasl || saslFailed_)) identify = account + " " + config_.nickservPassword; + else if (sasl && saslFailed_) identify = account + " " + config_.saslPassword; + if (!identify.empty()) { + // IRC channels for registered users only would refuse us until NickServ has logged us in. + send(IrcMessage::serialize("PRIVMSG", {"NickServ", "IDENTIFY " + identify})); + joinsHeld_ = true; + heldSinceMs_ = nowMs_; + return; } + joinChannels(); +} + +void IrcSession::joinChannels() { + joinsHeld_ = false; + std::vector channels; + auto addOnce = [&](const std::string& c) { + for (auto& existing : channels) + if (lower(existing) == lower(c)) return; + channels.push_back(c); + }; + for (auto& c : config_.autojoin) addOnce(c.channel); + for (auto& c : rejoin_) addOnce(c); + rejoin_.clear(); + if (channels.empty()) return; + + // JOIN #keyed,#open key: IRC pairs keys with the first channels listed. + std::string keyed, open, keys; + for (auto& c : channels) { + auto k = keys_.find(lower(c)); + if (k != keys_.end()) { + keyed += (keyed.empty() ? "" : ",") + c; + keys += (keys.empty() ? "" : ",") + k->second; + } else { + open += (open.empty() ? "" : ",") + c; + } + } + std::string list = keyed + (!keyed.empty() && !open.empty() ? "," : "") + open; + send(keys.empty() ? IrcMessage::serialize("JOIN", {list}) : IrcMessage::serialize("JOIN", {list, keys})); } void IrcSession::receive(const std::string& raw, int64_t utc) { @@ -163,11 +203,17 @@ void IrcSession::receive(const std::string& raw, int64_t utc) { } else if (cmd == "AUTHENTICATE" && m.param(0) == "+") { std::string user = config_.saslUser; send("AUTHENTICATE " + base64Encode(user + '\0' + user + '\0' + config_.saslPassword)); + } else if (cmd == "900") { + // Logged in. Take our nick back from a stale session, then join. + if (lower(nick_) != lower(config_.nick)) + send(IrcMessage::serialize("PRIVMSG", {"NickServ", "REGAIN " + config_.nick})); + if (joinsHeld_) joinChannels(); } else if (cmd == "903") { info(0, "SASL login succeeded", utc); send("CAP END"); } else if (cmd == "904" || cmd == "905" || cmd == "906" || cmd == "902") { - info(0, "SASL login failed: " + m.param(m.params.size() - 1), utc); + info(0, "SASL login failed: " + m.param(m.params.size() - 1) + " (trying NickServ instead)", utc); + saslFailed_ = true; send("CAP END"); } else if (cmd == "PRIVMSG" || cmd == "NOTICE") { onPrivmsg(m, utc, cmd == "NOTICE"); @@ -284,6 +330,7 @@ void IrcSession::command(int b, const std::string& text, int64_t utc) { auto [channel, key] = firstWord(rest); if (channel.empty()) return info(b, "Usage: /join #channel", utc); if (!isChannel(channel)) channel = "#" + channel; + if (!key.empty()) keys_[lower(channel)] = key; // reused when rejoining send(key.empty() ? IrcMessage::serialize("JOIN", {channel}) : IrcMessage::serialize("JOIN", {channel, key})); } else if (verb == "part") { std::string channel = inChannel ? buf.name : ""; diff --git a/lib/irc/src/irc_session.h b/lib/irc/src/irc_session.h index 7d2aba6..2724414 100644 --- a/lib/irc/src/irc_session.h +++ b/lib/irc/src/irc_session.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -53,6 +54,12 @@ class IrcSession { explicit IrcSession(const IrcConfig& config); + // Joins wait this long for NickServ to confirm a login before going ahead anyway. + static constexpr uint32_t kNickservWaitMs = 2000; + + // Uptime in ms, called regularly: releases joins held back for NickServ. + void tick(uint32_t nowMs); + void connected(int64_t utc); void disconnected(int64_t utc, const std::string& reason); void receive(const std::string& raw, int64_t utc); @@ -83,6 +90,7 @@ class IrcSession { bool mentionsMe(const std::string& text) const; void onPrivmsg(const IrcMessage& m, int64_t utc, bool notice); void onWelcome(const IrcMessage& m, int64_t utc); + void joinChannels(); void command(int buffer, const std::string& text, int64_t utc); void say(int buffer, const std::string& text, int64_t utc, bool action); @@ -94,6 +102,11 @@ class IrcSession { int viewing_ = -1; std::set namesRequested_; std::vector rejoin_; // IRC channels to join again after a reconnect + std::map keys_; // lower-case IRC channel -> key, from config and /join + bool joinsHeld_ = false; // waiting for NickServ before joining + bool saslFailed_ = false; + uint32_t heldSinceMs_ = 0; + uint32_t nowMs_ = 0; IrcEffects effects_; uint32_t revision_ = 0; }; diff --git a/src/apps/irc_app.cpp b/src/apps/irc_app.cpp index c36c8cf..d37bd6f 100644 --- a/src/apps/irc_app.cpp +++ b/src/apps/irc_app.cpp @@ -294,7 +294,7 @@ void IrcApp::drawSettings(Canvas& c) { c.setTextColor(theme::kMuted); c.drawString(fieldLabel(fields_.selected()).c_str(), 4, area.y + 4); widgets::lineEditor(c, fieldEditor_, {4, area.y + 22, area.w - 8, 0}); - c.drawString(fields_.selected() == kAutojoin ? "e.g. #roro #meshtastic" : "Enter: OK `: cancel", 4, + c.drawString(fields_.selected() == kAutojoin ? "e.g. #roro, #private key" : "Enter: OK `: cancel", 4, area.y + 44); return; } diff --git a/src/main.cpp b/src/main.cpp index 8dc1b21..567ebba 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -193,11 +193,15 @@ static void serialCommands() { if (line == "irc dump") { Serial.printf("irc: status %d, unread %d, heap %u min %u\n", (int)irc->status(), irc->totalUnread(), ESP.getFreeHeap(), ESP.getMinFreeHeap()); + IrcConfig c = irc->draftConfig(); // which login is configured, never the secrets + Serial.printf("irc: nick %s, sasl %s, nickserv %s, autojoin %u channels\n", c.nick.c_str(), + c.saslUser.empty() || c.saslPassword.empty() ? "off" : "on", + c.nickservPassword.empty() ? "off" : "on", (unsigned)c.autojoin.size()); irc->withSession([](IrcSession& s) { for (int i = 0; i < s.bufferCount(); i++) { const auto& b = s.buffer(i); Serial.printf("irc buffer %d %s unread %d%s\n", i, b.name.c_str(), b.unread, b.joined ? " joined" : ""); - size_t from = b.lines.size() > 6 ? b.lines.size() - 6 : 0; + size_t from = 0; for (size_t j = from; j < b.lines.size(); j++) Serial.printf(" <%s> %s\n", b.lines[j].nick.c_str(), b.lines[j].text.c_str()); } diff --git a/src/services/irc_service.cpp b/src/services/irc_service.cpp index d69d23f..64e87e1 100644 --- a/src/services/irc_service.cpp +++ b/src/services/irc_service.cpp @@ -229,6 +229,10 @@ void IrcService::loop() { pingSent_ = true; } } + { + Lock l(lock_); + session_->tick(millis()); + } flushEffects(); vTaskDelay(pdMS_TO_TICKS(open_ ? 30 : 250)); } diff --git a/test/test_irc_config/test_irc_config.cpp b/test/test_irc_config/test_irc_config.cpp index 5773032..6286b9a 100644 --- a/test/test_irc_config/test_irc_config.cpp +++ b/test/test_irc_config/test_irc_config.cpp @@ -28,7 +28,7 @@ void test_save_and_reload() { c.nick = "clement"; c.saslUser = "clement"; c.saslPassword = "s3cret"; - c.autojoin = {"#roro", "#meshtastic"}; + c.autojoin = {{"#roro", ""}, {"#meshtastic", "k3y"}}; TEST_ASSERT_TRUE(c.save().empty()); } IrcConfig again(store); @@ -36,7 +36,8 @@ void test_save_and_reload() { TEST_ASSERT_EQUAL_STRING("clement", again.nick.c_str()); TEST_ASSERT_EQUAL_STRING("s3cret", again.saslPassword.c_str()); TEST_ASSERT_EQUAL(2, again.autojoin.size()); - TEST_ASSERT_EQUAL_STRING("#meshtastic", again.autojoin[1].c_str()); + TEST_ASSERT_EQUAL_STRING("#meshtastic", again.autojoin[1].channel.c_str()); + TEST_ASSERT_EQUAL_STRING("k3y", again.autojoin[1].key.c_str()); } void test_invalid_values_are_refused_with_a_reason() { @@ -54,14 +55,32 @@ void test_invalid_values_are_refused_with_a_reason() { c.host = ""; TEST_ASSERT_FALSE(c.save().empty()); c.host = "irc.libera.chat"; - c.autojoin = {"roro"}; // IRC channels start with # or & + c.autojoin = {{"roro", ""}}; // IRC channels start with # or & TEST_ASSERT_FALSE(c.save().empty()); } void test_autojoin_text_round_trip() { TEST_ASSERT_EQUAL(2, IrcConfig::parseChannels("#a, #b").size()); - TEST_ASSERT_EQUAL(2, IrcConfig::parseChannels("#a #b").size()); - TEST_ASSERT_EQUAL_STRING("#a #b", IrcConfig::formatChannels({"#a", "#b"}).c_str()); + TEST_ASSERT_EQUAL(2, IrcConfig::parseChannels("#a #b").size()); // the older, space-separated form + TEST_ASSERT_EQUAL_STRING("#a, #b", IrcConfig::formatChannels({{"#a", ""}, {"#b", ""}}).c_str()); +} + +void test_autojoin_entries_can_carry_a_key() { + auto list = IrcConfig::parseChannels("#private s3cret, #public &local"); + TEST_ASSERT_EQUAL(3, list.size()); + TEST_ASSERT_EQUAL_STRING("#private", list[0].channel.c_str()); + TEST_ASSERT_EQUAL_STRING("s3cret", list[0].key.c_str()); + TEST_ASSERT_EQUAL_STRING("", list[1].key.c_str()); + TEST_ASSERT_EQUAL_STRING("&local", list[2].channel.c_str()); + TEST_ASSERT_EQUAL_STRING("#private s3cret, #public, &local", IrcConfig::formatChannels(list).c_str()); +} + +void test_a_key_without_a_channel_is_refused() { + MemoryStore store; + IrcConfig c(store); + c.load("x"); + c.autojoin = IrcConfig::parseChannels("orphan #a"); + TEST_ASSERT_FALSE(c.validate().empty()); } void test_copy_settings_from_a_draft() { @@ -70,7 +89,7 @@ void test_copy_settings_from_a_draft() { live.load("x"); IrcConfig draft = live; draft.host = "irc.example.org"; - draft.autojoin = {"#a"}; + draft.autojoin = {{"#a", ""}}; TEST_ASSERT_EQUAL_STRING("irc.libera.chat", live.host.c_str()); // the draft is independent live.copySettingsFrom(draft); TEST_ASSERT_EQUAL_STRING("irc.example.org", live.host.c_str()); @@ -84,5 +103,7 @@ int main() { RUN_TEST(test_invalid_values_are_refused_with_a_reason); RUN_TEST(test_autojoin_text_round_trip); RUN_TEST(test_copy_settings_from_a_draft); + RUN_TEST(test_autojoin_entries_can_carry_a_key); + RUN_TEST(test_a_key_without_a_channel_is_refused); return UNITY_END(); } diff --git a/test/test_irc_session/test_irc_session.cpp b/test/test_irc_session/test_irc_session.cpp index fcdbd7b..a44e303 100644 --- a/test/test_irc_session/test_irc_session.cpp +++ b/test/test_irc_session/test_irc_session.cpp @@ -20,7 +20,7 @@ struct Fixture { explicit Fixture(bool sasl = false) { config.load("roro"); config.nick = "roro"; - config.autojoin = {"#roro"}; + config.autojoin = {{"#roro", ""}}; if (sasl) { config.saslUser = "acct"; config.saslPassword = "pw"; @@ -28,6 +28,7 @@ struct Fixture { session.reset(new IrcSession(config)); } void recv(const std::string& line) { session->receive(line, 1000); } + void tick(uint32_t ms) { session->tick(ms); } IrcEffects& take() { fx = session->takeEffects(); return fx; @@ -65,15 +66,127 @@ void test_welcome_registers_and_joins_autojoin_channels() { TEST_ASSERT_TRUE(f.sent("JOIN #roro")); } -void test_nickserv_identify_after_welcome() { +void test_nickserv_identify_after_welcome_and_joins_wait_for_it() { Fixture f; f.config.nickservPassword = "pw"; f.session.reset(new IrcSession(f.config)); - f.registerNow(); + f.session->connected(0); + f.tick(0); + f.recv(":srv 001 roro :Welcome"); + f.take(); + TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY roro pw")); + TEST_ASSERT_FALSE(f.sent("JOIN #roro")); // not before NickServ has had its say + f.tick(1000); + TEST_ASSERT_TRUE(f.take().send.empty()); +} + +void test_joins_follow_the_logged_in_reply() { + Fixture f; + f.config.nickservPassword = "pw"; + f.session.reset(new IrcSession(f.config)); + f.session->connected(0); + f.tick(0); + f.recv(":srv 001 roro :Welcome"); + f.take(); + f.tick(300); + f.recv(":srv 900 roro roro!u@h roro :You are now logged in as roro"); + f.take(); + TEST_ASSERT_TRUE(f.sent("JOIN #roro")); + f.tick(5000); + TEST_ASSERT_TRUE(f.take().send.empty()); // and only once +} + +void test_joins_go_ahead_after_two_seconds_without_a_reply() { + Fixture f; + f.config.nickservPassword = "pw"; + f.session.reset(new IrcSession(f.config)); + f.session->connected(0); + f.tick(10000); + f.recv(":srv 001 roro :Welcome"); + f.take(); + f.tick(11999); + TEST_ASSERT_TRUE(f.take().send.empty()); + f.tick(12000); + f.take(); + TEST_ASSERT_TRUE(f.sent("JOIN #roro")); +} + +void test_failed_sasl_falls_back_to_nickserv_and_holds_joins() { + Fixture f(true); + f.config.nickservPassword = "nspw"; + f.session.reset(new IrcSession(f.config)); + f.session->connected(0); + f.recv(":srv CAP * ACK :sasl"); + f.recv("AUTHENTICATE +"); + f.recv(":srv 904 roro :SASL authentication failed"); + f.recv(":srv 001 roro :Welcome"); + f.take(); + TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY acct nspw")); + TEST_ASSERT_FALSE(f.sent("JOIN #roro")); + f.recv(":srv 900 roro roro!u@h roro :You are now logged in as roro"); + f.take(); + TEST_ASSERT_TRUE(f.sent("JOIN #roro")); +} + +void test_failed_sasl_without_nickserv_password_identifies_with_the_sasl_account() { + Fixture f(true); // SASL user "acct", password "pw" + f.session->connected(0); + f.recv(":srv 904 roro :SASL authentication failed"); + f.recv(":srv 001 roro :Welcome"); + f.take(); + TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY acct pw")); +} + +void test_successful_sasl_joins_at_once() { + Fixture f(true); + f.config.nickservPassword = "nspw"; + f.session.reset(new IrcSession(f.config)); + f.session->connected(0); + f.recv(":srv 903 roro :SASL authentication successful"); + f.recv(":srv 001 roro :Welcome"); + f.take(); + TEST_ASSERT_TRUE(f.sent("JOIN #roro")); + TEST_ASSERT_FALSE(f.sent("PRIVMSG NickServ :IDENTIFY acct nspw")); +} + +void test_identify_names_the_account_even_on_a_fallback_nick_then_regains_it() { + Fixture f; + f.config.nickservPassword = "pw"; + f.session.reset(new IrcSession(f.config)); + f.session->connected(0); + f.recv(":srv 433 * roro :Nickname is already in use"); // a stale session still holds it + f.recv(":srv 001 roro_ :Welcome"); + f.take(); + TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY roro pw")); + f.recv(":srv 900 roro_ roro_!u@h roro :You are now logged in as roro"); + f.take(); + TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :REGAIN roro")); + TEST_ASSERT_TRUE(f.sent("JOIN #roro")); +} + +void test_autojoin_sends_keyed_channels_first_with_their_keys() { + Fixture f; + f.config.autojoin = IrcConfig::parseChannels("#public, #private s3cret"); + f.session.reset(new IrcSession(f.config)); f.session->connected(0); f.recv(":srv 001 roro :Welcome"); f.take(); - TEST_ASSERT_TRUE(f.sent("PRIVMSG NickServ :IDENTIFY pw")); + TEST_ASSERT_TRUE(f.sent("JOIN #private,#public s3cret")); +} + +void test_keys_used_with_join_are_reused_on_rejoin() { + Fixture f; + f.registerNow(); + f.recv(":roro!u@h JOIN #roro"); + f.session->input(1, "/join #vault k3y", 0); + f.take(); + TEST_ASSERT_TRUE(f.sent("JOIN #vault k3y")); + f.recv(":roro!u@h JOIN #vault"); + f.session->disconnected(0, "Wi-Fi lost"); + f.session->connected(0); + f.recv(":srv 001 roro :Welcome"); + f.take(); + TEST_ASSERT_TRUE(f.sent("JOIN #vault,#roro k3y")); } void test_sasl_plain_flow() { @@ -347,7 +460,15 @@ int main() { UNITY_BEGIN(); RUN_TEST(test_plain_registration_sends_nick_and_user); RUN_TEST(test_welcome_registers_and_joins_autojoin_channels); - RUN_TEST(test_nickserv_identify_after_welcome); + RUN_TEST(test_nickserv_identify_after_welcome_and_joins_wait_for_it); + RUN_TEST(test_joins_follow_the_logged_in_reply); + RUN_TEST(test_joins_go_ahead_after_two_seconds_without_a_reply); + RUN_TEST(test_failed_sasl_falls_back_to_nickserv_and_holds_joins); + RUN_TEST(test_failed_sasl_without_nickserv_password_identifies_with_the_sasl_account); + RUN_TEST(test_successful_sasl_joins_at_once); + RUN_TEST(test_identify_names_the_account_even_on_a_fallback_nick_then_regains_it); + RUN_TEST(test_autojoin_sends_keyed_channels_first_with_their_keys); + RUN_TEST(test_keys_used_with_join_are_reused_on_rejoin); RUN_TEST(test_sasl_plain_flow); RUN_TEST(test_sasl_failure_still_ends_negotiation_and_says_so); RUN_TEST(test_nick_in_use_during_registration_tries_another);