feat(eventsub): reconnect (#6035)
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Args.hpp"
|
||||
#include "common/Literals.hpp"
|
||||
#include "common/network/NetworkRequest.hpp"
|
||||
#include "common/network/NetworkResult.hpp"
|
||||
@@ -3393,7 +3395,16 @@ NetworkRequest Helix::makeRequest(const QString &url, const QUrlQuery &urlQuery,
|
||||
// return std::nullopt;
|
||||
}
|
||||
|
||||
const QString baseUrl("https://api.twitch.tv/helix/");
|
||||
QString baseUrl("https://api.twitch.tv/helix/");
|
||||
|
||||
#ifndef NDEBUG
|
||||
bool ignoreSslErrors =
|
||||
getApp()->getArgs().useLocalEventsub && url == "eventsub/subscriptions";
|
||||
if (ignoreSslErrors)
|
||||
{
|
||||
baseUrl = "https://127.0.0.1:3012/";
|
||||
}
|
||||
#endif
|
||||
|
||||
QUrl fullUrl(baseUrl + url);
|
||||
|
||||
@@ -3403,7 +3414,11 @@ NetworkRequest Helix::makeRequest(const QString &url, const QUrlQuery &urlQuery,
|
||||
.timeout(5 * 1000)
|
||||
.header("Accept", "application/json")
|
||||
.header("Client-ID", this->clientId)
|
||||
.header("Authorization", "Bearer " + this->oauthToken);
|
||||
.header("Authorization", "Bearer " + this->oauthToken)
|
||||
#ifndef NDEBUG
|
||||
.ignoreSslErrors(ignoreSslErrors)
|
||||
#endif
|
||||
;
|
||||
}
|
||||
|
||||
NetworkRequest Helix::makeGet(const QString &url, const QUrlQuery &urlQuery)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "controllers/highlights/HighlightController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/twitch/eventsub/Controller.hpp"
|
||||
#include "providers/twitch/eventsub/MessageBuilder.hpp"
|
||||
#include "providers/twitch/eventsub/MessageHandlers.hpp"
|
||||
#include "providers/twitch/PubSubActions.hpp"
|
||||
@@ -18,7 +19,7 @@
|
||||
#include "util/PostToThread.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <qdatetime.h>
|
||||
#include <QDateTime>
|
||||
#include <twitch-eventsub-ws/listener.hpp>
|
||||
#include <twitch-eventsub-ws/session.hpp>
|
||||
|
||||
@@ -71,6 +72,19 @@ void Connection::onNotification(const lib::messages::Metadata &metadata,
|
||||
qCDebug(LOG) << "on notification: " << jsonString.c_str();
|
||||
}
|
||||
|
||||
void Connection::onClose(std::unique_ptr<lib::Listener> self,
|
||||
const std::optional<std::string> &reconnectURL)
|
||||
{
|
||||
auto *app = tryGetApp();
|
||||
if (!app)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
app->getEventSub()->reconnectConnection(std::move(self), reconnectURL,
|
||||
this->subscriptions);
|
||||
}
|
||||
|
||||
void Connection::onChannelBan(
|
||||
const lib::messages::Metadata &metadata,
|
||||
const lib::payload::channel_ban::v1::Payload &payload)
|
||||
@@ -387,4 +401,9 @@ void Connection::markRequestSubscribed(const SubscriptionRequest &request)
|
||||
this->subscriptions.emplace(request);
|
||||
}
|
||||
|
||||
void Connection::markRequestUnsubscribed(const SubscriptionRequest &request)
|
||||
{
|
||||
this->subscriptions.erase(request);
|
||||
}
|
||||
|
||||
} // namespace chatterino::eventsub
|
||||
|
||||
@@ -21,6 +21,9 @@ public:
|
||||
void onNotification(const lib::messages::Metadata &metadata,
|
||||
const boost::json::value &jv) override;
|
||||
|
||||
void onClose(std::unique_ptr<lib::Listener> self,
|
||||
const std::optional<std::string> &reconnectURL) override;
|
||||
|
||||
void onChannelBan(
|
||||
const lib::messages::Metadata &metadata,
|
||||
const lib::payload::channel_ban::v1::Payload &payload) override;
|
||||
@@ -85,7 +88,7 @@ public:
|
||||
|
||||
bool isSubscribedTo(const SubscriptionRequest &request) const;
|
||||
void markRequestSubscribed(const SubscriptionRequest &request);
|
||||
// TODO: Add an "markRequestUnsubscribed" method
|
||||
void markRequestUnsubscribed(const SubscriptionRequest &request);
|
||||
|
||||
private:
|
||||
QString sessionID;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "providers/twitch/eventsub/Controller.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Args.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "common/Version.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
@@ -17,16 +19,16 @@
|
||||
|
||||
namespace {
|
||||
|
||||
/// Enable LOCAL_EVENTSUB when you want to debug eventsub with a local instance of the Twitch CLI
|
||||
/// twitch event websocket start-server --ssl --port 3012
|
||||
constexpr bool LOCAL_EVENTSUB = false;
|
||||
using namespace chatterino;
|
||||
|
||||
std::tuple<std::string, std::string, std::string> getEventSubHost()
|
||||
{
|
||||
if constexpr (LOCAL_EVENTSUB)
|
||||
#ifndef NDEBUG
|
||||
if (getApp()->getArgs().useLocalEventsub)
|
||||
{
|
||||
return {"localhost", "3012", "/ws"};
|
||||
}
|
||||
#endif
|
||||
|
||||
return {"eventsub.wss.twitch.tv", "443", "/ws"};
|
||||
}
|
||||
@@ -222,6 +224,54 @@ SubscriptionHandle Controller::subscribe(const SubscriptionRequest &request)
|
||||
return handle;
|
||||
}
|
||||
|
||||
void Controller::reconnectConnection(
|
||||
std::unique_ptr<lib::Listener> connection,
|
||||
const std::optional<std::string> &reconnectURL,
|
||||
const std::unordered_set<SubscriptionRequest> &subs)
|
||||
{
|
||||
this->clearConnections();
|
||||
if (subs.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (reconnectURL)
|
||||
{
|
||||
qCDebug(chatterinoTwitchEventSub) << "Using reconnect URL to reconnect";
|
||||
// this is epic
|
||||
QUrl url(QString::fromStdString(*reconnectURL));
|
||||
this->createConnection(url.host(QUrl::FullyEncoded).toStdString(),
|
||||
std::to_string(url.port(443)),
|
||||
url.path(QUrl::FullyEncoded).toStdString(),
|
||||
std::move(connection));
|
||||
return;
|
||||
}
|
||||
|
||||
// no reconnect URL - something happened
|
||||
// but first, clear the subscriptions
|
||||
qCDebug(chatterinoTwitchEventSub)
|
||||
<< "Resubscribing to topics after connection failure";
|
||||
{
|
||||
std::lock_guard g(this->subscriptionsMutex);
|
||||
for (const auto &sub : subs)
|
||||
{
|
||||
auto it = this->subscriptions.find(sub);
|
||||
if (it != this->subscriptions.end())
|
||||
{
|
||||
qCDebug(chatterinoTwitchEventSub) << "Resetting" << it->first;
|
||||
it->second.connection = {};
|
||||
it->second.retryAttempts = 0;
|
||||
it->second.state = Subscription::State::Subscribing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &sub : subs)
|
||||
{
|
||||
this->subscribe(sub, false);
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::subscribe(const SubscriptionRequest &request, bool isRetry)
|
||||
{
|
||||
// 1. Flush dead connections (maybe this should not be done here)
|
||||
@@ -355,8 +405,10 @@ std::optional<std::shared_ptr<lib::Session>> Controller::getViableConnection(
|
||||
|
||||
auto *listener = dynamic_cast<Connection *>(connection->getListener());
|
||||
|
||||
assert(listener != nullptr && "Something goofy has gone wrong, Session "
|
||||
"listener must be our Connection type");
|
||||
if (!listener)
|
||||
{
|
||||
continue; // dead connection
|
||||
}
|
||||
|
||||
if (listener->getSessionID().isEmpty())
|
||||
{
|
||||
@@ -374,13 +426,23 @@ std::optional<std::shared_ptr<lib::Session>> Controller::getViableConnection(
|
||||
}
|
||||
|
||||
void Controller::createConnection()
|
||||
{
|
||||
this->createConnection(this->eventSubHost, this->eventSubPort,
|
||||
this->eventSubPath, std::make_unique<Connection>());
|
||||
}
|
||||
|
||||
void Controller::createConnection(std::string host, std::string port,
|
||||
std::string path,
|
||||
std::unique_ptr<lib::Listener> listener)
|
||||
{
|
||||
try
|
||||
{
|
||||
boost::asio::ssl::context sslContext{
|
||||
boost::asio::ssl::context::tlsv12_client};
|
||||
|
||||
if constexpr (!LOCAL_EVENTSUB)
|
||||
#ifndef NDEBUG
|
||||
if (!getApp()->getArgs().useLocalEventsub)
|
||||
#endif
|
||||
{
|
||||
sslContext.set_verify_mode(
|
||||
boost::asio::ssl::verify_peer |
|
||||
@@ -391,12 +453,12 @@ void Controller::createConnection()
|
||||
}
|
||||
|
||||
auto connection = std::make_shared<lib::Session>(
|
||||
this->ioContext, sslContext, std::make_unique<Connection>());
|
||||
this->ioContext, sslContext, std::move(listener));
|
||||
|
||||
this->registerConnection(connection);
|
||||
|
||||
connection->run(this->eventSubHost, this->eventSubPort,
|
||||
this->eventSubPath, this->userAgent);
|
||||
connection->run(std::move(host), std::move(port), std::move(path),
|
||||
this->userAgent);
|
||||
}
|
||||
catch (std::exception &e)
|
||||
{
|
||||
@@ -509,6 +571,20 @@ void Controller::markRequestSubscribed(const SubscriptionRequest &request,
|
||||
|
||||
std::lock_guard lock(this->subscriptionsMutex);
|
||||
|
||||
auto strong = connection.lock();
|
||||
if (!strong)
|
||||
{
|
||||
// we disconnected
|
||||
return;
|
||||
}
|
||||
auto *listener = dynamic_cast<Connection *>(strong->getListener());
|
||||
if (!listener)
|
||||
{
|
||||
// we disconnected
|
||||
return;
|
||||
}
|
||||
listener->markRequestSubscribed(request);
|
||||
|
||||
auto &subscription = this->subscriptions[request];
|
||||
|
||||
assert((subscription.state == Subscription::State::Subscribing ||
|
||||
@@ -560,6 +636,31 @@ void Controller::markRequestUnsubscribed(const SubscriptionRequest &request)
|
||||
qCDebug(LOG) << "Set state to unsubscribed" << request;
|
||||
subscription.state = Subscription::State::Unsubscribed;
|
||||
subscription.retryAttempts = 0;
|
||||
auto conn = subscription.connection.lock();
|
||||
if (conn)
|
||||
{
|
||||
auto *listener = dynamic_cast<Connection *>(conn->getListener());
|
||||
if (listener)
|
||||
{
|
||||
listener->markRequestUnsubscribed(request);
|
||||
}
|
||||
}
|
||||
subscription.connection = {};
|
||||
}
|
||||
|
||||
void Controller::clearConnections()
|
||||
{
|
||||
std::erase_if(this->connections, [](const auto &it) {
|
||||
auto conn = it.lock();
|
||||
return !conn || !conn->getListener();
|
||||
});
|
||||
}
|
||||
|
||||
void DummyController::reconnectConnection(
|
||||
std::unique_ptr<lib::Listener> /* connection */,
|
||||
const std::optional<std::string> & /* reconnectURL */,
|
||||
const std::unordered_set<SubscriptionRequest> & /* subs */)
|
||||
{
|
||||
}
|
||||
|
||||
} // namespace chatterino::eventsub
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace chatterino::eventsub {
|
||||
|
||||
@@ -44,6 +45,11 @@ public:
|
||||
/// create a new connection and queue up the subscription to run again after X seconds.
|
||||
[[nodiscard]] virtual SubscriptionHandle subscribe(
|
||||
const SubscriptionRequest &request) = 0;
|
||||
|
||||
virtual void reconnectConnection(
|
||||
std::unique_ptr<lib::Listener> connection,
|
||||
const std::optional<std::string> &reconnectURL,
|
||||
const std::unordered_set<SubscriptionRequest> &subs) = 0;
|
||||
};
|
||||
|
||||
class Controller : public IController
|
||||
@@ -59,10 +65,17 @@ public:
|
||||
[[nodiscard]] SubscriptionHandle subscribe(
|
||||
const SubscriptionRequest &request) override;
|
||||
|
||||
void reconnectConnection(
|
||||
std::unique_ptr<lib::Listener> connection,
|
||||
const std::optional<std::string> &reconnectURL,
|
||||
const std::unordered_set<SubscriptionRequest> &subs) override;
|
||||
|
||||
private:
|
||||
void subscribe(const SubscriptionRequest &request, bool isRetry);
|
||||
|
||||
void createConnection();
|
||||
void createConnection(std::string host, std::string port, std::string path,
|
||||
std::unique_ptr<lib::Listener> listener);
|
||||
void registerConnection(std::weak_ptr<lib::Session> &&connection);
|
||||
|
||||
void retrySubscription(const SubscriptionRequest &request,
|
||||
@@ -77,6 +90,8 @@ private:
|
||||
|
||||
void markRequestUnsubscribed(const SubscriptionRequest &request);
|
||||
|
||||
void clearConnections();
|
||||
|
||||
const std::string userAgent;
|
||||
|
||||
std::string eventSubHost;
|
||||
@@ -154,6 +169,11 @@ public:
|
||||
(void)request;
|
||||
return {};
|
||||
}
|
||||
|
||||
void reconnectConnection(
|
||||
std::unique_ptr<lib::Listener> connection,
|
||||
const std::optional<std::string> &reconnectURL,
|
||||
const std::unordered_set<SubscriptionRequest> &subs) override;
|
||||
};
|
||||
|
||||
} // namespace chatterino::eventsub
|
||||
|
||||
Reference in New Issue
Block a user