Merge branch 'master' into irc-support
This commit is contained in:
@@ -48,9 +48,15 @@ void ChatroomChannel::refreshFFZChannelEmotes()
|
||||
{
|
||||
return;
|
||||
}
|
||||
FfzEmotes::loadChannel(this->chatroomOwnerId, [this](auto &&emoteMap) {
|
||||
this->ffzEmotes_.set(std::make_shared<EmoteMap>(std::move(emoteMap)));
|
||||
});
|
||||
FfzEmotes::loadChannel(
|
||||
this->chatroomOwnerId,
|
||||
[this](auto &&emoteMap) {
|
||||
this->ffzEmotes_.set(
|
||||
std::make_shared<EmoteMap>(std::move(emoteMap)));
|
||||
},
|
||||
[this](auto &&modBadge) {
|
||||
this->ffzCustomModBadge_.set(std::move(modBadge));
|
||||
});
|
||||
}
|
||||
|
||||
const QString &ChatroomChannel::getDisplayName() const
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,58 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <IrcMessage>
|
||||
#include "messages/Message.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class TwitchIrcServer;
|
||||
class Channel;
|
||||
|
||||
class IrcMessageHandler
|
||||
{
|
||||
IrcMessageHandler() = default;
|
||||
|
||||
public:
|
||||
static IrcMessageHandler &getInstance();
|
||||
|
||||
// parseMessage parses a single IRC message into 0+ Chatterino messages
|
||||
std::vector<MessagePtr> parseMessage(Channel *channel,
|
||||
Communi::IrcMessage *message);
|
||||
|
||||
// parsePrivMessage arses a single IRC PRIVMSG into 0-1 Chatterino messages
|
||||
std::vector<MessagePtr> parsePrivMessage(
|
||||
Channel *channel, Communi::IrcPrivateMessage *message);
|
||||
void handlePrivMessage(Communi::IrcPrivateMessage *message,
|
||||
TwitchIrcServer &server);
|
||||
|
||||
void handleRoomStateMessage(Communi::IrcMessage *message);
|
||||
void handleClearChatMessage(Communi::IrcMessage *message);
|
||||
void handleClearMessageMessage(Communi::IrcMessage *message);
|
||||
void handleUserStateMessage(Communi::IrcMessage *message);
|
||||
void handleWhisperMessage(Communi::IrcMessage *message);
|
||||
|
||||
// parseUserNoticeMessage parses a single IRC USERNOTICE message into 0+
|
||||
// chatterino messages
|
||||
std::vector<MessagePtr> parseUserNoticeMessage(
|
||||
Channel *channel, Communi::IrcMessage *message);
|
||||
void handleUserNoticeMessage(Communi::IrcMessage *message,
|
||||
TwitchIrcServer &server);
|
||||
|
||||
void handleModeMessage(Communi::IrcMessage *message);
|
||||
|
||||
// parseNoticeMessage parses a single IRC NOTICE message into 0+ chatterino
|
||||
// messages
|
||||
std::vector<MessagePtr> parseNoticeMessage(
|
||||
Communi::IrcNoticeMessage *message);
|
||||
void handleNoticeMessage(Communi::IrcNoticeMessage *message);
|
||||
|
||||
void handleJoinMessage(Communi::IrcMessage *message);
|
||||
void handlePartMessage(Communi::IrcMessage *message);
|
||||
|
||||
private:
|
||||
void addMessage(Communi::IrcMessage *message, const QString &target,
|
||||
const QString &content, TwitchIrcServer &server, bool isResub,
|
||||
bool isAction);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
#pragma once
|
||||
|
||||
#include <IrcMessage>
|
||||
#include "messages/Message.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class TwitchIrcServer;
|
||||
class Channel;
|
||||
|
||||
class IrcMessageHandler
|
||||
{
|
||||
IrcMessageHandler() = default;
|
||||
|
||||
public:
|
||||
static IrcMessageHandler &getInstance();
|
||||
|
||||
// parseMessage parses a single IRC message into 0+ Chatterino messages
|
||||
std::vector<MessagePtr> parseMessage(Channel *channel,
|
||||
Communi::IrcMessage *message);
|
||||
|
||||
// parsePrivMessage arses a single IRC PRIVMSG into 0-1 Chatterino messages
|
||||
std::vector<MessagePtr> parsePrivMessage(
|
||||
Channel *channel, Communi::IrcPrivateMessage *message);
|
||||
void handlePrivMessage(Communi::IrcPrivateMessage *message,
|
||||
TwitchIrcServer &server);
|
||||
|
||||
void handleRoomStateMessage(Communi::IrcMessage *message);
|
||||
void handleClearChatMessage(Communi::IrcMessage *message);
|
||||
void handleClearMessageMessage(Communi::IrcMessage *message);
|
||||
void handleUserStateMessage(Communi::IrcMessage *message);
|
||||
void handleWhisperMessage(Communi::IrcMessage *message);
|
||||
|
||||
// parseUserNoticeMessage parses a single IRC USERNOTICE message into 0+
|
||||
// chatterino messages
|
||||
std::vector<MessagePtr> parseUserNoticeMessage(
|
||||
Channel *channel, Communi::IrcMessage *message);
|
||||
void handleUserNoticeMessage(Communi::IrcMessage *message,
|
||||
TwitchIrcServer &server);
|
||||
|
||||
void handleModeMessage(Communi::IrcMessage *message);
|
||||
|
||||
// parseNoticeMessage parses a single IRC NOTICE message into 0+ chatterino
|
||||
// messages
|
||||
std::vector<MessagePtr> parseNoticeMessage(
|
||||
Communi::IrcNoticeMessage *message);
|
||||
void handleNoticeMessage(Communi::IrcNoticeMessage *message);
|
||||
|
||||
void handleJoinMessage(Communi::IrcMessage *message);
|
||||
void handlePartMessage(Communi::IrcMessage *message);
|
||||
|
||||
private:
|
||||
void addMessage(Communi::IrcMessage *message, const QString &target,
|
||||
const QString &content, TwitchIrcServer &server,
|
||||
bool isResub, bool isAction);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -95,7 +95,7 @@ void TwitchAccount::loadIgnores()
|
||||
"/blocks");
|
||||
|
||||
NetworkRequest(url)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onSuccess([=](auto result) -> Outcome {
|
||||
auto document = result.parseRapidJson();
|
||||
@@ -168,7 +168,7 @@ void TwitchAccount::ignoreByID(
|
||||
"/blocks/" + targetUserID);
|
||||
|
||||
NetworkRequest(url, NetworkRequestType::Put)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
onFinished(IgnoreResult_Failed,
|
||||
@@ -245,7 +245,7 @@ void TwitchAccount::unignoreByID(
|
||||
"/blocks/" + targetUserID);
|
||||
|
||||
NetworkRequest(url, NetworkRequestType::Delete)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
onFinished(
|
||||
@@ -279,7 +279,7 @@ void TwitchAccount::checkFollow(const QString targetUserID,
|
||||
"/follows/channels/" + targetUserID);
|
||||
|
||||
NetworkRequest(url)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
if (errorCode == 203)
|
||||
@@ -308,7 +308,7 @@ void TwitchAccount::followUser(const QString userID,
|
||||
"/follows/channels/" + userID);
|
||||
|
||||
NetworkRequest(requestUrl, NetworkRequestType::Put)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onSuccess([successCallback](auto result) -> Outcome {
|
||||
// TODO: Properly check result of follow request
|
||||
@@ -326,7 +326,7 @@ void TwitchAccount::unfollowUser(const QString userID,
|
||||
"/follows/channels/" + userID);
|
||||
|
||||
NetworkRequest(requestUrl, NetworkRequestType::Delete)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([successCallback](int code) {
|
||||
if (code >= 200 && code <= 299)
|
||||
@@ -368,7 +368,7 @@ void TwitchAccount::loadEmotes()
|
||||
"/emotes");
|
||||
|
||||
NetworkRequest(url)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
log("[TwitchAccount::loadEmotes] Error {}", errorCode);
|
||||
@@ -409,7 +409,7 @@ void TwitchAccount::autoModAllow(const QString msgID)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Content-Length", QByteArray::number(qba.size()))
|
||||
.payload(qba)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
log("[TwitchAccounts::autoModAllow] Error {}", errorCode);
|
||||
@@ -429,7 +429,7 @@ void TwitchAccount::autoModDeny(const QString msgID)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Content-Length", QByteArray::number(qba.size()))
|
||||
.payload(qba)
|
||||
|
||||
|
||||
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
|
||||
.onError([=](int errorCode) {
|
||||
log("[TwitchAccounts::autoModDeny] Error {}", errorCode);
|
||||
|
||||
@@ -1,234 +1,234 @@
|
||||
#include "providers/twitch/TwitchAccountManager.hpp"
|
||||
|
||||
#include "common/Common.hpp"
|
||||
#include "debug/Log.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
TwitchAccountManager::TwitchAccountManager()
|
||||
: anonymousUser_(new TwitchAccount(ANONYMOUS_USERNAME, "", "", ""))
|
||||
{
|
||||
this->currentUserChanged.connect([this] {
|
||||
auto currentUser = this->getCurrent();
|
||||
currentUser->loadIgnores();
|
||||
});
|
||||
|
||||
this->accounts.itemRemoved.connect([this](const auto &acc) { //
|
||||
this->removeUser(acc.item.get());
|
||||
});
|
||||
}
|
||||
|
||||
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->accounts)
|
||||
{
|
||||
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->accounts)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
auto username = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/username");
|
||||
auto userID = pajlada::Settings::Setting<QString>::get("/accounts/" +
|
||||
uid + "/userID");
|
||||
auto clientID = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/clientID");
|
||||
auto oauthToken = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/oauthToken");
|
||||
|
||||
if (username.isEmpty() || userID.isEmpty() || clientID.isEmpty() ||
|
||||
oauthToken.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
userData.username = username.trimmed();
|
||||
userData.userID = userID.trimmed();
|
||||
userData.clientID = clientID.trimmed();
|
||||
userData.oauthToken = oauthToken.trimmed();
|
||||
|
||||
switch (this->addUser(userData))
|
||||
{
|
||||
case AddUserResponse::UserAlreadyExists:
|
||||
{
|
||||
log("User {} already exists", userData.username);
|
||||
// Do nothing
|
||||
}
|
||||
break;
|
||||
case AddUserResponse::UserValuesUpdated:
|
||||
{
|
||||
log("User {} already exists, and values updated!",
|
||||
userData.username);
|
||||
if (userData.username == this->getCurrent()->getUserName())
|
||||
{
|
||||
log("It was the current user, so we need to reconnect "
|
||||
"stuff!");
|
||||
this->currentUserChanged.invoke();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AddUserResponse::UserAdded:
|
||||
{
|
||||
log("Added user {}", userData.username);
|
||||
listUpdated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (listUpdated)
|
||||
{
|
||||
this->userListUpdated.invoke();
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchAccountManager::load()
|
||||
{
|
||||
this->reloadUsers();
|
||||
|
||||
this->currentUsername.connect([this](const QString &newUsername) {
|
||||
auto user = this->findUserByUsername(newUsername);
|
||||
if (user)
|
||||
{
|
||||
log("[AccountManager:currentUsernameChanged] User successfully "
|
||||
"updated to {}",
|
||||
newUsername);
|
||||
this->currentUser_ = user;
|
||||
}
|
||||
else
|
||||
{
|
||||
log("[AccountManager:currentUsernameChanged] User successfully "
|
||||
"updated to anonymous");
|
||||
this->currentUser_ = this->anonymousUser_;
|
||||
}
|
||||
|
||||
this->currentUserChanged.invoke();
|
||||
});
|
||||
}
|
||||
|
||||
bool TwitchAccountManager::isLoggedIn() const
|
||||
{
|
||||
if (!this->currentUser_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Once `TwitchAccount` class has a way to check, we should also return
|
||||
// false if the credentials are incorrect
|
||||
return !this->currentUser_->isAnon();
|
||||
}
|
||||
|
||||
bool TwitchAccountManager::removeUser(TwitchAccount *account)
|
||||
{
|
||||
auto userID(account->getUserId());
|
||||
if (!userID.isEmpty())
|
||||
{
|
||||
pajlada::Settings::SettingManager::removeSetting(
|
||||
fS("/accounts/uid{}", userID));
|
||||
}
|
||||
|
||||
if (account->getUserName() == this->currentUsername)
|
||||
{
|
||||
// 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, userData.userID);
|
||||
|
||||
// std::lock_guard<std::mutex> lock(this->mutex);
|
||||
|
||||
this->accounts.insertItem(newUser);
|
||||
|
||||
return AddUserResponse::UserAdded;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#include "providers/twitch/TwitchAccountManager.hpp"
|
||||
|
||||
#include "common/Common.hpp"
|
||||
#include "debug/Log.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchCommon.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
TwitchAccountManager::TwitchAccountManager()
|
||||
: anonymousUser_(new TwitchAccount(ANONYMOUS_USERNAME, "", "", ""))
|
||||
{
|
||||
this->currentUserChanged.connect([this] {
|
||||
auto currentUser = this->getCurrent();
|
||||
currentUser->loadIgnores();
|
||||
});
|
||||
|
||||
this->accounts.itemRemoved.connect([this](const auto &acc) { //
|
||||
this->removeUser(acc.item.get());
|
||||
});
|
||||
}
|
||||
|
||||
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->accounts)
|
||||
{
|
||||
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->accounts)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
auto username = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/username");
|
||||
auto userID = pajlada::Settings::Setting<QString>::get("/accounts/" +
|
||||
uid + "/userID");
|
||||
auto clientID = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/clientID");
|
||||
auto oauthToken = pajlada::Settings::Setting<QString>::get(
|
||||
"/accounts/" + uid + "/oauthToken");
|
||||
|
||||
if (username.isEmpty() || userID.isEmpty() || clientID.isEmpty() ||
|
||||
oauthToken.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
userData.username = username.trimmed();
|
||||
userData.userID = userID.trimmed();
|
||||
userData.clientID = clientID.trimmed();
|
||||
userData.oauthToken = oauthToken.trimmed();
|
||||
|
||||
switch (this->addUser(userData))
|
||||
{
|
||||
case AddUserResponse::UserAlreadyExists:
|
||||
{
|
||||
log("User {} already exists", userData.username);
|
||||
// Do nothing
|
||||
}
|
||||
break;
|
||||
case AddUserResponse::UserValuesUpdated:
|
||||
{
|
||||
log("User {} already exists, and values updated!",
|
||||
userData.username);
|
||||
if (userData.username == this->getCurrent()->getUserName())
|
||||
{
|
||||
log("It was the current user, so we need to reconnect "
|
||||
"stuff!");
|
||||
this->currentUserChanged.invoke();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AddUserResponse::UserAdded:
|
||||
{
|
||||
log("Added user {}", userData.username);
|
||||
listUpdated = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (listUpdated)
|
||||
{
|
||||
this->userListUpdated.invoke();
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchAccountManager::load()
|
||||
{
|
||||
this->reloadUsers();
|
||||
|
||||
this->currentUsername.connect([this](const QString &newUsername) {
|
||||
auto user = this->findUserByUsername(newUsername);
|
||||
if (user)
|
||||
{
|
||||
log("[AccountManager:currentUsernameChanged] User successfully "
|
||||
"updated to {}",
|
||||
newUsername);
|
||||
this->currentUser_ = user;
|
||||
}
|
||||
else
|
||||
{
|
||||
log("[AccountManager:currentUsernameChanged] User successfully "
|
||||
"updated to anonymous");
|
||||
this->currentUser_ = this->anonymousUser_;
|
||||
}
|
||||
|
||||
this->currentUserChanged.invoke();
|
||||
});
|
||||
}
|
||||
|
||||
bool TwitchAccountManager::isLoggedIn() const
|
||||
{
|
||||
if (!this->currentUser_)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Once `TwitchAccount` class has a way to check, we should also return
|
||||
// false if the credentials are incorrect
|
||||
return !this->currentUser_->isAnon();
|
||||
}
|
||||
|
||||
bool TwitchAccountManager::removeUser(TwitchAccount *account)
|
||||
{
|
||||
auto userID(account->getUserId());
|
||||
if (!userID.isEmpty())
|
||||
{
|
||||
pajlada::Settings::SettingManager::removeSetting(
|
||||
fS("/accounts/uid{}", userID));
|
||||
}
|
||||
|
||||
if (account->getUserName() == this->currentUsername)
|
||||
{
|
||||
// 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, userData.userID);
|
||||
|
||||
// std::lock_guard<std::mutex> lock(this->mutex);
|
||||
|
||||
this->accounts.insertItem(newUser);
|
||||
|
||||
return AddUserResponse::UserAdded;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/ChatterinoSetting.hpp"
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "util/SharedPtrElementLess.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 {
|
||||
|
||||
class TwitchAccount;
|
||||
class AccountController;
|
||||
|
||||
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();
|
||||
void load();
|
||||
|
||||
bool isLoggedIn() const;
|
||||
|
||||
pajlada::Settings::Setting<QString> currentUsername{"/accounts/current",
|
||||
""};
|
||||
pajlada::Signals::NoArgSignal currentUserChanged;
|
||||
pajlada::Signals::NoArgSignal userListUpdated;
|
||||
|
||||
SortedSignalVector<std::shared_ptr<TwitchAccount>,
|
||||
SharedPtrElementLess<TwitchAccount>>
|
||||
accounts;
|
||||
|
||||
private:
|
||||
enum class AddUserResponse {
|
||||
UserAlreadyExists,
|
||||
UserValuesUpdated,
|
||||
UserAdded,
|
||||
};
|
||||
AddUserResponse addUser(const UserData &data);
|
||||
bool removeUser(TwitchAccount *account);
|
||||
|
||||
std::shared_ptr<TwitchAccount> currentUser_;
|
||||
|
||||
std::shared_ptr<TwitchAccount> anonymousUser_;
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
friend class AccountController;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
#pragma once
|
||||
|
||||
#include "common/ChatterinoSetting.hpp"
|
||||
#include "common/SignalVector.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "util/SharedPtrElementLess.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 {
|
||||
|
||||
class TwitchAccount;
|
||||
class AccountController;
|
||||
|
||||
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();
|
||||
void load();
|
||||
|
||||
bool isLoggedIn() const;
|
||||
|
||||
pajlada::Settings::Setting<QString> currentUsername{"/accounts/current",
|
||||
""};
|
||||
pajlada::Signals::NoArgSignal currentUserChanged;
|
||||
pajlada::Signals::NoArgSignal userListUpdated;
|
||||
|
||||
SortedSignalVector<std::shared_ptr<TwitchAccount>,
|
||||
SharedPtrElementLess<TwitchAccount>>
|
||||
accounts;
|
||||
|
||||
private:
|
||||
enum class AddUserResponse {
|
||||
UserAlreadyExists,
|
||||
UserValuesUpdated,
|
||||
UserAdded,
|
||||
};
|
||||
AddUserResponse addUser(const UserData &data);
|
||||
bool removeUser(TwitchAccount *account);
|
||||
|
||||
std::shared_ptr<TwitchAccount> currentUser_;
|
||||
|
||||
std::shared_ptr<TwitchAccount> anonymousUser_;
|
||||
mutable std::mutex mutex_;
|
||||
|
||||
friend class AccountController;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -16,7 +16,7 @@ void TwitchApi::findUserId(const QString user,
|
||||
QString requestUrl("https://api.twitch.tv/kraken/users?login=" + user);
|
||||
|
||||
NetworkRequest(requestUrl)
|
||||
|
||||
|
||||
.authorizeTwitchV5(getDefaultClientID())
|
||||
.timeout(30000)
|
||||
.onSuccess([successCallback](auto result) mutable -> Outcome {
|
||||
@@ -65,7 +65,7 @@ void TwitchApi::findUserName(const QString userid,
|
||||
QString requestUrl("https://api.twitch.tv/kraken/users/" + userid);
|
||||
|
||||
NetworkRequest(requestUrl)
|
||||
|
||||
|
||||
.authorizeTwitchV5(getDefaultClientID())
|
||||
.timeout(30000)
|
||||
.onSuccess([successCallback](auto result) mutable -> Outcome {
|
||||
|
||||
@@ -18,7 +18,7 @@ void TwitchBadges::loadTwitchBadges()
|
||||
"https://badges.twitch.tv/v1/badges/global/display?language=en");
|
||||
|
||||
NetworkRequest(url)
|
||||
|
||||
|
||||
.onSuccess([this](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
auto badgeSets = this->badgeSets_.access();
|
||||
|
||||
@@ -88,7 +88,6 @@ TwitchChannel::TwitchChannel(const QString &name,
|
||||
, globalFfz_(ffz)
|
||||
, bttvEmotes_(std::make_shared<EmoteMap>())
|
||||
, ffzEmotes_(std::make_shared<EmoteMap>())
|
||||
, ffzCustomModBadge_(name)
|
||||
, mod_(false)
|
||||
{
|
||||
log("[TwitchChannel:{}] Opened", name);
|
||||
@@ -139,7 +138,6 @@ void TwitchChannel::initialize()
|
||||
{
|
||||
this->refreshChatters();
|
||||
this->refreshBadges();
|
||||
this->ffzCustomModBadge_.loadCustomModBadge();
|
||||
}
|
||||
|
||||
bool TwitchChannel::isEmpty() const
|
||||
@@ -165,10 +163,17 @@ void TwitchChannel::refreshBTTVChannelEmotes()
|
||||
void TwitchChannel::refreshFFZChannelEmotes()
|
||||
{
|
||||
FfzEmotes::loadChannel(
|
||||
this->roomId(), [this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
|
||||
this->roomId(),
|
||||
[this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
|
||||
if (auto shared = weak.lock())
|
||||
this->ffzEmotes_.set(
|
||||
std::make_shared<EmoteMap>(std::move(emoteMap)));
|
||||
},
|
||||
[this, weak = weakOf<Channel>(this)](auto &&modBadge) {
|
||||
if (auto shared = weak.lock())
|
||||
{
|
||||
this->ffzCustomModBadge_.set(std::move(modBadge));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -685,22 +690,23 @@ void TwitchChannel::refreshBadges()
|
||||
|
||||
void TwitchChannel::refreshCheerEmotes()
|
||||
{
|
||||
/*auto url = Url{"https://api.twitch.tv/kraken/bits/actions?channel_id=" +
|
||||
this->getRoomId()};
|
||||
auto request = NetworkRequest::twitchRequest(url.string);
|
||||
request.setCaller(QThread::currentThread());
|
||||
|
||||
request.onSuccess(
|
||||
[this, weak = weakOf<Channel>(this)](auto result) -> Outcome {
|
||||
QString url("https://api.twitch.tv/kraken/bits/actions?channel_id=" +
|
||||
this->roomId());
|
||||
NetworkRequest::twitchRequest(url)
|
||||
.onSuccess([this,
|
||||
weak = weakOf<Channel>(this)](auto result) -> Outcome {
|
||||
auto cheerEmoteSets = ParseCheermoteSets(result.parseRapidJson());
|
||||
std::vector<CheerEmoteSet> emoteSets;
|
||||
|
||||
for (auto &set : cheerEmoteSets) {
|
||||
for (auto &set : cheerEmoteSets)
|
||||
{
|
||||
auto cheerEmoteSet = CheerEmoteSet();
|
||||
cheerEmoteSet.regex = QRegularExpression(
|
||||
"^" + set.prefix.toLower() + "([1-9][0-9]*)$");
|
||||
"^" + set.prefix + "([1-9][0-9]*)$",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
for (auto &tier : set.tiers) {
|
||||
for (auto &tier : set.tiers)
|
||||
{
|
||||
CheerEmote cheerEmote;
|
||||
|
||||
cheerEmote.color = QColor(tier.color);
|
||||
@@ -733,7 +739,7 @@ void TwitchChannel::refreshCheerEmotes()
|
||||
std::sort(cheerEmoteSet.cheerEmotes.begin(),
|
||||
cheerEmoteSet.cheerEmotes.end(),
|
||||
[](const auto &lhs, const auto &rhs) {
|
||||
return lhs.minBits < rhs.minBits; //
|
||||
return lhs.minBits > rhs.minBits;
|
||||
});
|
||||
|
||||
emoteSets.emplace_back(cheerEmoteSet);
|
||||
@@ -741,10 +747,8 @@ void TwitchChannel::refreshCheerEmotes()
|
||||
*this->cheerEmoteSets_.access() = std::move(emoteSets);
|
||||
|
||||
return Success;
|
||||
});
|
||||
|
||||
request.execute();
|
||||
*/
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
boost::optional<EmotePtr> TwitchChannel::twitchBadge(
|
||||
@@ -765,9 +769,34 @@ boost::optional<EmotePtr> TwitchChannel::twitchBadge(
|
||||
|
||||
boost::optional<EmotePtr> TwitchChannel::ffzCustomModBadge() const
|
||||
{
|
||||
if (auto badge = this->ffzCustomModBadge_.badge())
|
||||
return badge;
|
||||
return this->ffzCustomModBadge_.get();
|
||||
}
|
||||
|
||||
boost::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string)
|
||||
{
|
||||
auto sets = this->cheerEmoteSets_.access();
|
||||
for (const auto &set : *sets)
|
||||
{
|
||||
auto match = set.regex.match(string);
|
||||
if (!match.hasMatch())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
QString amount = match.captured(1);
|
||||
bool ok = false;
|
||||
int bitAmount = amount.toInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
log("Error parsing bit amount in cheerEmote");
|
||||
}
|
||||
for (const auto &emote : set.cheerEmotes)
|
||||
{
|
||||
if (bitAmount >= emote.minBits)
|
||||
{
|
||||
return emote;
|
||||
}
|
||||
}
|
||||
}
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include "common/ChannelChatters.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "providers/ffz/FfzModBadge.hpp"
|
||||
#include "common/UsernameSet.hpp"
|
||||
#include "providers/twitch/TwitchEmotes.hpp"
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
@@ -96,6 +96,9 @@ public:
|
||||
boost::optional<EmotePtr> twitchBadge(const QString &set,
|
||||
const QString &version) const;
|
||||
|
||||
// Cheers
|
||||
boost::optional<CheerEmote> cheerEmote(const QString &string);
|
||||
|
||||
// Signals
|
||||
pajlada::Signals::NoArgSignal roomIdChanged;
|
||||
pajlada::Signals::NoArgSignal userStateChanged;
|
||||
@@ -145,13 +148,13 @@ protected:
|
||||
FfzEmotes &globalFfz_;
|
||||
Atomic<std::shared_ptr<const EmoteMap>> bttvEmotes_;
|
||||
Atomic<std::shared_ptr<const EmoteMap>> ffzEmotes_;
|
||||
Atomic<boost::optional<EmotePtr>> ffzCustomModBadge_;
|
||||
|
||||
private:
|
||||
// Badges
|
||||
UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>>
|
||||
badgeSets_; // "subscribers": { "0": ... "3": ... "6": ...
|
||||
UniqueAccess<std::vector<CheerEmoteSet>> cheerEmoteSets_;
|
||||
FfzModBadge ffzCustomModBadge_;
|
||||
|
||||
bool mod_ = false;
|
||||
bool vip_ = false;
|
||||
|
||||
@@ -1,373 +1,373 @@
|
||||
#include "TwitchIrcServer.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Common.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/highlights/HighlightController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/twitch/ChatroomChannel.hpp"
|
||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||
#include "providers/twitch/PubsubClient.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchHelpers.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "util/PostToThread.hpp"
|
||||
|
||||
#include <IrcCommand>
|
||||
#include <cassert>
|
||||
|
||||
// using namespace Communi;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
bool isChatroom(const QString &channel)
|
||||
{
|
||||
if (channel.left(10) == "chatrooms:")
|
||||
{
|
||||
auto reflist = channel.splitRef(':');
|
||||
if (reflist.size() == 3)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TwitchIrcServer::TwitchIrcServer()
|
||||
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
|
||||
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
|
||||
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
|
||||
{
|
||||
this->pubsub = new PubSub;
|
||||
|
||||
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
|
||||
// this->connect(); },
|
||||
// this->signalHolder_,
|
||||
// false);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
getApp()->accounts->twitch.currentUserChanged.connect(
|
||||
[this]() { postToThread([this] { this->connect(); }); });
|
||||
|
||||
this->twitchBadges.loadTwitchBadges();
|
||||
this->bttv.loadEmotes();
|
||||
this->ffz.loadEmotes();
|
||||
}
|
||||
|
||||
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type)
|
||||
{
|
||||
std::shared_ptr<TwitchAccount> account =
|
||||
getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
qDebug() << "logging in as" << account->getUserName();
|
||||
|
||||
QString username = account->getUserName();
|
||||
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);
|
||||
}
|
||||
|
||||
connection->setSecure(true);
|
||||
|
||||
// https://dev.twitch.tv/docs/irc/guide/#connecting-to-twitch-irc
|
||||
// SSL disabled: irc://irc.chat.twitch.tv:6667
|
||||
// SSL enabled: irc://irc.chat.twitch.tv:6697
|
||||
connection->setHost("irc.chat.twitch.tv");
|
||||
connection->setPort(6697);
|
||||
|
||||
this->open(type);
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::createChannel(
|
||||
const QString &channelName)
|
||||
{
|
||||
std::shared_ptr<TwitchChannel> channel;
|
||||
if (isChatroom(channelName))
|
||||
{
|
||||
channel = std::static_pointer_cast<TwitchChannel>(
|
||||
std::shared_ptr<ChatroomChannel>(new ChatroomChannel(
|
||||
channelName, this->twitchBadges, this->bttv, this->ffz)));
|
||||
}
|
||||
else
|
||||
{
|
||||
channel = std::shared_ptr<TwitchChannel>(new TwitchChannel(
|
||||
channelName, this->twitchBadges, this->bttv, this->ffz));
|
||||
}
|
||||
channel->initialize();
|
||||
|
||||
channel->sendMessageSignal.connect(
|
||||
[this, channel = channel.get()](auto &chan, auto &msg, bool &sent) {
|
||||
this->onMessageSendRequested(channel, msg, sent);
|
||||
});
|
||||
|
||||
return std::shared_ptr<Channel>(channel);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::privateMessageReceived(
|
||||
Communi::IrcPrivateMessage *message)
|
||||
{
|
||||
IrcMessageHandler::getInstance().handlePrivMessage(message, *this);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::readConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
AbstractIrcServer::readConnectionMessageReceived(message);
|
||||
|
||||
if (message->type() == Communi::IrcMessage::Type::Private)
|
||||
{
|
||||
// We already have a handler for private messages
|
||||
return;
|
||||
}
|
||||
|
||||
const QString &command = message->command();
|
||||
|
||||
auto &handler = IrcMessageHandler::getInstance();
|
||||
|
||||
// Below commands enabled through the twitch.tv/membership CAP REQ
|
||||
if (command == "MODE")
|
||||
{
|
||||
handler.handleModeMessage(message);
|
||||
}
|
||||
else if (command == "JOIN")
|
||||
{
|
||||
handler.handleJoinMessage(message);
|
||||
}
|
||||
else if (command == "PART")
|
||||
{
|
||||
handler.handlePartMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchIrcServer::writeConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
const QString &command = message->command();
|
||||
|
||||
auto &handler = IrcMessageHandler::getInstance();
|
||||
|
||||
// Below commands enabled through the twitch.tv/commands CAP REQ
|
||||
if (command == "USERSTATE")
|
||||
{
|
||||
handler.handleUserStateMessage(message);
|
||||
}
|
||||
else if (command == "WHISPER")
|
||||
{
|
||||
handler.handleWhisperMessage(message);
|
||||
}
|
||||
else if (command == "USERNOTICE")
|
||||
{
|
||||
handler.handleUserNoticeMessage(message, *this);
|
||||
}
|
||||
else if (command == "ROOMSTATE")
|
||||
{
|
||||
handler.handleRoomStateMessage(message);
|
||||
}
|
||||
else if (command == "CLEARCHAT")
|
||||
{
|
||||
handler.handleClearChatMessage(message);
|
||||
}
|
||||
else if (command == "CLEARMSG")
|
||||
{
|
||||
handler.handleClearMessageMessage(message);
|
||||
}
|
||||
else if (command == "NOTICE")
|
||||
{
|
||||
handler.handleNoticeMessage(
|
||||
static_cast<Communi::IrcNoticeMessage *>(message));
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onReadConnected(IrcConnection *connection)
|
||||
{
|
||||
AbstractIrcServer::onReadConnected(connection);
|
||||
|
||||
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
|
||||
// twitch.tv/membership enables the JOIN/PART/MODE/NAMES commands. See https://dev.twitch.tv/docs/irc/membership/
|
||||
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/membership");
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onWriteConnected(IrcConnection *connection)
|
||||
{
|
||||
AbstractIrcServer::onWriteConnected(connection);
|
||||
|
||||
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
|
||||
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands/
|
||||
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/commands");
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
|
||||
const QString &channelName)
|
||||
{
|
||||
if (channelName == "/whispers")
|
||||
{
|
||||
return this->whispersChannel;
|
||||
}
|
||||
|
||||
if (channelName == "/mentions")
|
||||
{
|
||||
return this->mentionsChannel;
|
||||
}
|
||||
|
||||
if (channelName == "$$$")
|
||||
{
|
||||
static auto channel =
|
||||
std::make_shared<Channel>("$$$", chatterino::Channel::Type::Misc);
|
||||
static auto getTimer = [&] {
|
||||
for (auto i = 0; i < 1000; i++)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(QString::number(i + 1)));
|
||||
}
|
||||
|
||||
auto timer = new QTimer;
|
||||
QObject::connect(timer, &QTimer::timeout, [] {
|
||||
channel->addMessage(
|
||||
makeSystemMessage(QTime::currentTime().toString()));
|
||||
});
|
||||
timer->start(500);
|
||||
return timer;
|
||||
}();
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TwitchIrcServer::forEachChannelAndSpecialChannels(
|
||||
std::function<void(ChannelPtr)> func)
|
||||
{
|
||||
this->forEachChannel(func);
|
||||
|
||||
func(this->whispersChannel);
|
||||
func(this->mentionsChannel);
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
|
||||
const QString &channelId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
for (const auto &weakChannel : this->channels)
|
||||
{
|
||||
auto channel = weakChannel.lock();
|
||||
if (!channel)
|
||||
continue;
|
||||
|
||||
auto twitchChannel = std::dynamic_pointer_cast<TwitchChannel>(channel);
|
||||
if (!twitchChannel)
|
||||
continue;
|
||||
|
||||
if (twitchChannel->roomId() == channelId &&
|
||||
twitchChannel->getName().splitRef(":").size() < 3)
|
||||
{
|
||||
return twitchChannel;
|
||||
}
|
||||
}
|
||||
|
||||
return Channel::getEmpty();
|
||||
}
|
||||
|
||||
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
|
||||
{
|
||||
if (dirtyChannelName.startsWith('#'))
|
||||
return dirtyChannelName.mid(1).toLower();
|
||||
else
|
||||
return dirtyChannelName.toLower();
|
||||
}
|
||||
|
||||
bool TwitchIrcServer::hasSeparateWriteConnection() const
|
||||
{
|
||||
return true;
|
||||
// return getSettings()->twitchSeperateWriteConnection;
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel,
|
||||
const QString &message, bool &sent)
|
||||
{
|
||||
sent = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(this->lastMessageMutex_);
|
||||
|
||||
// std::queue<std::chrono::steady_clock::time_point>
|
||||
auto &lastMessage = channel->hasHighRateLimit()
|
||||
? this->lastMessageMod_
|
||||
: this->lastMessagePleb_;
|
||||
size_t maxMessageCount = channel->hasHighRateLimit() ? 99 : 19;
|
||||
auto minMessageOffset = (channel->hasHighRateLimit() ? 100ms : 1100ms);
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// check if you are sending messages too fast
|
||||
if (!lastMessage.empty() && lastMessage.back() + minMessageOffset > now)
|
||||
{
|
||||
if (this->lastErrorTimeSpeed_ + 30s < now)
|
||||
{
|
||||
auto errorMessage =
|
||||
makeSystemMessage("sending messages too fast");
|
||||
|
||||
channel->addMessage(errorMessage);
|
||||
|
||||
this->lastErrorTimeSpeed_ = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// remove messages older than 30 seconds
|
||||
while (!lastMessage.empty() && lastMessage.front() + 32s < now)
|
||||
{
|
||||
lastMessage.pop();
|
||||
}
|
||||
|
||||
// check if you are sending too many messages
|
||||
if (lastMessage.size() >= maxMessageCount)
|
||||
{
|
||||
if (this->lastErrorTimeAmount_ + 30s < now)
|
||||
{
|
||||
auto errorMessage =
|
||||
makeSystemMessage("sending too many messages");
|
||||
|
||||
channel->addMessage(errorMessage);
|
||||
|
||||
this->lastErrorTimeAmount_ = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
lastMessage.push(now);
|
||||
}
|
||||
|
||||
this->sendMessage(channel->getName(), message);
|
||||
sent = true;
|
||||
}
|
||||
|
||||
const BttvEmotes &TwitchIrcServer::getBttvEmotes() const
|
||||
{
|
||||
return this->bttv;
|
||||
}
|
||||
const FfzEmotes &TwitchIrcServer::getFfzEmotes() const
|
||||
{
|
||||
return this->ffz;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
#include "TwitchIrcServer.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Common.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/highlights/HighlightController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/twitch/ChatroomChannel.hpp"
|
||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||
#include "providers/twitch/PubsubClient.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchHelpers.hpp"
|
||||
#include "providers/twitch/TwitchMessageBuilder.hpp"
|
||||
#include "util/PostToThread.hpp"
|
||||
|
||||
#include <IrcCommand>
|
||||
#include <cassert>
|
||||
|
||||
// using namespace Communi;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
namespace {
|
||||
bool isChatroom(const QString &channel)
|
||||
{
|
||||
if (channel.left(10) == "chatrooms:")
|
||||
{
|
||||
auto reflist = channel.splitRef(':');
|
||||
if (reflist.size() == 3)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TwitchIrcServer::TwitchIrcServer()
|
||||
: whispersChannel(new Channel("/whispers", Channel::Type::TwitchWhispers))
|
||||
, mentionsChannel(new Channel("/mentions", Channel::Type::TwitchMentions))
|
||||
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
|
||||
{
|
||||
this->pubsub = new PubSub;
|
||||
|
||||
// getSettings()->twitchSeperateWriteConnection.connect([this](auto, auto) {
|
||||
// this->connect(); },
|
||||
// this->signalHolder_,
|
||||
// false);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
getApp()->accounts->twitch.currentUserChanged.connect(
|
||||
[this]() { postToThread([this] { this->connect(); }); });
|
||||
|
||||
this->twitchBadges.loadTwitchBadges();
|
||||
this->bttv.loadEmotes();
|
||||
this->ffz.loadEmotes();
|
||||
}
|
||||
|
||||
void TwitchIrcServer::initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type)
|
||||
{
|
||||
std::shared_ptr<TwitchAccount> account =
|
||||
getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
qDebug() << "logging in as" << account->getUserName();
|
||||
|
||||
QString username = account->getUserName();
|
||||
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);
|
||||
}
|
||||
|
||||
connection->setSecure(true);
|
||||
|
||||
// https://dev.twitch.tv/docs/irc/guide/#connecting-to-twitch-irc
|
||||
// SSL disabled: irc://irc.chat.twitch.tv:6667
|
||||
// SSL enabled: irc://irc.chat.twitch.tv:6697
|
||||
connection->setHost("irc.chat.twitch.tv");
|
||||
connection->setPort(6697);
|
||||
|
||||
this->open(type);
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::createChannel(
|
||||
const QString &channelName)
|
||||
{
|
||||
std::shared_ptr<TwitchChannel> channel;
|
||||
if (isChatroom(channelName))
|
||||
{
|
||||
channel = std::static_pointer_cast<TwitchChannel>(
|
||||
std::shared_ptr<ChatroomChannel>(new ChatroomChannel(
|
||||
channelName, this->twitchBadges, this->bttv, this->ffz)));
|
||||
}
|
||||
else
|
||||
{
|
||||
channel = std::shared_ptr<TwitchChannel>(new TwitchChannel(
|
||||
channelName, this->twitchBadges, this->bttv, this->ffz));
|
||||
}
|
||||
channel->initialize();
|
||||
|
||||
channel->sendMessageSignal.connect(
|
||||
[this, channel = channel.get()](auto &chan, auto &msg, bool &sent) {
|
||||
this->onMessageSendRequested(channel, msg, sent);
|
||||
});
|
||||
|
||||
return std::shared_ptr<Channel>(channel);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::privateMessageReceived(
|
||||
Communi::IrcPrivateMessage *message)
|
||||
{
|
||||
IrcMessageHandler::getInstance().handlePrivMessage(message, *this);
|
||||
}
|
||||
|
||||
void TwitchIrcServer::readConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
AbstractIrcServer::readConnectionMessageReceived(message);
|
||||
|
||||
if (message->type() == Communi::IrcMessage::Type::Private)
|
||||
{
|
||||
// We already have a handler for private messages
|
||||
return;
|
||||
}
|
||||
|
||||
const QString &command = message->command();
|
||||
|
||||
auto &handler = IrcMessageHandler::getInstance();
|
||||
|
||||
// Below commands enabled through the twitch.tv/membership CAP REQ
|
||||
if (command == "MODE")
|
||||
{
|
||||
handler.handleModeMessage(message);
|
||||
}
|
||||
else if (command == "JOIN")
|
||||
{
|
||||
handler.handleJoinMessage(message);
|
||||
}
|
||||
else if (command == "PART")
|
||||
{
|
||||
handler.handlePartMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchIrcServer::writeConnectionMessageReceived(
|
||||
Communi::IrcMessage *message)
|
||||
{
|
||||
const QString &command = message->command();
|
||||
|
||||
auto &handler = IrcMessageHandler::getInstance();
|
||||
|
||||
// Below commands enabled through the twitch.tv/commands CAP REQ
|
||||
if (command == "USERSTATE")
|
||||
{
|
||||
handler.handleUserStateMessage(message);
|
||||
}
|
||||
else if (command == "WHISPER")
|
||||
{
|
||||
handler.handleWhisperMessage(message);
|
||||
}
|
||||
else if (command == "USERNOTICE")
|
||||
{
|
||||
handler.handleUserNoticeMessage(message, *this);
|
||||
}
|
||||
else if (command == "ROOMSTATE")
|
||||
{
|
||||
handler.handleRoomStateMessage(message);
|
||||
}
|
||||
else if (command == "CLEARCHAT")
|
||||
{
|
||||
handler.handleClearChatMessage(message);
|
||||
}
|
||||
else if (command == "CLEARMSG")
|
||||
{
|
||||
handler.handleClearMessageMessage(message);
|
||||
}
|
||||
else if (command == "NOTICE")
|
||||
{
|
||||
handler.handleNoticeMessage(
|
||||
static_cast<Communi::IrcNoticeMessage *>(message));
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onReadConnected(IrcConnection *connection)
|
||||
{
|
||||
AbstractIrcServer::onReadConnected(connection);
|
||||
|
||||
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
|
||||
// twitch.tv/membership enables the JOIN/PART/MODE/NAMES commands. See https://dev.twitch.tv/docs/irc/membership/
|
||||
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/membership");
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onWriteConnected(IrcConnection *connection)
|
||||
{
|
||||
AbstractIrcServer::onWriteConnected(connection);
|
||||
|
||||
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
|
||||
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands/
|
||||
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/commands");
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::getCustomChannel(
|
||||
const QString &channelName)
|
||||
{
|
||||
if (channelName == "/whispers")
|
||||
{
|
||||
return this->whispersChannel;
|
||||
}
|
||||
|
||||
if (channelName == "/mentions")
|
||||
{
|
||||
return this->mentionsChannel;
|
||||
}
|
||||
|
||||
if (channelName == "$$$")
|
||||
{
|
||||
static auto channel =
|
||||
std::make_shared<Channel>("$$$", chatterino::Channel::Type::Misc);
|
||||
static auto getTimer = [&] {
|
||||
for (auto i = 0; i < 1000; i++)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(QString::number(i + 1)));
|
||||
}
|
||||
|
||||
auto timer = new QTimer;
|
||||
QObject::connect(timer, &QTimer::timeout, [] {
|
||||
channel->addMessage(
|
||||
makeSystemMessage(QTime::currentTime().toString()));
|
||||
});
|
||||
timer->start(500);
|
||||
return timer;
|
||||
}();
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void TwitchIrcServer::forEachChannelAndSpecialChannels(
|
||||
std::function<void(ChannelPtr)> func)
|
||||
{
|
||||
this->forEachChannel(func);
|
||||
|
||||
func(this->whispersChannel);
|
||||
func(this->mentionsChannel);
|
||||
}
|
||||
|
||||
std::shared_ptr<Channel> TwitchIrcServer::getChannelOrEmptyByID(
|
||||
const QString &channelId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->channelMutex);
|
||||
|
||||
for (const auto &weakChannel : this->channels)
|
||||
{
|
||||
auto channel = weakChannel.lock();
|
||||
if (!channel)
|
||||
continue;
|
||||
|
||||
auto twitchChannel = std::dynamic_pointer_cast<TwitchChannel>(channel);
|
||||
if (!twitchChannel)
|
||||
continue;
|
||||
|
||||
if (twitchChannel->roomId() == channelId &&
|
||||
twitchChannel->getName().splitRef(":").size() < 3)
|
||||
{
|
||||
return twitchChannel;
|
||||
}
|
||||
}
|
||||
|
||||
return Channel::getEmpty();
|
||||
}
|
||||
|
||||
QString TwitchIrcServer::cleanChannelName(const QString &dirtyChannelName)
|
||||
{
|
||||
if (dirtyChannelName.startsWith('#'))
|
||||
return dirtyChannelName.mid(1).toLower();
|
||||
else
|
||||
return dirtyChannelName.toLower();
|
||||
}
|
||||
|
||||
bool TwitchIrcServer::hasSeparateWriteConnection() const
|
||||
{
|
||||
return true;
|
||||
// return getSettings()->twitchSeperateWriteConnection;
|
||||
}
|
||||
|
||||
void TwitchIrcServer::onMessageSendRequested(TwitchChannel *channel,
|
||||
const QString &message, bool &sent)
|
||||
{
|
||||
sent = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(this->lastMessageMutex_);
|
||||
|
||||
// std::queue<std::chrono::steady_clock::time_point>
|
||||
auto &lastMessage = channel->hasHighRateLimit()
|
||||
? this->lastMessageMod_
|
||||
: this->lastMessagePleb_;
|
||||
size_t maxMessageCount = channel->hasHighRateLimit() ? 99 : 19;
|
||||
auto minMessageOffset = (channel->hasHighRateLimit() ? 100ms : 1100ms);
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// check if you are sending messages too fast
|
||||
if (!lastMessage.empty() && lastMessage.back() + minMessageOffset > now)
|
||||
{
|
||||
if (this->lastErrorTimeSpeed_ + 30s < now)
|
||||
{
|
||||
auto errorMessage =
|
||||
makeSystemMessage("sending messages too fast");
|
||||
|
||||
channel->addMessage(errorMessage);
|
||||
|
||||
this->lastErrorTimeSpeed_ = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// remove messages older than 30 seconds
|
||||
while (!lastMessage.empty() && lastMessage.front() + 32s < now)
|
||||
{
|
||||
lastMessage.pop();
|
||||
}
|
||||
|
||||
// check if you are sending too many messages
|
||||
if (lastMessage.size() >= maxMessageCount)
|
||||
{
|
||||
if (this->lastErrorTimeAmount_ + 30s < now)
|
||||
{
|
||||
auto errorMessage =
|
||||
makeSystemMessage("sending too many messages");
|
||||
|
||||
channel->addMessage(errorMessage);
|
||||
|
||||
this->lastErrorTimeAmount_ = now;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
lastMessage.push(now);
|
||||
}
|
||||
|
||||
this->sendMessage(channel->getName(), message);
|
||||
sent = true;
|
||||
}
|
||||
|
||||
const BttvEmotes &TwitchIrcServer::getBttvEmotes() const
|
||||
{
|
||||
return this->bttv;
|
||||
}
|
||||
const FfzEmotes &TwitchIrcServer::getFfzEmotes() const
|
||||
{
|
||||
return this->ffz;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/Atomic.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/Singleton.hpp"
|
||||
#include "pajlada/signals/signalholder.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/irc/AbstractIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Settings;
|
||||
class Paths;
|
||||
class PubSub;
|
||||
class TwitchChannel;
|
||||
|
||||
class TwitchIrcServer final : public AbstractIrcServer, public Singleton
|
||||
{
|
||||
public:
|
||||
TwitchIrcServer();
|
||||
virtual ~TwitchIrcServer() override = default;
|
||||
|
||||
virtual void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
void forEachChannelAndSpecialChannels(std::function<void(ChannelPtr)> func);
|
||||
|
||||
std::shared_ptr<Channel> getChannelOrEmptyByID(const QString &channelID);
|
||||
|
||||
Atomic<QString> lastUserThatWhisperedMe;
|
||||
|
||||
const ChannelPtr whispersChannel;
|
||||
const ChannelPtr mentionsChannel;
|
||||
IndirectChannel watchingChannel;
|
||||
|
||||
PubSub *pubsub;
|
||||
|
||||
const BttvEmotes &getBttvEmotes() const;
|
||||
const FfzEmotes &getFfzEmotes() const;
|
||||
|
||||
protected:
|
||||
virtual void initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type) override;
|
||||
virtual std::shared_ptr<Channel> createChannel(
|
||||
const QString &channelName) override;
|
||||
|
||||
virtual void privateMessageReceived(
|
||||
Communi::IrcPrivateMessage *message) override;
|
||||
virtual void readConnectionMessageReceived(
|
||||
Communi::IrcMessage *message) override;
|
||||
virtual void writeConnectionMessageReceived(
|
||||
Communi::IrcMessage *message) override;
|
||||
|
||||
virtual void onReadConnected(IrcConnection *connection) override;
|
||||
virtual void onWriteConnected(IrcConnection *connection) override;
|
||||
|
||||
virtual std::shared_ptr<Channel> getCustomChannel(
|
||||
const QString &channelname) override;
|
||||
|
||||
virtual QString cleanChannelName(const QString &dirtyChannelName) override;
|
||||
virtual bool hasSeparateWriteConnection() const override;
|
||||
|
||||
private:
|
||||
void onMessageSendRequested(TwitchChannel *channel, const QString &message,
|
||||
bool &sent);
|
||||
|
||||
std::mutex lastMessageMutex_;
|
||||
std::queue<std::chrono::steady_clock::time_point> lastMessagePleb_;
|
||||
std::queue<std::chrono::steady_clock::time_point> lastMessageMod_;
|
||||
std::chrono::steady_clock::time_point lastErrorTimeSpeed_;
|
||||
std::chrono::steady_clock::time_point lastErrorTimeAmount_;
|
||||
|
||||
TwitchBadges twitchBadges;
|
||||
BttvEmotes bttv;
|
||||
FfzEmotes ffz;
|
||||
|
||||
pajlada::Signals::SignalHolder signalHolder_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
#pragma once
|
||||
|
||||
#include "common/Atomic.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/Singleton.hpp"
|
||||
#include "pajlada/signals/signalholder.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/irc/AbstractIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class Settings;
|
||||
class Paths;
|
||||
class PubSub;
|
||||
class TwitchChannel;
|
||||
|
||||
class TwitchIrcServer final : public AbstractIrcServer, public Singleton
|
||||
{
|
||||
public:
|
||||
TwitchIrcServer();
|
||||
virtual ~TwitchIrcServer() override = default;
|
||||
|
||||
virtual void initialize(Settings &settings, Paths &paths) override;
|
||||
|
||||
void forEachChannelAndSpecialChannels(std::function<void(ChannelPtr)> func);
|
||||
|
||||
std::shared_ptr<Channel> getChannelOrEmptyByID(const QString &channelID);
|
||||
|
||||
Atomic<QString> lastUserThatWhisperedMe;
|
||||
|
||||
const ChannelPtr whispersChannel;
|
||||
const ChannelPtr mentionsChannel;
|
||||
IndirectChannel watchingChannel;
|
||||
|
||||
PubSub *pubsub;
|
||||
|
||||
const BttvEmotes &getBttvEmotes() const;
|
||||
const FfzEmotes &getFfzEmotes() const;
|
||||
|
||||
protected:
|
||||
virtual void initializeConnection(IrcConnection *connection,
|
||||
ConnectionType type) override;
|
||||
virtual std::shared_ptr<Channel> createChannel(
|
||||
const QString &channelName) override;
|
||||
|
||||
virtual void privateMessageReceived(
|
||||
Communi::IrcPrivateMessage *message) override;
|
||||
virtual void readConnectionMessageReceived(
|
||||
Communi::IrcMessage *message) override;
|
||||
virtual void writeConnectionMessageReceived(
|
||||
Communi::IrcMessage *message) override;
|
||||
|
||||
virtual void onReadConnected(IrcConnection *connection) override;
|
||||
virtual void onWriteConnected(IrcConnection *connection) override;
|
||||
|
||||
virtual std::shared_ptr<Channel> getCustomChannel(
|
||||
const QString &channelname) override;
|
||||
|
||||
virtual QString cleanChannelName(const QString &dirtyChannelName) override;
|
||||
virtual bool hasSeparateWriteConnection() const override;
|
||||
|
||||
private:
|
||||
void onMessageSendRequested(TwitchChannel *channel, const QString &message,
|
||||
bool &sent);
|
||||
|
||||
std::mutex lastMessageMutex_;
|
||||
std::queue<std::chrono::steady_clock::time_point> lastMessagePleb_;
|
||||
std::queue<std::chrono::steady_clock::time_point> lastMessageMod_;
|
||||
std::chrono::steady_clock::time_point lastErrorTimeSpeed_;
|
||||
std::chrono::steady_clock::time_point lastErrorTimeAmount_;
|
||||
|
||||
TwitchBadges twitchBadges;
|
||||
BttvEmotes bttv;
|
||||
FfzEmotes ffz;
|
||||
|
||||
pajlada::Signals::SignalHolder signalHolder_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -143,9 +143,21 @@ bool TwitchMessageBuilder::isIgnored() const
|
||||
return false;
|
||||
}
|
||||
|
||||
inline QMediaPlayer *getPlayer()
|
||||
{
|
||||
if (isGuiThread())
|
||||
{
|
||||
static auto player = new QMediaPlayer;
|
||||
return player;
|
||||
}
|
||||
else
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::triggerHighlights()
|
||||
{
|
||||
static auto player = new QMediaPlayer;
|
||||
static QUrl currentPlayerUrl;
|
||||
|
||||
if (this->historicalMessage_)
|
||||
@@ -165,21 +177,24 @@ void TwitchMessageBuilder::triggerHighlights()
|
||||
|
||||
if (this->highlightSound_ && resolveFocus)
|
||||
{
|
||||
// update the media player url if necessary
|
||||
QUrl highlightSoundUrl =
|
||||
getSettings()->customHighlightSound
|
||||
? QUrl::fromLocalFile(
|
||||
getSettings()->pathHighlightSound.getValue())
|
||||
: QUrl("qrc:/sounds/ping2.wav");
|
||||
|
||||
if (currentPlayerUrl != highlightSoundUrl)
|
||||
if (auto player = getPlayer())
|
||||
{
|
||||
player->setMedia(highlightSoundUrl);
|
||||
// update the media player url if necessary
|
||||
QUrl highlightSoundUrl =
|
||||
getSettings()->customHighlightSound
|
||||
? QUrl::fromLocalFile(
|
||||
getSettings()->pathHighlightSound.getValue())
|
||||
: QUrl("qrc:/sounds/ping2.wav");
|
||||
|
||||
currentPlayerUrl = highlightSoundUrl;
|
||||
if (currentPlayerUrl != highlightSoundUrl)
|
||||
{
|
||||
player->setMedia(highlightSoundUrl);
|
||||
|
||||
currentPlayerUrl = highlightSoundUrl;
|
||||
}
|
||||
|
||||
player->play();
|
||||
}
|
||||
|
||||
player->play();
|
||||
}
|
||||
|
||||
if (this->highlightAlert_)
|
||||
@@ -274,7 +289,7 @@ MessagePtr TwitchMessageBuilder::build()
|
||||
if (iterator != this->tags.end())
|
||||
{
|
||||
this->hasBits_ = true;
|
||||
// bits = iterator.value().toString();
|
||||
this->bits = iterator.value().toString();
|
||||
}
|
||||
|
||||
// twitch emotes
|
||||
@@ -340,7 +355,7 @@ void TwitchMessageBuilder::addWords(
|
||||
auto i = int();
|
||||
auto currentTwitchEmote = twitchEmotes.begin();
|
||||
|
||||
for (const auto &word : words)
|
||||
for (auto word : words)
|
||||
{
|
||||
// check if it's a twitch emote twitch emote
|
||||
while (currentTwitchEmote != twitchEmotes.end() &&
|
||||
@@ -361,9 +376,19 @@ void TwitchMessageBuilder::addWords(
|
||||
MessageElementFlag::TwitchEmote);
|
||||
|
||||
i += word.length() + 1;
|
||||
|
||||
int len = std::get<2>(*currentTwitchEmote).string.length();
|
||||
currentTwitchEmote++;
|
||||
|
||||
continue;
|
||||
if (len < word.length())
|
||||
{
|
||||
word = word.mid(len);
|
||||
this->message().elements.back()->setTrailingSpace(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// split words
|
||||
@@ -952,7 +977,7 @@ void TwitchMessageBuilder::parseHighlights()
|
||||
{
|
||||
HighlightPhrase selfHighlight(
|
||||
currentUsername, getSettings()->enableSelfHighlightTaskbar,
|
||||
getSettings()->enableSelfHighlightSound, false);
|
||||
getSettings()->enableSelfHighlightSound, false, false);
|
||||
activeHighlights.emplace_back(std::move(selfHighlight));
|
||||
}
|
||||
|
||||
@@ -1171,7 +1196,7 @@ void TwitchMessageBuilder::appendTwitchBadges()
|
||||
{
|
||||
if (auto customModBadge = this->twitchChannel->ffzCustomModBadge())
|
||||
{
|
||||
this->emplace<BadgeElement>(
|
||||
this->emplace<ModBadgeElement>(
|
||||
customModBadge.get(),
|
||||
MessageElementFlag::BadgeChannelAuthority)
|
||||
->setTooltip((*customModBadge)->tooltip.string);
|
||||
@@ -1285,56 +1310,29 @@ void TwitchMessageBuilder::appendChatterinoBadges()
|
||||
}
|
||||
}
|
||||
|
||||
Outcome TwitchMessageBuilder::tryParseCheermote(const QString & /*string*/)
|
||||
Outcome TwitchMessageBuilder::tryParseCheermote(const QString &string)
|
||||
{
|
||||
// auto app = getApp();
|
||||
//// Try to parse custom cheermotes
|
||||
// const auto &channelResources = app->resources->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) {
|
||||
// Log("Error parsing bit amount in tryParseCheermote");
|
||||
// return Failure;
|
||||
// }
|
||||
|
||||
// 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()) {
|
||||
// Log("Error getting a cheermote from a cheermote set for the
|
||||
// bit amount {}",
|
||||
// numBits);
|
||||
// return Failure;
|
||||
// }
|
||||
|
||||
// const auto &cheermote = *savedIt;
|
||||
|
||||
// this->emplace<EmoteElement>(cheermote.animatedEmote,
|
||||
// MessageElementFlag::BitsAnimated);
|
||||
// this->emplace<TextElement>(amount, MessageElementFlag::Text,
|
||||
// cheermote.color);
|
||||
|
||||
// return Success;
|
||||
// }
|
||||
//}
|
||||
|
||||
return Failure;
|
||||
auto cheerOpt = this->twitchChannel->cheerEmote(string);
|
||||
if (!cheerOpt)
|
||||
{
|
||||
return Failure;
|
||||
}
|
||||
auto &cheerEmote = *cheerOpt;
|
||||
if (cheerEmote.staticEmote)
|
||||
{
|
||||
this->emplace<EmoteElement>(cheerEmote.staticEmote,
|
||||
MessageElementFlag::BitsStatic);
|
||||
}
|
||||
if (cheerEmote.animatedEmote)
|
||||
{
|
||||
this->emplace<EmoteElement>(cheerEmote.animatedEmote,
|
||||
MessageElementFlag::BitsAnimated);
|
||||
}
|
||||
if (cheerEmote.color != QColor())
|
||||
{
|
||||
this->emplace<TextElement>(this->bits, MessageElementFlag::BitsAmount,
|
||||
cheerEmote.color);
|
||||
}
|
||||
return Success;
|
||||
}
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -78,6 +78,7 @@ private:
|
||||
|
||||
QString roomID_;
|
||||
bool hasBits_ = false;
|
||||
QString bits;
|
||||
bool historicalMessage_ = false;
|
||||
|
||||
QString userId_;
|
||||
|
||||
Reference in New Issue
Block a user