refactored irc

This commit is contained in:
fourtf
2018-02-05 15:11:50 +01:00
parent 12b30eb2ed
commit b351c40d29
56 changed files with 1397 additions and 1154 deletions
+37
View File
@@ -0,0 +1,37 @@
#include "_ircaccount.hpp"
// namespace chatterino {
// namespace providers {
// namespace irc {
// IrcAccount::IrcAccount(const QString &_userName, const QString &_nickName, const QString
// &_realName,
// const QString &_password)
// : userName(_userName)
// , nickName(_nickName)
// , realName(_realName)
// , password(_password)
//{
//}
// const QString &IrcAccount::getUserName() const
//{
// return this->userName;
//}
// const QString &IrcAccount::getNickName() const
//{
// return this->nickName;
//}
// const QString &IrcAccount::getRealName() const
//{
// return this->realName;
//}
// const QString &IrcAccount::getPassword() const
//{
// return this->password;
//}
//} // namespace irc
//} // namespace providers
//} // namespace chatterino
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <QString>
// namespace chatterino {
// namespace providers {
// namespace irc {
// class IrcAccount
//{
// public:
// IrcAccount(const QString &userName, const QString &nickName, const QString &realName,
// const QString &password);
// const QString &getUserName() const;
// const QString &getNickName() const;
// const QString &getRealName() const;
// const QString &getPassword() const;
// private:
// QString userName;
// QString nickName;
// QString realName;
// QString password;
//};
//} // namespace irc
//} // namespace providers
//} // namespace chatterino
+11
View File
@@ -0,0 +1,11 @@
#include "_ircchannel.hpp"
namespace chatterino {
namespace providers {
namespace irc {
// IrcChannel::IrcChannel()
//{
//}
} // namespace irc
} // namespace providers
} // namespace chatterino
+13
View File
@@ -0,0 +1,13 @@
#pragma once
namespace chatterino {
namespace providers {
namespace irc {
// class IrcChannel
//{
// public:
// IrcChannel();
//};
} // namespace irc
} // namespace providers
} // namespace chatterino
+14
View File
@@ -0,0 +1,14 @@
#include "_ircserver.hpp"
#include <cassert>
namespace chatterino {
namespace providers {
namespace irc {
// IrcServer::IrcServer(const QString &hostname, int port)
//{
// this->initConnection();
//}
} // namespace irc
} // namespace providers
} // namespace chatterino
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "providers/irc/_ircaccount.hpp"
#include "providers/irc/abstractircserver.hpp"
namespace chatterino {
namespace providers {
namespace irc {
// class IrcServer
//{
// public:
// IrcServer(const QString &hostname, int port);
// void setAccount(std::shared_ptr<IrcAccount> newAccount);
// std::shared_ptr<IrcAccount> getAccount() const;
// protected:
// virtual void initializeConnection(Communi::IrcConnection *connection, bool isReadConnection);
// virtual void privateMessageReceived(Communi::IrcPrivateMessage *message);
// virtual void messageReceived(Communi::IrcMessage *message);
//};
} // namespace irc
} // namespace providers
} // namespace chatterino
+226
View File
@@ -0,0 +1,226 @@
#include "abstractircserver.hpp"
#include "common.hpp"
#include "messages/limitedqueuesnapshot.hpp"
#include "messages/message.hpp"
using namespace chatterino::messages;
namespace chatterino {
namespace providers {
namespace irc {
AbstractIrcServer::AbstractIrcServer()
{
// Initialize the connections
this->writeConnection.reset(new Communi::IrcConnection);
this->writeConnection->moveToThread(QCoreApplication::instance()->thread());
QObject::connect(this->writeConnection.get(), &Communi::IrcConnection::messageReceived,
[this](auto msg) { this->writeConnectionMessageReceived(msg); });
// Listen to read connection message signals
this->readConnection.reset(new Communi::IrcConnection);
this->readConnection->moveToThread(QCoreApplication::instance()->thread());
QObject::connect(this->readConnection.get(), &Communi::IrcConnection::messageReceived,
[this](auto msg) { this->messageReceived(msg); });
QObject::connect(this->readConnection.get(), &Communi::IrcConnection::privateMessageReceived,
[this](auto msg) { this->privateMessageReceived(msg); });
QObject::connect(this->readConnection.get(), &Communi::IrcConnection::connected,
[this] { this->onConnected(); });
QObject::connect(this->readConnection.get(), &Communi::IrcConnection::disconnected,
[this] { this->onDisconnected(); });
}
Communi::IrcConnection *AbstractIrcServer::getReadConnection() const
{
return this->readConnection.get();
}
void AbstractIrcServer::connect()
{
this->disconnect();
// if (this->hasSeperateWriteConnection()) {
this->initializeConnection(this->writeConnection.get(), false, true);
this->initializeConnection(this->readConnection.get(), true, false);
// } else {
// this->initializeConnection(this->readConnection.get(), true, true);
// }
// fourtf: this should be asynchronous
{
std::lock_guard<std::mutex> lock1(this->connectionMutex);
std::lock_guard<std::mutex> lock2(this->channelMutex);
for (std::weak_ptr<Channel> &weak : this->channels.values()) {
std::shared_ptr<Channel> chan = weak.lock();
if (!chan) {
continue;
}
this->writeConnection->sendRaw("JOIN #" + chan->name);
this->readConnection->sendRaw("JOIN #" + chan->name);
}
this->writeConnection->open();
this->readConnection->open();
}
this->onConnected();
this->connected.invoke();
}
void AbstractIrcServer::disconnect()
{
std::lock_guard<std::mutex> locker(this->connectionMutex);
this->readConnection->close();
this->writeConnection->close();
}
void AbstractIrcServer::sendMessage(const QString &channelName, const QString &message)
{
std::lock_guard<std::mutex> locker(this->connectionMutex);
// fourtf: trim the message if it's sent from twitch chat
if (this->writeConnection) {
this->writeConnection->sendRaw("PRIVMSG #" + channelName + " :" + message);
}
}
void AbstractIrcServer::writeConnectionMessageReceived(Communi::IrcMessage *message)
{
}
std::shared_ptr<Channel> AbstractIrcServer::addChannel(const QString &channelName)
{
std::lock_guard<std::mutex> lock(this->channelMutex);
// value exists
auto it = this->channels.find(channelName);
if (it != this->channels.end()) {
std::shared_ptr<Channel> chan = it.value().lock();
if (chan) {
return chan;
}
}
// value doesn't exist
std::shared_ptr<Channel> chan = this->createChannel(channelName);
if (!chan) {
return Channel::getEmpty();
}
QString clojuresInCppAreShit = channelName;
this->channels.insert(channelName, chan);
chan->destroyed.connect([this, clojuresInCppAreShit] {
// fourtf: issues when the server itself in destroyed
debug::Log("[AbstractIrcServer::addChannel] {} was destroyed", clojuresInCppAreShit);
this->channels.remove(clojuresInCppAreShit);
});
// join irc channel
{
std::lock_guard<std::mutex> lock2(this->connectionMutex);
if (this->readConnection) {
this->readConnection->sendRaw("JOIN #" + channelName);
}
if (this->writeConnection) {
this->writeConnection->sendRaw("JOIN #" + channelName);
}
}
return chan;
}
std::shared_ptr<Channel> AbstractIrcServer::getChannel(const QString &channelName)
{
std::lock_guard<std::mutex> lock(this->channelMutex);
// value exists
auto it = this->channels.find(channelName);
if (it != this->channels.end()) {
std::shared_ptr<Channel> chan = it.value().lock();
if (chan) {
return chan;
}
}
return Channel::getEmpty();
}
void AbstractIrcServer::onConnected()
{
std::lock_guard<std::mutex> lock(this->channelMutex);
MessagePtr connMsg = Message::createSystemMessage("connected to chat");
MessagePtr reconnMsg = Message::createSystemMessage("reconnected to chat");
for (std::weak_ptr<Channel> &weak : this->channels.values()) {
std::shared_ptr<Channel> chan = weak.lock();
if (!chan) {
continue;
}
LimitedQueueSnapshot<MessagePtr> snapshot = chan->getMessageSnapshot();
bool replaceMessage =
snapshot.getLength() > 0 &&
snapshot[snapshot.getLength() - 1]->flags & Message::DisconnectedMessage;
if (replaceMessage) {
chan->replaceMessage(snapshot[snapshot.getLength() - 1], reconnMsg);
continue;
}
chan->addMessage(connMsg);
}
}
void AbstractIrcServer::onDisconnected()
{
std::lock_guard<std::mutex> lock(this->channelMutex);
MessagePtr msg = Message::createSystemMessage("disconnected from chat");
msg->flags &= Message::DisconnectedMessage;
for (std::weak_ptr<Channel> &weak : this->channels.values()) {
std::shared_ptr<Channel> chan = weak.lock();
if (!chan) {
continue;
}
chan->addMessage(msg);
}
}
std::shared_ptr<Channel> AbstractIrcServer::getCustomChannel(const QString &channelName)
{
return nullptr;
}
void AbstractIrcServer::addFakeMessage(const QString &data)
{
auto fakeMessage = Communi::IrcMessage::fromData(data.toUtf8(), this->readConnection.get());
this->privateMessageReceived(qobject_cast<Communi::IrcPrivateMessage *>(fakeMessage));
}
void AbstractIrcServer::privateMessageReceived(Communi::IrcPrivateMessage *message)
{
}
void AbstractIrcServer::messageReceived(Communi::IrcMessage *message)
{
}
} // namespace irc
} // namespace providers
} // namespace chatterino
+63
View File
@@ -0,0 +1,63 @@
#pragma once
#include <IrcMessage>
#include <mutex>
#include <pajlada/signals/signal.hpp>
#include "channel.hpp"
namespace chatterino {
namespace providers {
namespace irc {
class AbstractIrcServer
{
public:
// connection
Communi::IrcConnection *getReadConnection() const;
void connect();
void disconnect();
void sendMessage(const QString &channelName, const QString &message);
// channels
std::shared_ptr<Channel> addChannel(const QString &channelName);
std::shared_ptr<Channel> getChannel(const QString &channelName);
// signals
pajlada::Signals::NoArgSignal connected;
pajlada::Signals::NoArgSignal disconnected;
pajlada::Signals::Signal<Communi::IrcPrivateMessage *> onPrivateMessage;
void addFakeMessage(const QString &data);
protected:
AbstractIrcServer();
virtual void initializeConnection(Communi::IrcConnection *connection, bool isRead,
bool isWrite) = 0;
virtual std::shared_ptr<Channel> createChannel(const QString &channelName) = 0;
virtual void privateMessageReceived(Communi::IrcPrivateMessage *message);
virtual void messageReceived(Communi::IrcMessage *message);
virtual void writeConnectionMessageReceived(Communi::IrcMessage *message);
virtual void onConnected();
virtual void onDisconnected();
virtual std::shared_ptr<Channel> getCustomChannel(const QString &channelName);
private:
void initConnection();
QMap<QString, std::weak_ptr<Channel>> channels;
std::unique_ptr<Communi::IrcConnection> writeConnection = nullptr;
std::unique_ptr<Communi::IrcConnection> readConnection = nullptr;
std::mutex connectionMutex;
std::mutex channelMutex;
};
} // namespace irc
} // namespace providers
} // namespace chatterino
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "QString"
namespace chatterino {
namespace providers {
namespace twitch {
struct EmoteValue {
public:
int getSet()
{
return _set;
}
int getId()
{
return _id;
}
const QString &getChannelName()
{
return _channelName;
}
private:
int _set;
int _id;
QString _channelName;
};
} // namespace twitch
} // namespace providers
} // namespace chatterino
+219
View File
@@ -0,0 +1,219 @@
#include "ircmessagehandler.hpp"
#include <memory>
#include "debug/log.hpp"
#include "messages/limitedqueue.hpp"
#include "messages/message.hpp"
#include "providers/twitch/twitchchannel.hpp"
//#include "singletons/channelmanager.hpp"
#include "singletons/resourcemanager.hpp"
#include "singletons/windowmanager.hpp"
#include "twitchserver.hpp"
using namespace chatterino::singletons;
using namespace chatterino::messages;
namespace chatterino {
namespace providers {
namespace twitch {
IrcMessageHandler::IrcMessageHandler(singletons::ResourceManager &_resourceManager)
: resourceManager(_resourceManager)
{
}
IrcMessageHandler &IrcMessageHandler::getInstance()
{
static IrcMessageHandler instance(singletons::ResourceManager::getInstance());
return instance;
}
void IrcMessageHandler::handleRoomStateMessage(Communi::IrcMessage *message)
{
const auto &tags = message->tags();
auto iterator = tags.find("room-id");
if (iterator != tags.end()) {
auto roomID = iterator.value().toString();
QStringList words = QString(message->toData()).split("#");
// ensure the format is valid
if (words.length() < 2)
return;
QString channelName = words.at(1);
auto channel = TwitchServer::getInstance().getChannel(channelName);
if (auto twitchChannel = dynamic_cast<twitch::TwitchChannel *>(channel.get())) {
// set the room id of the channel
twitchChannel->setRoomID(roomID);
}
ResourceManager::getInstance().loadChannelData(roomID);
}
}
void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message)
{
// check parameter count
if (message->parameters().length() < 1)
return;
QString chanName = message->parameter(0);
// check channel name length
if (chanName.length() >= 2)
return;
chanName = chanName.mid(1);
// get channel
auto chan = TwitchServer::getInstance().getChannel(chanName);
if (!chan) {
debug::Log("[IrcMessageHandler:handleClearChatMessage] Twitch channel {} not found",
chanName);
return;
}
// check if the chat has been cleared by a moderator
if (message->parameters().length() == 1) {
chan->addMessage(Message::createSystemMessage("Chat has been cleared by a moderator."));
return;
}
assert(message->parameters().length() >= 2);
// get username, duration and message of the timed out user
QString username = message->parameter(1);
QString durationInSeconds, reason;
QVariant v = message->tag("ban-duration");
if (v.isValid()) {
durationInSeconds = v.toString();
}
v = message->tag("ban-reason");
if (v.isValid()) {
reason = v.toString();
}
// add the notice that the user has been timed out
LimitedQueueSnapshot<MessagePtr> snapshot = chan->getMessageSnapshot();
bool addMessage = true;
int snapshotLength = snapshot.getLength();
for (int i = std::max(0, snapshotLength - 20); i < snapshotLength; i++) {
if (snapshot[i]->flags & Message::Timeout && snapshot[i]->loginName == username) {
MessagePtr replacement(
Message::createTimeoutMessage(username, durationInSeconds, reason, true));
chan->replaceMessage(snapshot[i], replacement);
addMessage = false;
break;
}
}
if (addMessage) {
chan->addMessage(Message::createTimeoutMessage(username, durationInSeconds, reason, false));
}
// disable the messages from the user
for (int i = 0; i < snapshotLength; i++) {
if (!(snapshot[i]->flags & Message::Timeout) && snapshot[i]->loginName == username) {
snapshot[i]->flags &= Message::Disabled;
}
}
// refresh all
WindowManager::getInstance().repaintVisibleChatWidgets(chan.get());
}
void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
{
QVariant _mod = message->tag("mod");
if (_mod.isValid()) {
auto rawChannelName = message->parameters().at(0);
auto trimmedChannelName = rawChannelName.mid(1);
auto c = TwitchServer::getInstance().getChannel(trimmedChannelName);
twitch::TwitchChannel *tc = dynamic_cast<twitch::TwitchChannel *>(c.get());
if (tc != nullptr) {
tc->setMod(_mod == "1");
}
}
}
void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
{
// TODO: Implement
}
void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message)
{
// do nothing
}
void IrcMessageHandler::handleModeMessage(Communi::IrcMessage *message)
{
auto channel = TwitchServer::getInstance().getChannel(message->parameter(0).remove(0, 1));
if (message->parameter(1) == "+o") {
channel->modList.append(message->parameter(2));
} else if (message->parameter(1) == "-o") {
channel->modList.append(message->parameter(2));
}
}
void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
{
auto rawChannelName = message->target();
bool broadcast = rawChannelName.length() < 2;
MessagePtr msg = Message::createSystemMessage(message->content());
if (broadcast) {
// fourtf: send to all twitch channels
// this->channelManager.doOnAll([msg](const auto &c) {
// c->addMessage(msg); //
// });
return;
}
auto trimmedChannelName = rawChannelName.mid(1);
auto channel = TwitchServer::getInstance().getChannel(trimmedChannelName);
if (!channel) {
debug::Log("[IrcManager:handleNoticeMessage] Channel {} not found in channel manager",
trimmedChannelName);
return;
}
channel->addMessage(msg);
}
void IrcMessageHandler::handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message)
{
QVariant v = message->tag("msg-id");
if (!v.isValid()) {
return;
}
QString msg_id = v.toString();
static QList<QString> idsToSkip = {"timeout_success", "ban_success"};
if (idsToSkip.contains(msg_id)) {
// Already handled in the read-connection
return;
}
this->handleNoticeMessage(message);
}
} // namespace twitch
} // namespace providers
} // namespace chatterino
@@ -0,0 +1,33 @@
#pragma once
#include <IrcMessage>
namespace chatterino {
namespace singletons {
class ChannelManager;
class ResourceManager;
} // namespace singletons
namespace providers {
namespace twitch {
class IrcMessageHandler
{
IrcMessageHandler(singletons::ResourceManager &resourceManager);
singletons::ResourceManager &resourceManager;
public:
static IrcMessageHandler &getInstance();
void handleRoomStateMessage(Communi::IrcMessage *message);
void handleClearChatMessage(Communi::IrcMessage *message);
void handleUserStateMessage(Communi::IrcMessage *message);
void handleWhisperMessage(Communi::IrcMessage *message);
void handleUserNoticeMessage(Communi::IrcMessage *message);
void handleModeMessage(Communi::IrcMessage *message);
void handleNoticeMessage(Communi::IrcNoticeMessage *message);
void handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message);
};
} // namespace twitch
} // namespace providers
} // namespace chatterino
+70
View File
@@ -0,0 +1,70 @@
#include "providers/twitch/twitchaccount.hpp"
#include "const.hpp"
#include "util/urlfetch.hpp"
namespace chatterino {
namespace providers {
namespace twitch {
TwitchAccount::TwitchAccount(const QString &_username, const QString &_oauthToken,
const QString &_oauthClient)
: userName(_username)
, oauthClient(_oauthClient)
, oauthToken(_oauthToken)
, _isAnon(_username == ANONYMOUS_USERNAME)
{
}
const QString &TwitchAccount::getUserName() const
{
return this->userName;
}
const QString &TwitchAccount::getOAuthClient() const
{
return this->oauthClient;
}
const QString &TwitchAccount::getOAuthToken() const
{
return this->oauthToken;
}
const QString &TwitchAccount::getUserId() const
{
return this->userId;
}
void TwitchAccount::setUserId(const QString &id)
{
this->userId = id;
}
bool TwitchAccount::setOAuthClient(const QString &newClientID)
{
if (this->oauthClient.compare(newClientID) == 0) {
return false;
}
this->oauthClient = newClientID;
return true;
}
bool TwitchAccount::setOAuthToken(const QString &newOAuthToken)
{
if (this->oauthToken.compare(newOAuthToken) == 0) {
return false;
}
this->oauthToken = newOAuthToken;
return true;
}
bool TwitchAccount::isAnon() const
{
return this->_isAnon;
}
} // namespace twitch
} // namespace providers
} // namespace chatterino
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <QString>
namespace chatterino {
namespace providers {
namespace twitch {
class TwitchAccount
{
public:
TwitchAccount(const QString &username, const QString &oauthToken, const QString &oauthClient);
const QString &getUserName() const;
const QString &getOAuthToken() const;
const QString &getOAuthClient() const;
const QString &getUserId() const;
void setUserId(const QString &id);
// Attempts to update the users OAuth Client ID
// Returns true if the value has changed, otherwise false
bool setOAuthClient(const QString &newClientID);
// Attempts to update the users OAuth Token
// Returns true if the value has changed, otherwise false
bool setOAuthToken(const QString &newOAuthToken);
bool isAnon() const;
private:
QString oauthClient;
QString oauthToken;
QString userId;
QString userName;
const bool _isAnon;
};
} // namespace twitch
} // namespace providers
} // namespace chatterino
@@ -0,0 +1,193 @@
#include "providers/twitch/twitchaccountmanager.hpp"
#include "common.hpp"
#include "const.hpp"
#include "debug/log.hpp"
namespace chatterino {
namespace providers {
namespace twitch {
TwitchAccountManager::TwitchAccountManager()
{
this->anonymousUser.reset(new TwitchAccount(ANONYMOUS_USERNAME, "", ""));
this->currentUsername.connect([this](const auto &newValue, auto) {
QString newUsername(QString::fromStdString(newValue));
auto user = this->findUserByUsername(newUsername);
if (user) {
debug::Log("[AccountManager:currentUsernameChanged] User successfully updated to {}",
newUsername);
this->currentUser = user;
} else {
debug::Log(
"[AccountManager:currentUsernameChanged] User successfully updated to anonymous");
this->currentUser = this->anonymousUser;
}
this->userChanged.invoke();
});
}
std::shared_ptr<TwitchAccount> TwitchAccountManager::getCurrent()
{
if (!this->currentUser) {
return this->anonymousUser;
}
return this->currentUser;
}
std::vector<QString> TwitchAccountManager::getUsernames() const
{
std::vector<QString> userNames;
std::lock_guard<std::mutex> lock(this->mutex);
for (const auto &user : this->users) {
userNames.push_back(user->getUserName());
}
return userNames;
}
std::shared_ptr<TwitchAccount> TwitchAccountManager::findUserByUsername(
const QString &username) const
{
std::lock_guard<std::mutex> lock(this->mutex);
for (const auto &user : this->users) {
if (username.compare(user->getUserName(), Qt::CaseInsensitive) == 0) {
return user;
}
}
return nullptr;
}
bool TwitchAccountManager::userExists(const QString &username) const
{
return this->findUserByUsername(username) != nullptr;
}
void TwitchAccountManager::reloadUsers()
{
auto keys = pajlada::Settings::SettingManager::getObjectKeys("/accounts");
UserData userData;
bool listUpdated = false;
for (const auto &uid : keys) {
if (uid == "current") {
continue;
}
std::string username =
pajlada::Settings::Setting<std::string>::get("/accounts/" + uid + "/username");
std::string userID =
pajlada::Settings::Setting<std::string>::get("/accounts/" + uid + "/userID");
std::string clientID =
pajlada::Settings::Setting<std::string>::get("/accounts/" + uid + "/clientID");
std::string oauthToken =
pajlada::Settings::Setting<std::string>::get("/accounts/" + uid + "/oauthToken");
if (username.empty() || userID.empty() || clientID.empty() || oauthToken.empty()) {
continue;
}
userData.username = qS(username);
userData.userID = qS(userID);
userData.clientID = qS(clientID);
userData.oauthToken = qS(oauthToken);
switch (this->addUser(userData)) {
case AddUserResponse::UserAlreadyExists: {
debug::Log("User {} already exists", userData.username);
// Do nothing
} break;
case AddUserResponse::UserValuesUpdated: {
debug::Log("User {} already exists, and values updated!", userData.username);
if (userData.username == this->getCurrent()->getUserName()) {
debug::Log("It was the current user, so we need to reconnect stuff!");
this->userChanged.invoke();
}
} break;
case AddUserResponse::UserAdded: {
debug::Log("Added user {}", userData.username);
listUpdated = true;
} break;
}
}
if (listUpdated) {
this->userListUpdated.invoke();
}
}
bool TwitchAccountManager::removeUser(const QString &username)
{
if (!this->userExists(username)) {
return false;
}
this->mutex.lock();
this->users.erase(std::remove_if(this->users.begin(), this->users.end(), [username](auto user) {
if (user->getUserName() == username) {
std::string userID(user->getUserId().toStdString());
assert(!userID.empty());
pajlada::Settings::SettingManager::removeSetting("/accounts/uid" + userID);
return true;
}
return false;
}));
this->mutex.unlock();
if (username == qS(this->currentUsername.getValue())) {
// The user that was removed is the current user, log into the anonymous user
this->currentUsername = "";
}
this->userListUpdated.invoke();
return true;
}
TwitchAccountManager::AddUserResponse TwitchAccountManager::addUser(
const TwitchAccountManager::UserData &userData)
{
auto previousUser = this->findUserByUsername(userData.username);
if (previousUser) {
bool userUpdated = false;
if (previousUser->setOAuthClient(userData.clientID)) {
userUpdated = true;
}
if (previousUser->setOAuthToken(userData.oauthToken)) {
userUpdated = true;
}
if (userUpdated) {
return AddUserResponse::UserValuesUpdated;
} else {
return AddUserResponse::UserAlreadyExists;
}
}
auto newUser =
std::make_shared<TwitchAccount>(userData.username, userData.oauthToken, userData.clientID);
// Set users User ID without the uid prefix
newUser->setUserId(userData.userID);
std::lock_guard<std::mutex> lock(this->mutex);
this->users.push_back(newUser);
return AddUserResponse::UserAdded;
}
} // namespace twitch
} // namespace providers
} // namespace chatterino
@@ -0,0 +1,67 @@
#pragma once
#include "providers/twitch/twitchaccount.hpp"
#include <pajlada/settings/setting.hpp>
#include <mutex>
#include <vector>
//
// Warning: This class is not supposed to be created directly.
// Get yourself an instance from our friends over at AccountManager.hpp
//
namespace chatterino {
namespace singletons {
class AccountManager;
}
namespace providers {
namespace twitch {
class TwitchAccountManager
{
TwitchAccountManager();
public:
struct UserData {
QString username;
QString userID;
QString clientID;
QString oauthToken;
};
// Returns the current twitchUsers, or the anonymous user if we're not currently logged in
std::shared_ptr<TwitchAccount> getCurrent();
std::vector<QString> getUsernames() const;
std::shared_ptr<TwitchAccount> findUserByUsername(const QString &username) const;
bool userExists(const QString &username) const;
void reloadUsers();
bool removeUser(const QString &username);
pajlada::Settings::Setting<std::string> currentUsername = {"/accounts/current", ""};
pajlada::Signals::NoArgSignal userChanged;
pajlada::Signals::NoArgSignal userListUpdated;
private:
enum class AddUserResponse {
UserAlreadyExists,
UserValuesUpdated,
UserAdded,
};
AddUserResponse addUser(const UserData &data);
std::shared_ptr<TwitchAccount> currentUser;
std::shared_ptr<TwitchAccount> anonymousUser;
std::vector<std::shared_ptr<TwitchAccount>> users;
mutable std::mutex mutex;
friend class chatterino::singletons::AccountManager;
};
} // namespace twitch
} // namespace providers
} // namespace chatterino
+254
View File
@@ -0,0 +1,254 @@
#include "providers/twitch/twitchchannel.hpp"
#include "common.hpp"
#include "debug/log.hpp"
#include "messages/message.hpp"
#include "providers/twitch/twitchmessagebuilder.hpp"
#include "singletons/channelmanager.hpp"
#include "singletons/emotemanager.hpp"
#include "singletons/ircmanager.hpp"
#include "singletons/settingsmanager.hpp"
#include "util/urlfetch.hpp"
#include <IrcConnection>
#include <QThread>
#include <QTimer>
namespace chatterino {
namespace providers {
namespace twitch {
TwitchChannel::TwitchChannel(const QString &channelName, Communi::IrcConnection *_readConnection)
: Channel(channelName)
, bttvChannelEmotes(new util::EmoteMap)
, ffzChannelEmotes(new util::EmoteMap)
, subscriptionURL("https://www.twitch.tv/subs/" + name)
, channelURL("https://twitch.tv/" + name)
, popoutPlayerURL("https://player.twitch.tv/?channel=" + name)
, isLive(false)
, mod(false)
, readConnecetion(_readConnection)
{
debug::Log("[TwitchChannel:{}] Opened", this->name);
this->reloadChannelEmotes();
this->liveStatusTimer = new QTimer;
QObject::connect(this->liveStatusTimer, &QTimer::timeout, [this]() {
this->refreshLiveStatus(); //
});
this->liveStatusTimer->start(60000);
this->roomIDchanged.connect([this]() {
this->refreshLiveStatus(); //
});
this->fetchMessages.connect([this] {
this->fetchRecentMessages(); //
});
this->messageSuffix.append(' ');
this->messageSuffix.append(QChar(0x206D));
}
TwitchChannel::~TwitchChannel()
{
this->connectedConnection.disconnect();
this->liveStatusTimer->stop();
this->liveStatusTimer->deleteLater();
}
bool TwitchChannel::isEmpty() const
{
return this->name.isEmpty();
}
bool TwitchChannel::canSendMessage() const
{
return !this->isEmpty();
}
void TwitchChannel::setRoomID(const QString &_roomID)
{
this->roomID = _roomID;
this->roomIDchanged();
this->fetchMessages.invoke();
}
void TwitchChannel::reloadChannelEmotes()
{
auto &emoteManager = singletons::EmoteManager::getInstance();
debug::Log("[TwitchChannel:{}] Reloading channel emotes", this->name);
emoteManager.reloadBTTVChannelEmotes(this->name, this->bttvChannelEmotes);
emoteManager.reloadFFZChannelEmotes(this->name, this->ffzChannelEmotes);
}
void TwitchChannel::sendMessage(const QString &message)
{
auto &emoteManager = singletons::EmoteManager::getInstance();
debug::Log("[TwitchChannel:{}] Send message: {}", this->name, message);
// Do last message processing
QString parsedMessage = emoteManager.replaceShortCodes(message);
parsedMessage = parsedMessage.trimmed();
if (parsedMessage.isEmpty()) {
return;
}
if (singletons::SettingManager::getInstance().allowDuplicateMessages) {
if (parsedMessage == this->lastSentMessage) {
parsedMessage.append(this->messageSuffix);
this->lastSentMessage = "";
}
}
this->sendMessageSignal.invoke(this->name, parsedMessage);
}
bool TwitchChannel::isMod() const
{
return this->mod;
}
void TwitchChannel::setMod(bool value)
{
if (this->mod != value) {
this->mod = value;
this->userStateChanged();
}
}
bool TwitchChannel::isBroadcaster()
{
return this->name ==
singletons::AccountManager::getInstance().Twitch.getCurrent()->getUserName();
}
bool TwitchChannel::hasModRights()
{
// fourtf: check if staff
return this->isMod() || this->isBroadcaster();
}
void TwitchChannel::setLive(bool newLiveStatus)
{
if (this->isLive == newLiveStatus) {
return;
}
this->isLive = newLiveStatus;
this->onlineStatusChanged();
}
void TwitchChannel::refreshLiveStatus()
{
if (this->roomID.isEmpty()) {
this->setLive(false);
return;
}
debug::Log("[TwitchChannel:{}] Refreshing live status", this->name);
QString url("https://api.twitch.tv/kraken/streams/" + this->roomID);
std::weak_ptr<Channel> weak = this->shared_from_this();
util::twitch::get2(url, QThread::currentThread(), false, [weak](const rapidjson::Document &d) {
ChannelPtr shared = weak.lock();
if (!shared) {
return;
}
TwitchChannel *channel = dynamic_cast<TwitchChannel *>(shared.get());
if (!d.IsObject()) {
debug::Log("[TwitchChannel:refreshLiveStatus] root is not an object");
return;
}
if (!d.HasMember("stream")) {
debug::Log("[TwitchChannel:refreshLiveStatus] Missing stream in root");
return;
}
const auto &stream = d["stream"];
if (!stream.IsObject()) {
// Stream is offline (stream is most likely null)
channel->setLive(false);
return;
}
if (!stream.HasMember("viewers") || !stream.HasMember("game") ||
!stream.HasMember("channel") || !stream.HasMember("created_at")) {
debug::Log("[TwitchChannel:refreshLiveStatus] Missing members in stream");
channel->setLive(false);
return;
}
const rapidjson::Value &streamChannel = stream["channel"];
if (!streamChannel.IsObject() || !streamChannel.HasMember("status")) {
debug::Log("[TwitchChannel:refreshLiveStatus] Missing member \"status\" in channel");
return;
}
// Stream is live
channel->streamViewerCount = QString::number(stream["viewers"].GetInt());
channel->streamGame = stream["game"].GetString();
channel->streamStatus = streamChannel["status"].GetString();
QDateTime since = QDateTime::fromString(stream["created_at"].GetString(), Qt::ISODate);
auto diff = since.secsTo(QDateTime::currentDateTime());
channel->streamUptime =
QString::number(diff / 3600) + "h " + QString::number(diff % 3600 / 60) + "m";
channel->setLive(true);
});
}
void TwitchChannel::fetchRecentMessages()
{
static QString genericURL =
"https://tmi.twitch.tv/api/rooms/%1/recent_messages?client_id=" + getDefaultClientID();
std::weak_ptr<Channel> weak = this->shared_from_this();
util::twitch::get(genericURL.arg(roomID), QThread::currentThread(), [weak](QJsonObject obj) {
ChannelPtr shared = weak.lock();
if (!shared) {
return;
}
TwitchChannel *channel = dynamic_cast<TwitchChannel *>(shared.get());
static auto readConnection = channel->readConnecetion;
auto msgArray = obj.value("messages").toArray();
if (msgArray.size() > 0) {
std::vector<messages::MessagePtr> messages;
for (int i = 0; i < msgArray.size(); i++) {
QByteArray content = msgArray[i].toString().toUtf8();
auto msg = Communi::IrcMessage::fromData(content, readConnection);
auto privMsg = static_cast<Communi::IrcPrivateMessage *>(msg);
messages::MessageParseArgs args;
twitch::TwitchMessageBuilder builder(channel, privMsg, args);
if (!builder.isIgnored()) {
messages.push_back(builder.build());
}
}
channel->addMessagesAtStart(messages);
}
});
}
} // namespace twitch
} // namespace providers
} // namespace chatterino
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include <IrcConnection>
#include "channel.hpp"
#include "common.hpp"
#include "singletons/emotemanager.hpp"
#include "singletons/ircmanager.hpp"
#include "util/concurrentmap.hpp"
namespace chatterino {
namespace providers {
namespace twitch {
class TwitchServer;
class TwitchChannel final : public Channel
{
QTimer *liveStatusTimer;
public:
~TwitchChannel();
void reloadChannelEmotes();
bool isEmpty() const override;
bool canSendMessage() const override;
void sendMessage(const QString &message) override;
bool isMod() const override;
void setMod(bool value);
bool isBroadcaster();
bool hasModRights();
const std::shared_ptr<chatterino::util::EmoteMap> bttvChannelEmotes;
const std::shared_ptr<chatterino::util::EmoteMap> ffzChannelEmotes;
const QString subscriptionURL;
const QString channelURL;
const QString popoutPlayerURL;
void setRoomID(const QString &_roomID);
boost::signals2::signal<void()> roomIDchanged;
boost::signals2::signal<void()> onlineStatusChanged;
pajlada::Signals::NoArgBoltSignal fetchMessages;
boost::signals2::signal<void()> userStateChanged;
QString roomID;
bool isLive;
QString streamViewerCount;
QString streamStatus;
QString streamGame;
QString streamUptime;
private:
explicit TwitchChannel(const QString &channelName, Communi::IrcConnection *readConnection);
void setLive(bool newLiveStatus);
void refreshLiveStatus();
void fetchRecentMessages();
boost::signals2::connection connectedConnection;
bool mod;
QByteArray messageSuffix;
QString lastSentMessage;
Communi::IrcConnection *readConnecetion;
friend class TwitchServer;
};
} // namespace twitch
} // namespace providers
} // namespace chatterino
@@ -0,0 +1,712 @@
#include "providers/twitch/twitchmessagebuilder.hpp"
#include "debug/log.hpp"
#include "providers/twitch/twitchchannel.hpp"
#include "singletons/emotemanager.hpp"
#include "singletons/ircmanager.hpp"
#include "singletons/resourcemanager.hpp"
#include "singletons/settingsmanager.hpp"
#include "singletons/thememanager.hpp"
#include "singletons/windowmanager.hpp"
#include <QApplication>
#include <QDebug>
#include <QMediaPlayer>
using namespace chatterino::messages;
namespace chatterino {
namespace providers {
namespace twitch {
TwitchMessageBuilder::TwitchMessageBuilder(Channel *_channel,
const Communi::IrcPrivateMessage *_ircMessage,
const messages::MessageParseArgs &_args)
: channel(_channel)
, twitchChannel(dynamic_cast<TwitchChannel *>(_channel))
, ircMessage(_ircMessage)
, args(_args)
, tags(this->ircMessage->tags())
, usernameColor(singletons::ThemeManager::getInstance().messages.textColors.system)
{
this->originalMessage = this->ircMessage->content();
}
bool TwitchMessageBuilder::isIgnored() const
{
singletons::SettingManager &settings = singletons::SettingManager::getInstance();
std::shared_ptr<std::vector<QString>> ignoredKeywords = settings.getIgnoredKeywords();
for (const QString &keyword : *ignoredKeywords) {
if (this->originalMessage.contains(keyword, Qt::CaseInsensitive)) {
return true;
}
}
return false;
}
MessagePtr TwitchMessageBuilder::build()
{
singletons::SettingManager &settings = singletons::SettingManager::getInstance();
singletons::EmoteManager &emoteManager = singletons::EmoteManager::getInstance();
// PARSING
this->parseUsername();
// this->message->setCollapsedDefault(true);
// this->appendWord(Word(Resources::getInstance().badgeCollapsed, Word::Collapsed, QString(),
// QString()));
// PARSING
this->parseMessageID();
this->parseRoomID();
this->appendChannelName();
// timestamp
bool isPastMsg = this->tags.contains("historical");
if (isPastMsg) {
// This may be architecture dependent(datatype)
qint64 ts = this->tags.value("tmi-sent-ts").toLongLong();
QDateTime dateTime = QDateTime::fromMSecsSinceEpoch(ts);
this->emplace<TimestampElement>(dateTime.time());
} else {
this->emplace<TimestampElement>();
}
this->emplace<TwitchModerationElement>();
this->appendTwitchBadges();
this->appendChatterinoBadges();
this->appendUsername();
// highlights
if (settings.enableHighlights && !isPastMsg) {
this->parseHighlights();
}
QString bits;
auto iterator = this->tags.find("bits");
if (iterator != this->tags.end()) {
bits = iterator.value().toString();
}
// twitch emotes
std::vector<std::pair<long, util::EmoteData>> twitchEmotes;
iterator = this->tags.find("emotes");
if (iterator != this->tags.end()) {
QStringList emoteString = iterator.value().toString().split('/');
for (QString emote : emoteString) {
this->appendTwitchEmote(ircMessage, emote, twitchEmotes);
}
struct {
bool operator()(const std::pair<long, util::EmoteData> &lhs,
const std::pair<long, util::EmoteData> &rhs)
{
return lhs.first < rhs.first;
}
} customLess;
std::sort(twitchEmotes.begin(), twitchEmotes.end(), customLess);
}
auto currentTwitchEmote = twitchEmotes.begin();
// words
QStringList splits = this->originalMessage.split(' ');
long int i = 0;
for (QString split : splits) {
MessageColor textColor = ircMessage->isAction() ? MessageColor(this->usernameColor)
: MessageColor(MessageColor::Text);
// twitch emote
if (currentTwitchEmote != twitchEmotes.end() && currentTwitchEmote->first == i) {
auto emoteImage = currentTwitchEmote->second;
this->emplace<EmoteElement>(emoteImage, MessageElement::TwitchEmote);
i += split.length() + 1;
currentTwitchEmote = std::next(currentTwitchEmote);
continue;
}
// split words
std::vector<std::tuple<util::EmoteData, QString>> parsed;
// Parse emojis and take all non-emojis and put them in parsed as full text-words
emoteManager.parseEmojis(parsed, split);
for (const auto &tuple : parsed) {
const util::EmoteData &emoteData = std::get<0>(tuple);
if (!emoteData.isValid()) { // is text
QString string = std::get<1>(tuple);
if (!bits.isEmpty() && this->tryParseCheermote(string)) {
// This string was parsed as a cheermote
continue;
}
// TODO: Implement ignored emotes
// Format of ignored emotes:
// Emote name: "forsenPuke" - if string in ignoredEmotes
// Will match emote regardless of source (i.e. bttv, ffz)
// Emote source + name: "bttv:nyanPls"
if (this->tryAppendEmote(string)) {
// Successfully appended an emote
continue;
}
// Actually just text
QString linkString = this->matchLink(string);
Link link;
if (linkString.isEmpty()) {
link = Link();
} else {
link = Link(Link::Url, linkString);
textColor = MessageColor(MessageColor::Link);
}
this->emplace<TextElement>(string, MessageElement::Text, textColor) //
->setLink(link);
} else { // is emoji
this->emplace<EmoteElement>(emoteData, EmoteElement::EmojiAll);
}
}
for (int j = 0; j < split.size(); j++) {
i++;
if (split.at(j).isHighSurrogate()) {
j++;
}
}
i++;
}
this->message->searchText = this->userName + ": " + this->originalMessage;
return this->getMessage();
}
void TwitchMessageBuilder::parseMessageID()
{
auto iterator = this->tags.find("id");
if (iterator != this->tags.end()) {
this->messageID = iterator.value().toString();
}
}
void TwitchMessageBuilder::parseRoomID()
{
if (this->twitchChannel == nullptr) {
return;
}
auto iterator = this->tags.find("room-id");
if (iterator != std::end(this->tags)) {
this->roomID = iterator.value().toString();
if (this->twitchChannel->roomID.isEmpty()) {
this->twitchChannel->roomID = this->roomID;
}
}
}
void TwitchMessageBuilder::appendChannelName()
{
QString channelName("#" + this->channel->name);
Link link(Link::Url, this->channel->name + "\n" + this->messageID);
this->emplace<TextElement>(channelName, MessageElement::ChannelName, MessageColor::System) //
->setLink(link);
}
void TwitchMessageBuilder::parseUsername()
{
auto iterator = this->tags.find("color");
if (iterator != this->tags.end()) {
this->usernameColor = QColor(iterator.value().toString());
}
// username
this->userName = ircMessage->nick();
if (this->userName.isEmpty()) {
this->userName = this->tags.value(QLatin1String("login")).toString();
}
this->message->loginName = this->userName;
}
void TwitchMessageBuilder::appendUsername()
{
QString username = this->userName;
this->message->loginName = username;
QString localizedName;
auto iterator = this->tags.find("display-name");
if (iterator != this->tags.end()) {
QString displayName = iterator.value().toString();
if (QString::compare(displayName, this->userName, Qt::CaseInsensitive) == 0) {
username = displayName;
this->message->displayName = displayName;
} else {
localizedName = displayName;
this->message->displayName = username;
this->message->localizedName = displayName;
}
}
bool hasLocalizedName = !localizedName.isEmpty();
// The full string that will be rendered in the chat widget
QString usernameText;
pajlada::Settings::Setting<int> usernameDisplayMode(
"/appearance/messages/usernameDisplayMode", UsernameDisplayMode::UsernameAndLocalizedName);
switch (usernameDisplayMode.getValue()) {
case UsernameDisplayMode::Username: {
usernameText = username;
} break;
case UsernameDisplayMode::LocalizedName: {
if (hasLocalizedName) {
usernameText = localizedName;
} else {
usernameText = username;
}
} break;
default:
case UsernameDisplayMode::UsernameAndLocalizedName: {
if (hasLocalizedName) {
usernameText = username + "(" + localizedName + ")";
} else {
usernameText = username;
}
} break;
}
if (this->args.isSentWhisper) {
// TODO(pajlada): Re-implement
// userDisplayString += IrcManager::getInstance().getUser().getUserName();
}
if (this->args.isReceivedWhisper) {
// TODO(pajlada): Re-implement
// userDisplayString += " -> " + IrcManager::getInstance().getUser().getUserName();
}
if (!ircMessage->isAction()) {
usernameText += ":";
}
this->emplace<TextElement>(usernameText, MessageElement::Text, this->usernameColor,
FontStyle::MediumBold)
->setLink({Link::UserInfo, this->userName});
}
void TwitchMessageBuilder::parseHighlights()
{
static auto player = new QMediaPlayer;
static QUrl currentPlayerUrl;
singletons::SettingManager &settings = singletons::SettingManager::getInstance();
static pajlada::Settings::Setting<std::string> currentUser("/accounts/current");
QString currentUsername = QString::fromStdString(currentUser.getValue());
if (this->ircMessage->nick() == currentUsername) {
// Do nothing. Highlights cannot be triggered by yourself
return;
}
// update the media player url if necessary
QUrl highlightSoundUrl;
if (settings.customHighlightSound) {
highlightSoundUrl = QUrl(settings.pathHighlightSound.getValue());
} else {
highlightSoundUrl = QUrl("qrc:/sounds/ping2.wav");
}
if (currentPlayerUrl != highlightSoundUrl) {
player->setMedia(highlightSoundUrl);
currentPlayerUrl = highlightSoundUrl;
}
QStringList blackList =
settings.highlightUserBlacklist.getValue().split("\n", QString::SkipEmptyParts);
// TODO: This vector should only be rebuilt upon highlights being changed
auto activeHighlights = settings.highlightProperties.getValue();
if (settings.enableHighlightsSelf && currentUsername.size() > 0) {
messages::HighlightPhrase selfHighlight;
selfHighlight.key = currentUsername;
selfHighlight.sound = settings.enableHighlightSound;
selfHighlight.alert = settings.enableHighlightTaskbar;
activeHighlights.emplace_back(std::move(selfHighlight));
}
bool doHighlight = false;
bool playSound = false;
bool doAlert = false;
bool hasFocus = (QApplication::focusWidget() != nullptr);
if (!blackList.contains(this->ircMessage->nick(), Qt::CaseInsensitive)) {
for (const messages::HighlightPhrase &highlight : activeHighlights) {
if (this->originalMessage.contains(highlight.key, Qt::CaseInsensitive)) {
debug::Log("Highlight because {} contains {}", this->originalMessage,
highlight.key);
doHighlight = true;
if (highlight.sound) {
playSound = true;
}
if (highlight.alert) {
doAlert = true;
}
if (playSound && doAlert) {
// Break if no further action can be taken from other highlights
// This might change if highlights can have custom colors/sounds/actions
break;
}
}
}
this->setHighlight(doHighlight);
if (playSound && (!hasFocus || settings.highlightAlwaysPlaySound)) {
player->play();
}
if (doAlert) {
QApplication::alert(singletons::WindowManager::getInstance().getMainWindow().window(),
2500);
}
if (doHighlight) {
this->message->flags &= Message::Highlighted;
}
}
}
void TwitchMessageBuilder::appendTwitchEmote(const Communi::IrcPrivateMessage *ircMessage,
const QString &emote,
std::vector<std::pair<long int, util::EmoteData>> &vec)
{
singletons::EmoteManager &emoteManager = singletons::EmoteManager::getInstance();
if (!emote.contains(':')) {
return;
}
QStringList parameters = emote.split(':');
if (parameters.length() < 2) {
return;
}
long int id = std::stol(parameters.at(0).toStdString(), nullptr, 10);
QStringList occurences = parameters.at(1).split(',');
for (QString occurence : occurences) {
QStringList coords = occurence.split('-');
if (coords.length() < 2) {
return;
}
long int start = std::stol(coords.at(0).toStdString(), nullptr, 10);
long int end = std::stol(coords.at(1).toStdString(), nullptr, 10);
if (start >= end || start < 0 || end > ircMessage->content().length()) {
return;
}
QString name = ircMessage->content().mid(start, end - start + 1);
vec.push_back(
std::pair<long int, util::EmoteData>(start, emoteManager.getTwitchEmoteById(id, name)));
}
}
bool TwitchMessageBuilder::tryAppendEmote(QString &emoteString)
{
singletons::EmoteManager &emoteManager = singletons::EmoteManager::getInstance();
util::EmoteData emoteData;
auto appendEmote = [&](MessageElement::Flags flags) {
this->emplace<EmoteElement>(emoteData, flags);
return true;
};
if (emoteManager.bttvGlobalEmotes.tryGet(emoteString, emoteData)) {
// BTTV Global Emote
return appendEmote(MessageElement::BttvEmote);
} else if (this->twitchChannel != nullptr &&
this->twitchChannel->bttvChannelEmotes->tryGet(emoteString, emoteData)) {
// BTTV Channel Emote
return appendEmote(MessageElement::BttvEmote);
} else if (emoteManager.ffzGlobalEmotes.tryGet(emoteString, emoteData)) {
// FFZ Global Emote
return appendEmote(MessageElement::FfzEmote);
} else if (this->twitchChannel != nullptr &&
this->twitchChannel->ffzChannelEmotes->tryGet(emoteString, emoteData)) {
// FFZ Channel Emote
return appendEmote(MessageElement::FfzEmote);
} else if (emoteManager.getChatterinoEmotes().tryGet(emoteString, emoteData)) {
// Chatterino Emote
return appendEmote(MessageElement::Misc);
}
return false;
}
// fourtf: this is ugly
// maybe put the individual badges into a map instead of this mess
void TwitchMessageBuilder::appendTwitchBadges()
{
singletons::ResourceManager &resourceManager = singletons::ResourceManager::getInstance();
const auto &channelResources = resourceManager.channels[this->roomID];
auto iterator = this->tags.find("badges");
if (iterator == this->tags.end()) {
// No badges in this message
return;
}
QStringList badges = iterator.value().toString().split(',');
for (QString badge : badges) {
if (badge.isEmpty()) {
continue;
}
if (badge.startsWith("bits/")) {
if (!singletons::ResourceManager::getInstance().dynamicBadgesLoaded) {
// Do nothing
continue;
}
QString cheerAmountQS = badge.mid(5);
std::string versionKey = cheerAmountQS.toStdString();
// Try to fetch channel-specific bit badge
try {
const auto &badge = channelResources.badgeSets.at("bits").versions.at(versionKey);
this->emplace<ImageElement>(badge.badgeImage1x, MessageElement::BadgeVanity);
continue;
} catch (const std::out_of_range &) {
// Channel does not contain a special bit badge for this version
}
// Use default bit badge
try {
const auto &badge = resourceManager.badgeSets.at("bits").versions.at(versionKey);
this->emplace<ImageElement>(badge.badgeImage1x, MessageElement::BadgeVanity);
} catch (const std::out_of_range &) {
debug::Log("No default bit badge for version {} found", versionKey);
continue;
}
} else if (badge == "staff/1") {
this->emplace<ImageElement>(resourceManager.badgeStaff,
MessageElement::BadgeGlobalAuthority)
->setTooltip("Twitch Staff");
} else if (badge == "admin/1") {
this->emplace<ImageElement>(resourceManager.badgeAdmin,
MessageElement::BadgeGlobalAuthority)
->setTooltip("Twitch Admin");
} else if (badge == "global_mod/1") {
this->emplace<ImageElement>(resourceManager.badgeGlobalModerator,
MessageElement::BadgeGlobalAuthority)
->setTooltip("Twitch Global Moderator");
} else if (badge == "moderator/1") {
// TODO: Implement custom FFZ moderator badge
this->emplace<ImageElement>(resourceManager.badgeModerator,
MessageElement::BadgeChannelAuthority)
->setTooltip("Twitch Channel Moderator");
} else if (badge == "turbo/1") {
this->emplace<ImageElement>(resourceManager.badgeTurbo,
MessageElement::BadgeGlobalAuthority)
->setTooltip("Twitch Turbo Subscriber");
} else if (badge == "broadcaster/1") {
this->emplace<ImageElement>(resourceManager.badgeBroadcaster,
MessageElement::BadgeChannelAuthority)
->setTooltip("Twitch Broadcaster");
} else if (badge == "premium/1") {
this->emplace<ImageElement>(resourceManager.badgePremium, MessageElement::BadgeVanity)
->setTooltip("Twitch Prime Subscriber");
} else if (badge.startsWith("partner/")) {
int index = badge.midRef(8).toInt();
switch (index) {
case 1: {
this->emplace<ImageElement>(resourceManager.badgeVerified,
MessageElement::BadgeVanity)
->setTooltip("Twitch Verified");
} break;
default: {
printf("[TwitchMessageBuilder] Unhandled partner badge index: %d\n", index);
} break;
}
} else if (badge.startsWith("subscriber/")) {
if (channelResources.loaded == false) {
qDebug() << "Channel resources are not loaded, can't add the subscriber badge";
continue;
}
auto badgeSetIt = channelResources.badgeSets.find("subscriber");
if (badgeSetIt == channelResources.badgeSets.end()) {
// Fall back to default badge
this->emplace<ImageElement>(resourceManager.badgeSubscriber,
MessageElement::BadgeSubscription)
->setTooltip("Twitch Subscriber");
continue;
}
const auto &badgeSet = badgeSetIt->second;
std::string versionKey = badge.mid(11).toStdString();
auto badgeVersionIt = badgeSet.versions.find(versionKey);
if (badgeVersionIt == badgeSet.versions.end()) {
// Fall back to default badge
this->emplace<ImageElement>(resourceManager.badgeSubscriber,
MessageElement::BadgeSubscription)
->setTooltip("Twitch Subscriber");
continue;
}
auto &badgeVersion = badgeVersionIt->second;
this->emplace<ImageElement>(badgeVersion.badgeImage1x,
MessageElement::BadgeSubscription)
->setTooltip("Twitch " + QString::fromStdString(badgeVersion.title));
} else {
if (!resourceManager.dynamicBadgesLoaded) {
// Do nothing
continue;
}
QStringList parts = badge.split('/');
if (parts.length() != 2) {
qDebug() << "Bad number of parts: " << parts.length() << " in " << parts;
continue;
}
MessageElement::Flags badgeType = MessageElement::Flags::BadgeVanity;
std::string badgeSetKey = parts[0].toStdString();
std::string versionKey = parts[1].toStdString();
try {
auto &badgeSet = resourceManager.badgeSets.at(badgeSetKey);
try {
auto &badgeVersion = badgeSet.versions.at(versionKey);
this->emplace<ImageElement>(badgeVersion.badgeImage1x, badgeType)
->setTooltip("Twitch " + QString::fromStdString(badgeVersion.title));
} catch (const std::exception &e) {
qDebug() << "Exception caught:" << e.what()
<< "when trying to fetch badge version " << versionKey.c_str();
}
} catch (const std::exception &e) {
qDebug() << "No badge set with key" << badgeSetKey.c_str()
<< ". Exception: " << e.what();
}
}
}
}
void TwitchMessageBuilder::appendChatterinoBadges()
{
auto &badges = singletons::ResourceManager::getInstance().chatterinoBadges;
auto it = badges.find(this->userName.toStdString());
if (it == badges.end()) {
return;
}
const auto badge = it->second;
this->emplace<ImageElement>(badge->image, MessageElement::BadgeChatterino)
->setTooltip(QString::fromStdString(badge->tooltip));
}
bool TwitchMessageBuilder::tryParseCheermote(const QString &string)
{
// Try to parse custom cheermotes
const auto &channelResources =
singletons::ResourceManager::getInstance().channels[this->roomID];
if (channelResources.loaded) {
for (const auto &cheermoteSet : channelResources.cheermoteSets) {
auto match = cheermoteSet.regex.match(string);
if (!match.hasMatch()) {
continue;
}
QString amount = match.captured(1);
bool ok = false;
int numBits = amount.toInt(&ok);
if (!ok) {
debug::Log("Error parsing bit amount in tryParseCheermote");
return false;
}
auto savedIt = cheermoteSet.cheermotes.end();
// Fetch cheermote that matches our numBits
for (auto it = cheermoteSet.cheermotes.begin(); it != cheermoteSet.cheermotes.end();
++it) {
if (numBits >= it->minBits) {
savedIt = it;
} else {
break;
}
}
if (savedIt == cheermoteSet.cheermotes.end()) {
debug::Log("Error getting a cheermote from a cheermote set for the bit amount {}",
numBits);
return false;
}
const auto &cheermote = *savedIt;
this->emplace<EmoteElement>(cheermote.emoteDataAnimated, EmoteElement::BitsAnimated);
this->emplace<TextElement>(amount, EmoteElement::Text, cheermote.color);
return true;
}
}
return false;
}
} // namespace twitch
} // namespace providers
} // namespace chatterino
@@ -0,0 +1,69 @@
#pragma once
#include "messages/messagebuilder.hpp"
#include "messages/messageparseargs.hpp"
#include "singletons/emotemanager.hpp"
#include <IrcMessage>
#include <QString>
#include <QVariant>
namespace chatterino {
class Channel;
namespace providers {
namespace twitch {
class TwitchChannel;
class TwitchMessageBuilder : public messages::MessageBuilder
{
public:
enum UsernameDisplayMode : int {
Username = 1, // Username
LocalizedName = 2, // Localized name
UsernameAndLocalizedName = 3, // Username (Localized name)
};
TwitchMessageBuilder() = delete;
explicit TwitchMessageBuilder(Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
const messages::MessageParseArgs &_args);
Channel *channel;
TwitchChannel *twitchChannel;
const Communi::IrcPrivateMessage *ircMessage;
messages::MessageParseArgs args;
const QVariantMap tags;
QString messageID;
QString userName;
bool isIgnored() const;
messages::MessagePtr build();
private:
QString roomID;
QColor usernameColor;
QString originalMessage;
void parseMessageID();
void parseRoomID();
void appendChannelName();
void parseUsername();
void appendUsername();
void parseHighlights();
void appendTwitchEmote(const Communi::IrcPrivateMessage *ircMessage, const QString &emote,
std::vector<std::pair<long, util::EmoteData>> &vec);
bool tryAppendEmote(QString &emoteString);
void appendTwitchBadges();
void appendChatterinoBadges();
bool tryParseCheermote(const QString &string);
};
} // namespace twitch
} // namespace providers
} // namespace chatterino
+148
View File
@@ -0,0 +1,148 @@
#include "twitchserver.hpp"
#include "providers/twitch/ircmessagehandler.hpp"
#include "providers/twitch/twitchaccount.hpp"
#include "providers/twitch/twitchmessagebuilder.hpp"
#include "singletons/accountmanager.hpp"
#include "util/posttothread.hpp"
#include <cassert>
using namespace Communi;
using namespace chatterino::singletons;
namespace chatterino {
namespace providers {
namespace twitch {
TwitchServer::TwitchServer()
: whispersChannel(new Channel("/mentions"))
, mentionsChannel(new Channel("/mentions"))
{
AccountManager::getInstance().Twitch.userChanged.connect([this]() { //
util::postToThread([this] { this->connect(); });
});
}
TwitchServer &TwitchServer::getInstance()
{
static TwitchServer s;
return s;
}
void TwitchServer::initializeConnection(IrcConnection *connection, bool isRead, bool isWrite)
{
std::shared_ptr<TwitchAccount> account = AccountManager::getInstance().Twitch.getCurrent();
QString username = account->getUserName();
// QString oauthClient = account->getOAuthClient();
QString oauthToken = account->getOAuthToken();
if (!oauthToken.startsWith("oauth:")) {
oauthToken.prepend("oauth:");
}
connection->setUserName(username);
connection->setNickName(username);
connection->setRealName(username);
if (!account->isAnon()) {
connection->setPassword(oauthToken);
// fourtf: ignored users
// this->refreshIgnoredUsers(username, oauthClient, oauthToken);
}
connection->sendCommand(IrcCommand::createCapability("REQ", "twitch.tv/membership"));
connection->sendCommand(IrcCommand::createCapability("REQ", "twitch.tv/commands"));
connection->sendCommand(IrcCommand::createCapability("REQ", "twitch.tv/tags"));
connection->setHost("irc.chat.twitch.tv");
connection->setPort(6667);
}
std::shared_ptr<Channel> TwitchServer::createChannel(const QString &channelName)
{
TwitchChannel *channel = new TwitchChannel(channelName, this->getReadConnection());
channel->sendMessageSignal.connect(
[this](auto chan, auto msg) { this->sendMessage(chan, msg); });
return std::shared_ptr<Channel>(channel);
}
void TwitchServer::privateMessageReceived(IrcPrivateMessage *message)
{
this->onPrivateMessage.invoke(message);
auto chan = TwitchServer::getInstance().getChannel(message->target().mid(1));
if (!chan) {
return;
}
messages::MessageParseArgs args;
TwitchMessageBuilder builder(chan.get(), message, args);
if (!builder.isIgnored()) {
messages::MessagePtr _message = builder.build();
if (_message->flags & messages::Message::Highlighted) {
TwitchServer::getInstance().mentionsChannel->addMessage(_message);
}
chan->addMessage(_message);
}
}
void TwitchServer::messageReceived(IrcMessage *message)
{
// this->readConnection
if (message->type() == IrcMessage::Type::Private) {
// We already have a handler for private messages
return;
}
const QString &command = message->command();
if (command == "ROOMSTATE") {
IrcMessageHandler::getInstance().handleRoomStateMessage(message);
} else if (command == "CLEARCHAT") {
IrcMessageHandler::getInstance().handleClearChatMessage(message);
} else if (command == "USERSTATE") {
IrcMessageHandler::getInstance().handleUserStateMessage(message);
} else if (command == "WHISPER") {
IrcMessageHandler::getInstance().handleWhisperMessage(message);
} else if (command == "USERNOTICE") {
IrcMessageHandler::getInstance().handleUserNoticeMessage(message);
} else if (command == "MODE") {
IrcMessageHandler::getInstance().handleModeMessage(message);
} else if (command == "NOTICE") {
IrcMessageHandler::getInstance().handleNoticeMessage(
static_cast<IrcNoticeMessage *>(message));
}
}
void TwitchServer::writeConnectionMessageReceived(IrcMessage *message)
{
switch (message->type()) {
case IrcMessage::Type::Notice: {
IrcMessageHandler::getInstance().handleWriteConnectionNoticeMessage(
static_cast<IrcNoticeMessage *>(message));
} break;
}
}
std::shared_ptr<Channel> TwitchServer::getCustomChannel(const QString &channelName)
{
if (channelName == "/whispers") {
return whispersChannel;
}
if (channelName == "/mentions") {
return mentionsChannel;
}
return nullptr;
}
} // namespace twitch
} // namespace providers
} // namespace chatterino
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <memory>
#include "providers/irc/abstractircserver.hpp"
#include "providers/twitch/twitchaccount.hpp"
#include "providers/twitch/twitchchannel.hpp"
namespace chatterino {
namespace providers {
namespace twitch {
class TwitchServer final : public irc::AbstractIrcServer
{
TwitchServer();
public:
static TwitchServer &getInstance();
const ChannelPtr whispersChannel;
const ChannelPtr mentionsChannel;
protected:
virtual void initializeConnection(Communi::IrcConnection *connection, bool isRead,
bool isWrite) override;
virtual std::shared_ptr<Channel> createChannel(const QString &channelName) override;
virtual void privateMessageReceived(Communi::IrcPrivateMessage *message) override;
virtual void messageReceived(Communi::IrcMessage *message) override;
virtual void writeConnectionMessageReceived(Communi::IrcMessage *message) override;
virtual std::shared_ptr<Channel> getCustomChannel(const QString &channelname) override;
};
} // namespace twitch
} // namespace providers
} // namespace chatterino