fix: don't use same websocket connection for multiple accounts (#6348)

This commit is contained in:
pajlada
2025-08-10 11:57:14 +02:00
committed by GitHub
parent 1c9090d350
commit 4b6ba68489
12 changed files with 180 additions and 16 deletions
+1
View File
@@ -52,6 +52,7 @@
- Bugfix: Fixed some minor typos. (#6196) - Bugfix: Fixed some minor typos. (#6196)
- Bugfix: Fixed inconsistent spaces in messages when using fractional scaling. (#6231, #6254) - Bugfix: Fixed inconsistent spaces in messages when using fractional scaling. (#6231, #6254)
- Bugfix: Fixed eventsub message delete notifications not being affected by "Show deletions of single messages". (#6233) - Bugfix: Fixed eventsub message delete notifications not being affected by "Show deletions of single messages". (#6233)
- Bugfix: Fixed an issue where eventsub subscriptions might fail after changing user. (#6348)
- Bugfix: Don't add reply buttons to messages that are invalid reply targets. (#6119) - Bugfix: Don't add reply buttons to messages that are invalid reply targets. (#6119)
- Bugfix: Fixed invalid commands from being forwarded to Helix, making it possible for information to leak (e.g. if you typed `/bann username ban reason` it would be seen by others in chat as `username ban reason`). (#6272, #6330) - Bugfix: Fixed invalid commands from being forwarded to Helix, making it possible for information to leak (e.g. if you typed `/bann username ban reason` it would be seen by others in chat as `username ban reason`). (#6272, #6330)
- Bugfix: Emotes that failed to load their images now show as text. (#6355) - Bugfix: Emotes that failed to load their images now show as text. (#6355)
@@ -471,6 +471,8 @@ CommandController::CommandController(const Paths &paths)
this->registerCommand("/debug-invalidate-buffers", this->registerCommand("/debug-invalidate-buffers",
&commands::invalidateBuffers); &commands::invalidateBuffers);
this->registerCommand("/debug-eventsub", &commands::eventsub);
this->registerCommand("/debug-test", &commands::debugTest); this->registerCommand("/debug-test", &commands::debugTest);
this->registerCommand("/shield", &commands::shieldModeOn); this->registerCommand("/shield", &commands::shieldModeOn);
@@ -10,6 +10,7 @@
#include "messages/Message.hpp" #include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp" #include "messages/MessageBuilder.hpp"
#include "messages/MessageElement.hpp" #include "messages/MessageElement.hpp"
#include "providers/twitch/eventsub/Controller.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Theme.hpp" #include "singletons/Theme.hpp"
@@ -158,6 +159,12 @@ QString invalidateBuffers(const CommandContext & /*ctx*/)
return {}; return {};
} }
QString eventsub(const CommandContext & /*ctx*/)
{
getApp()->getEventSub()->debug();
return {};
}
QString debugTest(const CommandContext &ctx) QString debugTest(const CommandContext &ctx)
{ {
if (!ctx.channel) if (!ctx.channel)
@@ -28,6 +28,8 @@ QString incrementImageGeneration(const CommandContext &ctx);
QString invalidateBuffers(const CommandContext &ctx); QString invalidateBuffers(const CommandContext &ctx);
QString eventsub(const CommandContext &ctx);
QString debugTest(const CommandContext &ctx); QString debugTest(const CommandContext &ctx);
} // namespace chatterino::commands } // namespace chatterino::commands
@@ -342,6 +342,9 @@ void TwitchAccountManager::load()
this->currentUsername.connect([this](const QString &newUsername) { this->currentUsername.connect([this](const QString &newUsername) {
auto user = this->findUserByUsername(newUsername); auto user = this->findUserByUsername(newUsername);
this->currentUserAboutToChange.invoke(this->currentUser_, user);
if (user) if (user)
{ {
qCDebug(chatterinoTwitch) qCDebug(chatterinoTwitch)
@@ -55,7 +55,15 @@ public:
pajlada::Settings::Setting<QString> currentUsername{"/accounts/current", pajlada::Settings::Setting<QString> currentUsername{"/accounts/current",
""}; ""};
// pajlada::Signals::NoArgSignal currentUserChanged;
/// This signal fires after we've figured out what the new account is, but before
/// any updates to Helix have been made.
///
/// Useful for scenarios where you have to call Helix using the previous account.
pajlada::Signals::Signal<std::shared_ptr<TwitchAccount>,
std::shared_ptr<TwitchAccount>>
currentUserAboutToChange;
boost::signals2::signal<void()> currentUserChanged; boost::signals2::signal<void()> currentUserChanged;
pajlada::Signals::NoArgSignal userListUpdated; pajlada::Signals::NoArgSignal userListUpdated;
+28 -7
View File
@@ -125,6 +125,18 @@ TwitchChannel::TwitchChannel(const QString &name)
{ {
qCDebug(chatterinoTwitch) << "[TwitchChannel" << name << "] Opened"; qCDebug(chatterinoTwitch) << "[TwitchChannel" << name << "] Opened";
this->signalHolder_.managedConnect(
getApp()->getAccounts()->twitch.currentUserAboutToChange,
[this](const auto & /*oldAccount*/, const auto & /*newAccount*/) {
this->eventSubChannelChatUserMessageHoldHandle.reset();
this->eventSubChannelChatUserMessageUpdateHandle.reset();
this->eventSubChannelModerateHandle.reset();
this->eventSubAutomodMessageHoldHandle.reset();
this->eventSubAutomodMessageUpdateHandle.reset();
this->eventSubSuspiciousUserMessageHandle.reset();
this->eventSubSuspiciousUserUpdateHandle.reset();
});
this->bSignals_.emplace_back( this->bSignals_.emplace_back(
getApp()->getAccounts()->twitch.currentUserChanged.connect([this] { getApp()->getAccounts()->twitch.currentUserChanged.connect([this] {
this->setMod(false); this->setMod(false);
@@ -1547,12 +1559,15 @@ void TwitchChannel::refreshPubSub()
auto currentAccount = getApp()->getAccounts()->twitch.getCurrent(); auto currentAccount = getApp()->getAccounts()->twitch.getCurrent();
const auto &currentTwitchUserID = currentAccount->getUserId();
if (this->hasModRights()) if (this->hasModRights())
{ {
this->eventSubChannelModerateHandle = this->eventSubChannelModerateHandle =
getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{
.subscriptionType = "channel.moderate", .subscriptionType = "channel.moderate",
.subscriptionVersion = "2", .subscriptionVersion = "2",
.ownerTwitchUserID = currentTwitchUserID,
.conditions = .conditions =
{ {
{ {
@@ -1561,7 +1576,7 @@ void TwitchChannel::refreshPubSub()
}, },
{ {
"moderator_user_id", "moderator_user_id",
currentAccount->getUserId(), currentTwitchUserID,
}, },
}, },
}); });
@@ -1569,6 +1584,7 @@ void TwitchChannel::refreshPubSub()
getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{
.subscriptionType = "automod.message.hold", .subscriptionType = "automod.message.hold",
.subscriptionVersion = "2", .subscriptionVersion = "2",
.ownerTwitchUserID = currentTwitchUserID,
.conditions = .conditions =
{ {
{ {
@@ -1577,7 +1593,7 @@ void TwitchChannel::refreshPubSub()
}, },
{ {
"moderator_user_id", "moderator_user_id",
currentAccount->getUserId(), currentTwitchUserID,
}, },
}, },
}); });
@@ -1585,6 +1601,7 @@ void TwitchChannel::refreshPubSub()
getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{
.subscriptionType = "automod.message.update", .subscriptionType = "automod.message.update",
.subscriptionVersion = "2", .subscriptionVersion = "2",
.ownerTwitchUserID = currentTwitchUserID,
.conditions = .conditions =
{ {
{ {
@@ -1593,7 +1610,7 @@ void TwitchChannel::refreshPubSub()
}, },
{ {
"moderator_user_id", "moderator_user_id",
currentAccount->getUserId(), currentTwitchUserID,
}, },
}, },
}); });
@@ -1601,6 +1618,7 @@ void TwitchChannel::refreshPubSub()
getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{
.subscriptionType = "channel.suspicious_user.message", .subscriptionType = "channel.suspicious_user.message",
.subscriptionVersion = "1", .subscriptionVersion = "1",
.ownerTwitchUserID = currentTwitchUserID,
.conditions = .conditions =
{ {
{ {
@@ -1609,7 +1627,7 @@ void TwitchChannel::refreshPubSub()
}, },
{ {
"moderator_user_id", "moderator_user_id",
currentAccount->getUserId(), currentTwitchUserID,
}, },
}, },
}); });
@@ -1617,6 +1635,7 @@ void TwitchChannel::refreshPubSub()
getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{
.subscriptionType = "channel.suspicious_user.update", .subscriptionType = "channel.suspicious_user.update",
.subscriptionVersion = "1", .subscriptionVersion = "1",
.ownerTwitchUserID = currentTwitchUserID,
.conditions = .conditions =
{ {
{ {
@@ -1625,7 +1644,7 @@ void TwitchChannel::refreshPubSub()
}, },
{ {
"moderator_user_id", "moderator_user_id",
currentAccount->getUserId(), currentTwitchUserID,
}, },
}, },
}); });
@@ -1645,6 +1664,7 @@ void TwitchChannel::refreshPubSub()
getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{
.subscriptionType = "channel.chat.user_message_hold", .subscriptionType = "channel.chat.user_message_hold",
.subscriptionVersion = "1", .subscriptionVersion = "1",
.ownerTwitchUserID = currentTwitchUserID,
.conditions = .conditions =
{ {
{ {
@@ -1653,7 +1673,7 @@ void TwitchChannel::refreshPubSub()
}, },
{ {
"user_id", "user_id",
currentAccount->getUserId(), currentTwitchUserID,
}, },
}, },
}); });
@@ -1662,6 +1682,7 @@ void TwitchChannel::refreshPubSub()
getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{
.subscriptionType = "channel.chat.user_message_update", .subscriptionType = "channel.chat.user_message_update",
.subscriptionVersion = "1", .subscriptionVersion = "1",
.ownerTwitchUserID = currentTwitchUserID,
.conditions = .conditions =
{ {
{ {
@@ -1670,7 +1691,7 @@ void TwitchChannel::refreshPubSub()
}, },
{ {
"user_id", "user_id",
currentAccount->getUserId(), currentTwitchUserID,
}, },
}, },
}); });
@@ -9,6 +9,7 @@
#include "providers/twitch/eventsub/Controller.hpp" #include "providers/twitch/eventsub/Controller.hpp"
#include "providers/twitch/eventsub/MessageBuilder.hpp" #include "providers/twitch/eventsub/MessageBuilder.hpp"
#include "providers/twitch/eventsub/MessageHandlers.hpp" #include "providers/twitch/eventsub/MessageHandlers.hpp"
#include "providers/twitch/PubSubManager.hpp"
#include "providers/twitch/TwitchBadge.hpp" #include "providers/twitch/TwitchBadge.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
@@ -416,12 +417,42 @@ bool Connection::isSubscribedTo(const SubscriptionRequest &request) const
void Connection::markRequestSubscribed(const SubscriptionRequest &request) void Connection::markRequestSubscribed(const SubscriptionRequest &request)
{ {
assert((this->twitchUserID.isEmpty() ||
this->twitchUserID == request.ownerTwitchUserID) &&
"A subscription was made when another user's subscriptions were "
"still active");
this->twitchUserID = request.ownerTwitchUserID;
this->subscriptions.emplace(request); this->subscriptions.emplace(request);
} }
void Connection::markRequestUnsubscribed(const SubscriptionRequest &request) void Connection::markRequestUnsubscribed(const SubscriptionRequest &request)
{ {
this->subscriptions.erase(request); this->subscriptions.erase(request);
if (this->subscriptions.empty())
{
// TODO: Verify that it's fine for us to reuse a connection for another
// user after all old subscriptions are gone
this->twitchUserID.clear();
}
}
bool Connection::canHandleSubscriptionFrom(
const QString &otherTwitchUserID) const
{
return this->twitchUserID.isEmpty() ||
this->twitchUserID == otherTwitchUserID;
}
void Connection::debug()
{
for (const auto &request : this->subscriptions)
{
qCInfo(LOG).noquote().nospace()
<< this->getSessionID() << " -> " << request;
}
} }
} // namespace chatterino::eventsub } // namespace chatterino::eventsub
@@ -90,9 +90,17 @@ public:
void markRequestSubscribed(const SubscriptionRequest &request); void markRequestSubscribed(const SubscriptionRequest &request);
void markRequestUnsubscribed(const SubscriptionRequest &request); void markRequestUnsubscribed(const SubscriptionRequest &request);
bool canHandleSubscriptionFrom(const QString &otherTwitchUserID) const;
void debug();
private: private:
QString sessionID; QString sessionID;
/// The Twitch User ID that's attached to all subscriptions of this connection.
/// If no subscriptions are attached yet, this is an empty string, meaning it's open for any subscription.
QString twitchUserID;
std::unordered_set<SubscriptionRequest> subscriptions; std::unordered_set<SubscriptionRequest> subscriptions;
}; };
+73 -7
View File
@@ -6,6 +6,7 @@
#include "common/Version.hpp" #include "common/Version.hpp"
#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/eventsub/Connection.hpp" #include "providers/twitch/eventsub/Connection.hpp"
#include "util/QMagicEnum.hpp"
#include "util/RenameThread.hpp" #include "util/RenameThread.hpp"
#include <boost/asio/io_context.hpp> #include <boost/asio/io_context.hpp>
@@ -169,7 +170,7 @@ void Controller::removeRef(const SubscriptionRequest &request)
<< "Refcount fell to zero for" << request << "Refcount fell to zero for" << request
<< "but we had no subscription ID attached - a " << "but we had no subscription ID attached - a "
"successful subscription was never made. From state " "successful subscription was never made. From state "
<< static_cast<uint8_t>(subscription.state); << qmagicenum::enumName(subscription.state);
return; return;
} }
@@ -203,6 +204,9 @@ SubscriptionHandle Controller::subscribe(const SubscriptionRequest &request)
assert(!this->quitting && assert(!this->quitting &&
"Subscribe cannot be called while we are quitting"); "Subscribe cannot be called while we are quitting");
assert(!request.ownerTwitchUserID.isEmpty() &&
"Subscription requests must include a Twitch User ID");
bool needToSubscribe = false; bool needToSubscribe = false;
{ {
@@ -251,7 +255,7 @@ SubscriptionHandle Controller::subscribe(const SubscriptionRequest &request)
subscription.refCount++; subscription.refCount++;
qCDebug(LOG) << "Added ref for" << request << subscription.refCount qCDebug(LOG) << "Added ref for" << request << subscription.refCount
<< needToSubscribe << needToSubscribe
<< "state:" << static_cast<uint8_t>(subscription.state); << "state:" << qmagicenum::enumName(subscription.state);
} }
auto handle = std::make_unique<RawSubscriptionHandle>(request); auto handle = std::make_unique<RawSubscriptionHandle>(request);
@@ -314,6 +318,59 @@ void Controller::reconnectConnection(
} }
} }
void Controller::debug()
{
std::lock_guard g(this->subscriptionsMutex);
for (const auto &[request, subscription] : this->subscriptions)
{
QString sessionID;
auto connection = subscription.connection.lock();
if (connection)
{
auto *connection2 =
dynamic_cast<Connection *>(connection->getListener());
if (connection2)
{
sessionID = connection2->getSessionID();
}
else
{
sessionID = "BAD";
}
}
else
{
sessionID = "DEAD";
}
qCInfo(LOG).noquote().nospace()
<< request << " (" << qmagicenum::enumName(subscription.state)
<< ") -> " << sessionID;
}
boost::asio::post(this->ioContext, [this] {
for (const auto &weakConnection : this->connections)
{
auto connection = weakConnection.lock();
if (connection)
{
auto *connection2 =
dynamic_cast<Connection *>(connection->getListener());
if (connection2)
{
// qCInfo(LOG)
// << "Connected to" << connection2->getSessionID();
connection2->debug();
}
}
else
{
qCInfo(LOG) << "Dead connection";
}
}
});
}
void Controller::subscribe(const SubscriptionRequest &request, bool isRetry) void Controller::subscribe(const SubscriptionRequest &request, bool isRetry)
{ {
// 1. Flush dead connections (maybe this should not be done here) // 1. Flush dead connections (maybe this should not be done here)
@@ -341,8 +398,8 @@ void Controller::subscribe(const SubscriptionRequest &request, bool isRetry)
uint32_t openButNotReadyConnections = 0; uint32_t openButNotReadyConnections = 0;
// 2. Check if any currently open connection can handle this subscription // 2. Check if any currently open connection can handle this subscription
auto viableConnection = auto viableConnection = this->getViableConnection(
this->getViableConnection(openButNotReadyConnections); request.ownerTwitchUserID, openButNotReadyConnections);
if (viableConnection.has_value()) if (viableConnection.has_value())
{ {
@@ -436,7 +493,7 @@ void Controller::subscribe(const SubscriptionRequest &request, bool isRetry)
} }
std::optional<std::shared_ptr<lib::Session>> Controller::getViableConnection( std::optional<std::shared_ptr<lib::Session>> Controller::getViableConnection(
uint32_t &openButNotReadyConnections) const QString &ownerTwitchUserID, uint32_t &openButNotReadyConnections)
{ {
for (const auto &weakConnection : this->connections) for (const auto &weakConnection : this->connections)
{ {
@@ -461,6 +518,11 @@ std::optional<std::shared_ptr<lib::Session>> Controller::getViableConnection(
continue; continue;
} }
if (!listener->canHandleSubscriptionFrom(ownerTwitchUserID))
{
continue; // Connection is active with another Twitch User's subscriptions
}
// TODO: Check if this listener has room for another subscription // TODO: Check if this listener has room for another subscription
return connection; return connection;
@@ -536,9 +598,12 @@ void Controller::retrySubscription(const SubscriptionRequest &request)
qCDebug(LOG) << "No one is interested in this subscription anymore, " qCDebug(LOG) << "No one is interested in this subscription anymore, "
"stop trying" "stop trying"
<< request << "from state" << request << "from state"
<< static_cast<uint8_t>(subscription.state); << qmagicenum::enumName(subscription.state);
qCDebug(LOG) << "Set state to unsubscribed" << request; qCDebug(LOG) << "Set state to unsubscribed" << request;
subscription.state = Subscription::State::Unsubscribed; subscription.state = Subscription::State::Unsubscribed;
this->subscriptions.erase(request);
return; return;
} }
@@ -656,7 +721,7 @@ void Controller::markRequestUnsubscribed(const SubscriptionRequest &request)
auto &subscription = this->subscriptions[request]; auto &subscription = this->subscriptions[request];
qCDebug(LOG) << "Request" << request << "marked as unsubscribed from state" qCDebug(LOG) << "Request" << request << "marked as unsubscribed from state"
<< static_cast<uint8_t>(subscription.state); << qmagicenum::enumName(subscription.state);
assert(subscription.state == Subscription::State::Unsubscribing || assert(subscription.state == Subscription::State::Unsubscribing ||
subscription.state == Subscription::State::Retrying); subscription.state == Subscription::State::Retrying);
@@ -679,6 +744,7 @@ void Controller::markRequestUnsubscribed(const SubscriptionRequest &request)
if (subscription.refCount == 0) if (subscription.refCount == 0)
{ {
// we could remove the subscription here // we could remove the subscription here
this->subscriptions.erase(request);
return; return;
} }
+10 -1
View File
@@ -53,6 +53,8 @@ public:
std::unique_ptr<lib::Listener> connection, std::unique_ptr<lib::Listener> connection,
const std::optional<std::string> &reconnectURL, const std::optional<std::string> &reconnectURL,
const std::unordered_set<SubscriptionRequest> &subs) = 0; const std::unordered_set<SubscriptionRequest> &subs) = 0;
virtual void debug() = 0;
}; };
class Controller : public IController class Controller : public IController
@@ -73,6 +75,8 @@ public:
const std::optional<std::string> &reconnectURL, const std::optional<std::string> &reconnectURL,
const std::unordered_set<SubscriptionRequest> &subs) override; const std::unordered_set<SubscriptionRequest> &subs) override;
void debug() override;
private: private:
void subscribe(const SubscriptionRequest &request, bool isRetry); void subscribe(const SubscriptionRequest &request, bool isRetry);
@@ -110,7 +114,8 @@ private:
std::vector<std::weak_ptr<lib::Session>> connections; std::vector<std::weak_ptr<lib::Session>> connections;
[[nodiscard]] std::optional<std::shared_ptr<lib::Session>> [[nodiscard]] std::optional<std::shared_ptr<lib::Session>>
getViableConnection(uint32_t &openButNotReadyConnections); getViableConnection(const QString &ownerTwitchUserID,
uint32_t &openButNotReadyConnections);
struct Subscription { struct Subscription {
enum class State : uint8_t { enum class State : uint8_t {
@@ -179,6 +184,10 @@ public:
std::unique_ptr<lib::Listener> connection, std::unique_ptr<lib::Listener> connection,
const std::optional<std::string> &reconnectURL, const std::optional<std::string> &reconnectURL,
const std::unordered_set<SubscriptionRequest> &subs) override; const std::unordered_set<SubscriptionRequest> &subs) override;
void debug() override
{
}
}; };
} // namespace chatterino::eventsub } // namespace chatterino::eventsub
@@ -18,6 +18,12 @@ struct SubscriptionRequest {
// maybe this should be part of the enum later // maybe this should be part of the enum later
QString subscriptionVersion; QString subscriptionVersion;
/// The ID of the Twitch User that wants to create this subscription
///
/// Since a single websocket connection cannot share connections between users,
/// we need to key them by the user id even if it's not used in the condition.
QString ownerTwitchUserID;
/// Optional list of conditions for the subscription /// Optional list of conditions for the subscription
std::vector<std::pair<QString, QString>> conditions; std::vector<std::pair<QString, QString>> conditions;