2018-07-06 19:23:47 +02:00
|
|
|
#include "providers/twitch/PubsubClient.hpp"
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-06-26 14:09:39 +02:00
|
|
|
#include "debug/Log.hpp"
|
|
|
|
#include "providers/twitch/PubsubActions.hpp"
|
|
|
|
#include "providers/twitch/PubsubHelpers.hpp"
|
|
|
|
#include "util/RapidjsonHelpers.hpp"
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
#include <rapidjson/error/en.h>
|
|
|
|
|
|
|
|
#include <exception>
|
|
|
|
#include <thread>
|
|
|
|
|
|
|
|
#define TWITCH_PUBSUB_URL "wss://pubsub-edge.twitch.tv"
|
|
|
|
|
|
|
|
using websocketpp::lib::bind;
|
|
|
|
using websocketpp::lib::placeholders::_1;
|
|
|
|
using websocketpp::lib::placeholders::_2;
|
|
|
|
|
|
|
|
namespace chatterino {
|
|
|
|
|
|
|
|
static const char *pingPayload = "{\"type\":\"PING\"}";
|
|
|
|
|
2018-06-07 13:24:07 +02:00
|
|
|
static std::map<QString, std::string> sentMessages;
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-04-28 15:48:40 +02:00
|
|
|
namespace detail {
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
PubSubClient::PubSubClient(WebsocketClient &websocketClient, WebsocketHandle handle)
|
|
|
|
: websocketClient_(websocketClient)
|
|
|
|
, handle_(handle)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSubClient::start()
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
assert(!this->started_);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
this->started_ = true;
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
this->ping();
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSubClient::stop()
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
assert(this->started_);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
this->started_ = false;
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
bool PubSubClient::listen(rapidjson::Document &message)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
int numRequestedListens = message["data"]["topics"].Size();
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
if (this->numListens_ + numRequestedListens > MAX_PUBSUB_LISTENS) {
|
2018-04-15 15:09:31 +02:00
|
|
|
// This PubSubClient is already at its peak listens
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
this->numListens_ += numRequestedListens;
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
for (const auto &topic : message["data"]["topics"].GetArray()) {
|
2018-07-06 19:23:47 +02:00
|
|
|
this->listeners_.emplace_back(Listener{topic.GetString(), false, false, false});
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
auto uuid = CreateUUID();
|
|
|
|
|
|
|
|
rj::set(message, "nonce", uuid);
|
|
|
|
|
2018-05-13 17:51:01 +02:00
|
|
|
std::string payload = rj::stringify(message);
|
2018-06-07 13:24:07 +02:00
|
|
|
sentMessages[uuid] = payload;
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
this->send(payload.c_str());
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSubClient::unlistenPrefix(const std::string &prefix)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
std::vector<std::string> topics;
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
for (auto it = this->listeners_.begin(); it != this->listeners_.end();) {
|
2018-04-15 15:09:31 +02:00
|
|
|
const auto &listener = *it;
|
|
|
|
if (listener.topic.find(prefix) == 0) {
|
|
|
|
topics.push_back(listener.topic);
|
2018-07-06 19:23:47 +02:00
|
|
|
it = this->listeners_.erase(it);
|
2018-04-15 15:09:31 +02:00
|
|
|
} else {
|
|
|
|
++it;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (topics.empty()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
auto message = createUnlistenMessage(topics);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
auto uuid = CreateUUID();
|
|
|
|
|
|
|
|
rj::set(message, "nonce", CreateUUID());
|
|
|
|
|
2018-05-13 17:51:01 +02:00
|
|
|
std::string payload = rj::stringify(message);
|
2018-06-07 13:24:07 +02:00
|
|
|
sentMessages[uuid] = payload;
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
this->send(payload.c_str());
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSubClient::handlePong()
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
assert(this->awaitingPong_);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Got pong!");
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
this->awaitingPong_ = false;
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
bool PubSubClient::isListeningToTopic(const std::string &payload)
|
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
for (const auto &listener : this->listeners_) {
|
2018-04-15 15:09:31 +02:00
|
|
|
if (listener.topic == payload) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSubClient::ping()
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-07-06 19:23:47 +02:00
|
|
|
assert(this->started_);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
if (!this->send(pingPayload)) {
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
this->awaitingPong_ = true;
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
auto self = this->shared_from_this();
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
runAfter(this->websocketClient_.get_io_service(), std::chrono::seconds(15), [self](auto timer) {
|
|
|
|
if (!self->started_) {
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
if (self->awaitingPong_) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("No pong respnose, disconnect!");
|
2018-04-15 15:09:31 +02:00
|
|
|
// TODO(pajlada): Label this connection as "disconnect me"
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-07-06 19:23:47 +02:00
|
|
|
runAfter(this->websocketClient_.get_io_service(), std::chrono::minutes(5), [self](auto timer) {
|
|
|
|
if (!self->started_) {
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
self->ping(); //
|
2018-04-15 15:09:31 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
bool PubSubClient::send(const char *payload)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
WebsocketErrorCode ec;
|
2018-07-06 19:23:47 +02:00
|
|
|
this->websocketClient_.send(this->handle_, payload, websocketpp::frame::opcode::text, ec);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
if (ec) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Error sending message {}: {}", payload, ec.message());
|
2018-04-15 15:09:31 +02:00
|
|
|
// TODO(pajlada): Check which error code happened and maybe gracefully handle it
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-04-28 15:48:40 +02:00
|
|
|
} // namespace detail
|
|
|
|
|
|
|
|
PubSub::PubSub()
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-04-28 15:48:40 +02:00
|
|
|
qDebug() << "init PubSub";
|
2018-04-26 18:10:26 +02:00
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["clear"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
ClearChatAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.chatCleared.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["slowoff"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
ModeChangedAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
action.mode = ModeChangedAction::Mode::Slow;
|
|
|
|
action.state = ModeChangedAction::State::Off;
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.modeChanged.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["slow"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
ModeChangedAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
action.mode = ModeChangedAction::Mode::Slow;
|
|
|
|
action.state = ModeChangedAction::State::On;
|
|
|
|
|
|
|
|
if (!data.HasMember("args")) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Missing required args member");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto &args = data["args"];
|
|
|
|
|
|
|
|
if (!args.IsArray()) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("args member must be an array");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.Size() == 0) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Missing duration argument in slowmode on");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto &durationArg = args[0];
|
|
|
|
|
|
|
|
if (!durationArg.IsString()) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Duration arg must be a string");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool ok;
|
|
|
|
|
2018-04-29 13:24:37 +02:00
|
|
|
action.duration = QString(durationArg.GetString()).toUInt(&ok, 10);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.modeChanged.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["r9kbetaoff"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
ModeChangedAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
action.mode = ModeChangedAction::Mode::R9K;
|
|
|
|
action.state = ModeChangedAction::State::Off;
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.modeChanged.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["r9kbeta"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
ModeChangedAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
action.mode = ModeChangedAction::Mode::R9K;
|
|
|
|
action.state = ModeChangedAction::State::On;
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.modeChanged.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["subscribersoff"] = [this](const auto &data,
|
|
|
|
const auto &roomID) {
|
|
|
|
ModeChangedAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
action.mode = ModeChangedAction::Mode::SubscribersOnly;
|
|
|
|
action.state = ModeChangedAction::State::Off;
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.modeChanged.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["subscribers"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
ModeChangedAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
action.mode = ModeChangedAction::Mode::SubscribersOnly;
|
|
|
|
action.state = ModeChangedAction::State::On;
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.modeChanged.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["emoteonlyoff"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
ModeChangedAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
action.mode = ModeChangedAction::Mode::EmoteOnly;
|
|
|
|
action.state = ModeChangedAction::State::Off;
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.modeChanged.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["emoteonly"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
ModeChangedAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
action.mode = ModeChangedAction::Mode::EmoteOnly;
|
|
|
|
action.state = ModeChangedAction::State::On;
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.modeChanged.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["unmod"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
ModerationStateAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
getTargetUser(data, action.target);
|
2018-04-29 13:24:37 +02:00
|
|
|
|
|
|
|
try {
|
|
|
|
const auto &args = getArgs(data);
|
|
|
|
|
|
|
|
if (args.Size() < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!rj::getSafe(args[0], action.target.name)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} catch (const std::runtime_error &ex) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Error parsing moderation action: {}", ex.what());
|
2018-04-29 13:24:37 +02:00
|
|
|
}
|
|
|
|
|
2018-04-15 15:09:31 +02:00
|
|
|
action.modded = false;
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.moderationStateChanged.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["mod"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
ModerationStateAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
getTargetUser(data, action.target);
|
2018-04-29 13:24:37 +02:00
|
|
|
|
|
|
|
try {
|
|
|
|
const auto &args = getArgs(data);
|
|
|
|
|
|
|
|
if (args.Size() < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!rj::getSafe(args[0], action.target.name)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} catch (const std::runtime_error &ex) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Error parsing moderation action: {}", ex.what());
|
2018-04-29 13:24:37 +02:00
|
|
|
}
|
|
|
|
|
2018-04-15 15:09:31 +02:00
|
|
|
action.modded = true;
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.moderationStateChanged.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["timeout"] = [this](const auto &data, const auto &roomID) {
|
2018-04-27 18:35:31 +02:00
|
|
|
BanAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
getCreatedByUser(data, action.source);
|
|
|
|
getTargetUser(data, action.target);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const auto &args = getArgs(data);
|
|
|
|
|
|
|
|
if (args.Size() < 2) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!rj::getSafe(args[0], action.target.name)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString durationString;
|
|
|
|
if (!rj::getSafe(args[1], durationString)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
bool ok;
|
|
|
|
action.duration = durationString.toUInt(&ok, 10);
|
|
|
|
|
|
|
|
if (args.Size() >= 3) {
|
|
|
|
if (!rj::getSafe(args[2], action.reason)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.userBanned.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
} catch (const std::runtime_error &ex) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Error parsing moderation action: {}", ex.what());
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["ban"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
BanAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
getCreatedByUser(data, action.source);
|
|
|
|
getTargetUser(data, action.target);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const auto &args = getArgs(data);
|
|
|
|
|
|
|
|
if (args.Size() < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!rj::getSafe(args[0], action.target.name)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.Size() >= 2) {
|
|
|
|
if (!rj::getSafe(args[1], action.reason)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.userBanned.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
} catch (const std::runtime_error &ex) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Error parsing moderation action: {}", ex.what());
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["unban"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
UnbanAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
getCreatedByUser(data, action.source);
|
|
|
|
getTargetUser(data, action.target);
|
|
|
|
|
|
|
|
action.previousState = UnbanAction::Banned;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const auto &args = getArgs(data);
|
|
|
|
|
|
|
|
if (args.Size() < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!rj::getSafe(args[0], action.target.name)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.userUnbanned.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
} catch (const std::runtime_error &ex) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Error parsing moderation action: {}", ex.what());
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
this->moderationActionHandlers["untimeout"] = [this](const auto &data, const auto &roomID) {
|
|
|
|
UnbanAction action(data, roomID);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
getCreatedByUser(data, action.source);
|
|
|
|
getTargetUser(data, action.target);
|
|
|
|
|
|
|
|
action.previousState = UnbanAction::TimedOut;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const auto &args = getArgs(data);
|
|
|
|
|
|
|
|
if (args.Size() < 1) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!rj::getSafe(args[0], action.target.name)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.moderation.userUnbanned.invoke(action);
|
2018-04-15 15:09:31 +02:00
|
|
|
} catch (const std::runtime_error &ex) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Error parsing moderation action: {}", ex.what());
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
this->websocketClient.set_access_channels(websocketpp::log::alevel::all);
|
|
|
|
this->websocketClient.clear_access_channels(websocketpp::log::alevel::frame_payload);
|
|
|
|
|
|
|
|
this->websocketClient.init_asio();
|
|
|
|
|
|
|
|
// SSL Handshake
|
2018-04-28 16:07:18 +02:00
|
|
|
this->websocketClient.set_tls_init_handler(bind(&PubSub::onTLSInit, this, ::_1));
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
this->websocketClient.set_message_handler(bind(&PubSub::onMessage, this, ::_1, ::_2));
|
|
|
|
this->websocketClient.set_open_handler(bind(&PubSub::onConnectionOpen, this, ::_1));
|
|
|
|
this->websocketClient.set_close_handler(bind(&PubSub::onConnectionClose, this, ::_1));
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
// Add an initial client
|
2018-04-28 16:07:18 +02:00
|
|
|
this->addClient();
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSub::addClient()
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
websocketpp::lib::error_code ec;
|
|
|
|
auto con = this->websocketClient.get_connection(TWITCH_PUBSUB_URL, ec);
|
|
|
|
|
|
|
|
if (ec) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Unable to establish connection: {}", ec.message());
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this->websocketClient.connect(con);
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSub::start()
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-04-28 16:07:18 +02:00
|
|
|
this->mainThread.reset(new std::thread(std::bind(&PubSub::runThread, this)));
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
|
2018-06-26 17:06:17 +02:00
|
|
|
void PubSub::listenToWhispers(std::shared_ptr<TwitchAccount> account)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
assert(account != nullptr);
|
|
|
|
|
|
|
|
std::string userID = account->getUserId().toStdString();
|
|
|
|
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Connection open!");
|
2018-04-15 15:09:31 +02:00
|
|
|
websocketpp::lib::error_code ec;
|
|
|
|
|
|
|
|
std::vector<std::string> topics({"whispers." + userID});
|
|
|
|
|
2018-07-03 20:09:07 +02:00
|
|
|
this->listen(createListenMessage(topics, account));
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
if (ec) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Unable to send message to websocket server: {}", ec.message());
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSub::unlistenAllModerationActions()
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
for (const auto &p : this->clients) {
|
|
|
|
const auto &client = p.second;
|
2018-04-28 16:07:18 +02:00
|
|
|
client->unlistenPrefix("chat_moderator_actions.");
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:20:03 +02:00
|
|
|
void PubSub::listenToChannelModerationActions(const QString &channelID,
|
|
|
|
std::shared_ptr<TwitchAccount> account)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
assert(!channelID.isEmpty());
|
|
|
|
assert(account != nullptr);
|
|
|
|
QString userID = account->getUserId();
|
|
|
|
assert(!userID.isEmpty());
|
|
|
|
|
|
|
|
std::string topic(fS("chat_moderator_actions.{}.{}", userID, channelID));
|
|
|
|
|
|
|
|
if (this->isListeningToTopic(topic)) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("We are already listening to topic {}", topic);
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Listen to topic {}", topic);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
this->listenToTopic(topic, account);
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:20:03 +02:00
|
|
|
void PubSub::listenToTopic(const std::string &topic, std::shared_ptr<TwitchAccount> account)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-04-28 16:07:18 +02:00
|
|
|
auto message = createListenMessage({topic}, account);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
this->listen(std::move(message));
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSub::listen(rapidjson::Document &&msg)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-04-28 16:07:18 +02:00
|
|
|
if (this->tryListen(msg)) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Successfully listened!");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Added to the back of the queue");
|
2018-04-15 15:09:31 +02:00
|
|
|
this->requests.emplace_back(std::make_unique<rapidjson::Document>(std::move(msg)));
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
bool PubSub::tryListen(rapidjson::Document &msg)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("tryListen with {} clients", this->clients.size());
|
2018-04-15 15:09:31 +02:00
|
|
|
for (const auto &p : this->clients) {
|
|
|
|
const auto &client = p.second;
|
2018-04-28 16:07:18 +02:00
|
|
|
if (client->listen(msg)) {
|
2018-04-15 15:09:31 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-04-28 15:48:40 +02:00
|
|
|
bool PubSub::isListeningToTopic(const std::string &topic)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
for (const auto &p : this->clients) {
|
|
|
|
const auto &client = p.second;
|
|
|
|
if (client->isListeningToTopic(topic)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSub::onMessage(websocketpp::connection_hdl hdl, WebsocketMessagePtr websocketMessage)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
const std::string &payload = websocketMessage->get_payload();
|
|
|
|
|
|
|
|
rapidjson::Document msg;
|
|
|
|
|
|
|
|
rapidjson::ParseResult res = msg.Parse(payload.c_str());
|
|
|
|
|
|
|
|
if (!res) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Error parsing message '{}' from PubSub: {}", payload,
|
2018-06-26 17:20:03 +02:00
|
|
|
rapidjson::GetParseError_En(res.Code()));
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!msg.IsObject()) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Error parsing message '{}' from PubSub. Root object is not an object", payload);
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string type;
|
|
|
|
|
|
|
|
if (!rj::getSafe(msg, "type", type)) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Missing required string member `type` in message root");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type == "RESPONSE") {
|
2018-04-28 16:07:18 +02:00
|
|
|
this->handleListenResponse(msg);
|
2018-04-15 15:09:31 +02:00
|
|
|
} else if (type == "MESSAGE") {
|
|
|
|
if (!msg.HasMember("data")) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Missing required object member `data` in message root");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const auto &data = msg["data"];
|
|
|
|
|
|
|
|
if (!data.IsObject()) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Member `data` must be an object");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
this->handleMessageResponse(data);
|
2018-04-15 15:09:31 +02:00
|
|
|
} else if (type == "PONG") {
|
|
|
|
auto clientIt = this->clients.find(hdl);
|
|
|
|
|
|
|
|
// If this assert goes off, there's something wrong with the connection creation/preserving
|
|
|
|
// code KKona
|
|
|
|
assert(clientIt != this->clients.end());
|
|
|
|
|
|
|
|
auto &client = *clientIt;
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
client.second->handlePong();
|
2018-04-15 15:09:31 +02:00
|
|
|
} else {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Unknown message type: {}", type);
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSub::onConnectionOpen(WebsocketHandle hdl)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-04-28 15:48:40 +02:00
|
|
|
auto client = std::make_shared<detail::PubSubClient>(this->websocketClient, hdl);
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
// We separate the starting from the constructor because we will want to use shared_from_this
|
2018-04-28 16:07:18 +02:00
|
|
|
client->start();
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
this->clients.emplace(hdl, client);
|
|
|
|
|
|
|
|
this->connected.invoke();
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSub::onConnectionClose(WebsocketHandle hdl)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
auto clientIt = this->clients.find(hdl);
|
|
|
|
|
|
|
|
// If this assert goes off, there's something wrong with the connection creation/preserving
|
|
|
|
// code KKona
|
|
|
|
assert(clientIt != this->clients.end());
|
|
|
|
|
|
|
|
auto &client = clientIt->second;
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
client->stop();
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
this->clients.erase(clientIt);
|
|
|
|
|
|
|
|
this->connected.invoke();
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
PubSub::WebsocketContextPtr PubSub::onTLSInit(websocketpp::connection_hdl hdl)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
WebsocketContextPtr ctx(new boost::asio::ssl::context(boost::asio::ssl::context::tlsv1));
|
|
|
|
|
|
|
|
try {
|
|
|
|
ctx->set_options(boost::asio::ssl::context::default_workarounds |
|
|
|
|
boost::asio::ssl::context::no_sslv2 |
|
|
|
|
boost::asio::ssl::context::single_dh_use);
|
|
|
|
} catch (const std::exception &e) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Exception caught in OnTLSInit: {}", e.what());
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return ctx;
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSub::handleListenResponse(const rapidjson::Document &msg)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
|
|
|
std::string error;
|
|
|
|
|
|
|
|
if (rj::getSafe(msg, "error", error)) {
|
|
|
|
std::string nonce;
|
|
|
|
rj::getSafe(msg, "nonce", nonce);
|
|
|
|
|
|
|
|
if (error.empty()) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Successfully listened to nonce {}", nonce);
|
2018-04-15 15:09:31 +02:00
|
|
|
// Nothing went wrong
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("PubSub error: {} on nonce {}", error, nonce);
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-04-22 15:37:02 +02:00
|
|
|
QString topic;
|
2018-04-15 15:09:31 +02:00
|
|
|
|
|
|
|
if (!rj::getSafe(outerData, "topic", topic)) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Missing required string member `topic` in outerData");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string payload;
|
|
|
|
|
|
|
|
if (!rj::getSafe(outerData, "message", payload)) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Expected string message in outerData");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
rapidjson::Document msg;
|
|
|
|
|
|
|
|
rapidjson::ParseResult res = msg.Parse(payload.c_str());
|
|
|
|
|
|
|
|
if (!res) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Error parsing message '{}' from PubSub: {}", payload,
|
2018-06-26 17:20:03 +02:00
|
|
|
rapidjson::GetParseError_En(res.Code()));
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-22 15:37:02 +02:00
|
|
|
if (topic.startsWith("whispers.")) {
|
2018-04-15 15:09:31 +02:00
|
|
|
std::string whisperType;
|
|
|
|
|
|
|
|
if (!rj::getSafe(msg, "type", whisperType)) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Bad whisper data");
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (whisperType == "whisper_received") {
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.whisper.received.invoke(msg);
|
2018-04-15 15:09:31 +02:00
|
|
|
} else if (whisperType == "whisper_sent") {
|
2018-06-26 17:47:44 +02:00
|
|
|
this->signals_.whisper.sent.invoke(msg);
|
2018-04-15 15:09:31 +02:00
|
|
|
} else if (whisperType == "thread") {
|
|
|
|
// Handle thread?
|
|
|
|
} else {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Invalid whisper type: {}", whisperType);
|
2018-04-15 15:09:31 +02:00
|
|
|
assert(false);
|
|
|
|
return;
|
|
|
|
}
|
2018-04-22 15:37:02 +02:00
|
|
|
} else if (topic.startsWith("chat_moderator_actions.")) {
|
|
|
|
auto topicParts = topic.split(".");
|
|
|
|
assert(topicParts.length() == 3);
|
2018-04-15 15:09:31 +02:00
|
|
|
const auto &data = msg["data"];
|
|
|
|
|
|
|
|
std::string moderationAction;
|
|
|
|
|
|
|
|
if (!rj::getSafe(data, "moderation_action", moderationAction)) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Missing moderation action in data: {}", rj::stringify(data));
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto handlerIt = this->moderationActionHandlers.find(moderationAction);
|
|
|
|
|
|
|
|
if (handlerIt == this->moderationActionHandlers.end()) {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("No handler found for moderation action {}", moderationAction);
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Invoke handler function
|
2018-04-22 15:37:02 +02:00
|
|
|
handlerIt->second(data, topicParts[2]);
|
2018-04-15 15:09:31 +02:00
|
|
|
} else {
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Unknown topic: {}", topic);
|
2018-04-15 15:09:31 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-28 16:07:18 +02:00
|
|
|
void PubSub::runThread()
|
2018-04-15 15:09:31 +02:00
|
|
|
{
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Start pubsub manager thread");
|
2018-04-15 15:09:31 +02:00
|
|
|
this->websocketClient.run();
|
2018-06-26 17:06:17 +02:00
|
|
|
Log("Done with pubsub manager thread");
|
2018-04-15 15:09:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace chatterino
|