diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d7e97d..cf5ef01 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Network) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS WebSockets) set(PROJECT_SOURCES main.cpp @@ -37,6 +38,11 @@ if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) settings.hpp settings.cpp invalididexception.hpp invalididexception.cpp tierplaceholder.hpp tierplaceholder.cpp + chatreader.hpp chatreader.cpp + chatmessage.hpp chatmessage.cpp + messagetype.hpp messagetype.cpp + ircmessagetype.hpp ircmessagetype.cpp + ircexception.hpp ircexception.cpp ) # Define target properties for Android with Qt 6 as: @@ -59,6 +65,7 @@ endif() target_link_libraries(qt-app PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) target_link_libraries(qt-app PRIVATE Qt${QT_VERISON_MAJOR}::Network) +target_link_libraries(qt-app PRIVATE Qt${QT_VERISON_MAJOR}::WebSockets) # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. # If you are developing for iOS or macOS you should consider setting an diff --git a/chatmessage.cpp b/chatmessage.cpp new file mode 100644 index 0000000..e212fe7 --- /dev/null +++ b/chatmessage.cpp @@ -0,0 +1,253 @@ +#include "chatmessage.hpp" + +#include "ircexception.hpp" + +#include + +std::unordered_map ChatMessage::parseTags(QString tagStr) +{ + enum + { + FindingKey, + FindingValue, + ValueEscaped, + } state; + QString s = tagStr; + if (s.startsWith('@')) + s = s.mid(1); + std::unordered_map tags; + QString key = ""; + QString value = ""; + state = FindingKey; + for(const auto c : std::as_const(s)) + { + switch (state) + { + case FindingKey: + if (c == '=') + state = FindingValue; + else if (c == ';') + { + state = FindingKey; + tags[key] = ""; + key = ""; + } + else if (c == ' ') + { + tags[key] = ""; + goto EndParse; + } + else + key += c; + break; + case FindingValue: + if (c == '\\') + { + state = ValueEscaped; + } + else if (c == ';') + { + tags[key] = value; + key = value = ""; + state = FindingKey; + } + else if (c == ' ') + { + tags[key] = value; + goto EndParse; + } + else if (c == '\r' || c == '\n' || c == '\0') + throw IrcParseException("Invalid character in tag string", s); + else + { + value += c; + } + break; + case ValueEscaped: + if (c == ':') + { + value += ';'; + state = FindingValue; + } + else if (c == 's') + { + value += ' '; + state = FindingValue; + } + else if (c == '\\') + { + value += '\\'; + state = FindingValue; + } + else if (c == 'r') + { + value += '\r'; + state = FindingValue; + } + else if (c == 'n') + { + value += '\n'; + state = FindingValue; + } + else if (c == ';') + { + tags[key] = value; + key = value = ""; + state = FindingKey; + } + //spaces should already be stripped, but handle this as end of tags just in case + else if (c == ' ') + { + tags[key] = value; + key = value = ""; + goto EndParse; + } + else if (c == '\r' || c == '\n' || c == '\0') + throw IrcParseException("Invalid character in tag string", s); + else + { + value += c; + state = FindingValue; + } + break; + default: + throw new QException(); + } + } + //this is reached after processing the last character without hitting a space + tags[key] = value; +EndParse: + return tags; +} + +ChatMessage* ChatMessage::Parse(QString message) +{ + QString rawMessage = message; + std::unordered_map tags; + QString sender = ""; + QList parameters; + QString prefix = ""; + + QString s = message; + if (s.startsWith('@')) + { + s = s.mid(1); + //first ' ' acts as the delimeter + auto split = s.split(' '); + auto tagString = split.takeFirst(); + s = split.join(' '); + tags = parseTags(tagString); + } + //message has source + if (s.startsWith(':')) + { + s = s.mid(1); + auto split = s.split(' '); + prefix = split.takeFirst(); + s = split.join(' '); + while (s[0] == ' ') + s = s.mid(1); + } + auto spl_command = s.split(' '); + IrcMessageType type = IrcMessageType::parse(spl_command.takeFirst()); + //message has parameters + if (!spl_command.isEmpty()) + { + s = spl_command.join(' '); + //message has single parameter marked as the final parameter + //this needs to be handled specially because the leading ' ' + //is stripped + if (s.startsWith(':')) + { + parameters.append(s.mid(1)); + } + else + { + auto spl_final = s.split(" :"); + auto spl_initial = spl_final.takeFirst().split(' ', Qt::SkipEmptyParts); + for (auto& part : spl_initial) + { + part = part.trimmed(); + } + parameters.append(spl_initial); + if (!spl_final.isEmpty()) + parameters.append(spl_final.join(" :")); + } + } + return new ChatMessage(rawMessage, tags, sender, parameters, prefix, type); +} + + + +ChatMessage::ChatMessage(QString rawMessage, std::unordered_map tags, + QString sender, QList parameters, QString prefix, + IrcMessageType type) + : _rawMessage(rawMessage), _tags(tags), _sender(sender), _parameters(parameters), + _prefix(prefix), _type(type) +{ + +} + +QString ChatMessage::rawMessage() const +{ + return _rawMessage; +} + +const std::unordered_map* ChatMessage::tags() const +{ + return &_tags; +} + +QString ChatMessage::sender() const +{ + return _sender; +} + +const QList* ChatMessage::parameters() const +{ + return &_parameters; +} + +QString ChatMessage::prefix() const +{ + return _prefix; +} + +IrcMessageType ChatMessage::type() const +{ + return _type; +} + + + +PrivmsgView::PrivmsgView(const ChatMessage* msg) + : parent(msg) +{ + if (parent->type() != Expected) + throw IrcIncorrectTypeException(Expected, parent->type()); +} + +QString PrivmsgView::message() const +{ + return parent->parameters()->last(); +} + +QString PrivmsgView::user() const +{ + static constexpr const char* KEY = "display-name"; + if (parent->tags()->contains(KEY)) + return parent->tags()->at(KEY); + throw IrcTagNotFoundException(KEY); +} + +PingView::PingView(const ChatMessage* msg) + : parent(msg) +{ + if (parent->type() != Expected) + throw IrcIncorrectTypeException(Expected, parent->type()); +} + +QString PingView::toPong() const +{ + return parent->rawMessage().replace("PING", "PONG"); +} diff --git a/chatmessage.hpp b/chatmessage.hpp new file mode 100644 index 0000000..b95a579 --- /dev/null +++ b/chatmessage.hpp @@ -0,0 +1,67 @@ +#ifndef CHATMESSAGE_HPP +#define CHATMESSAGE_HPP + +// #include "messagetype.hpp" +#include "ircmessagetype.hpp" + +#include +#include + +#include + +class ChatMessage +{ + Q_GADGET +public: + static ChatMessage* Parse(QString message); + QString rawMessage() const; + const std::unordered_map* tags() const; + QString sender() const; + const QList* parameters() const; + QString prefix() const; + IrcMessageType type() const; + // explicit ChatMessage(QObject *parent = nullptr); + +protected: + explicit ChatMessage(QString message); + +signals: + +private: + ChatMessage(QString rawMessage, std::unordered_map tags, + QString sender, QList parameters, QString prefix, + IrcMessageType type); + const QString _rawMessage; + const std::unordered_map _tags; + const QString _sender; + const QList _parameters; + const QString _prefix; + const IrcMessageType _type; + static std::unordered_map parseTags(QString tagStr); + +}; + +class PrivmsgView +{ +public: + PrivmsgView(const ChatMessage* msg); + QString message() const; + QString user() const; + static constexpr IrcMessageType Expected = IrcMessageType::PRIVMSG; + +private: + const ChatMessage* const parent; +}; + +class PingView +{ +public: + PingView(const ChatMessage* msg); + QString toPong() const; + static constexpr IrcMessageType Expected = IrcMessageType::PING; + +private: + const ChatMessage* const parent; +}; + +#endif // CHATMESSAGE_HPP diff --git a/chatreader.cpp b/chatreader.cpp new file mode 100644 index 0000000..e04a795 --- /dev/null +++ b/chatreader.cpp @@ -0,0 +1,53 @@ +#include "chatreader.hpp" + +ChatReader::ChatReader() + : QObject(nullptr), socket() +{ + QObject::connect(&socket, SIGNAL(socketConnect), + this, SLOT(socketConnect)); + QObject::connect(&socket, SIGNAL(socketDisconnect), + this, SLOT(socketDisconnect)); +} + +bool ChatReader::getConnected() const +{ + return _connected; +} + +void ChatReader::socketConnect() +{ + _connected = true; +} + +void ChatReader::socketDisconnect() +{ + _connected = false; +} + +void ChatReader::open() +{ + socket.open(URL); +} + +qint64 ChatReader::send(QString message) +{ + return socket.sendTextMessage(message); +} + +void ChatReader::messageReceived(QString message) +{ + auto chat = ChatMessage::Parse(message); + if (chat->type() == PingView::Expected) + { + const PingView ping(chat); + send(ping.toPong()); + } + emit chatMessageReceived(chat); +} + +ChatReader::~ChatReader() +{ + +} + +const QUrl ChatReader::URL = QUrl("wss://irc-ws.chat.twitch.tv:443"); diff --git a/chatreader.hpp b/chatreader.hpp new file mode 100644 index 0000000..da2893a --- /dev/null +++ b/chatreader.hpp @@ -0,0 +1,30 @@ +#ifndef CHATREADER_HPP +#define CHATREADER_HPP + +#include "chatmessage.hpp" + +#include + +class ChatReader : public QObject +{ + Q_OBJECT +public: + static const QUrl URL; + + ChatReader(); + ~ChatReader(); + bool getConnected() const; + void open(); + qint64 send(QString message); +signals: + void chatMessageReceived(const ChatMessage* message); +protected slots: + void socketConnect(); + void socketDisconnect(); + void messageReceived(QString message); +private: + QWebSocket socket; + bool _connected = false; +}; + +#endif // CHATREADER_HPP diff --git a/invalididexception.cpp b/invalididexception.cpp index 0f06d8c..bfc09fa 100644 --- a/invalididexception.cpp +++ b/invalididexception.cpp @@ -6,7 +6,7 @@ InvalidIdException::InvalidIdException() } InvalidRowIdException::InvalidRowIdException(TierRow::IdType id) - : _id(id), _what(std::format("id: {}", _id)) + : _id(id), _what(QString("id: {}").arg(QString::number(id)).toLocal8Bit()) { } @@ -18,11 +18,11 @@ TierRow::IdType InvalidRowIdException::id() const const char* InvalidRowIdException::what() const noexcept { - return _what.c_str(); + return _what.data(); } InvalidCardIdException::InvalidCardIdException(TierCard::IdType id) - : _id(id), _what(std::format("id: {}", _id)) + : _id(id), _what(QString("id: {}").arg(QString::number(id)).toLocal8Bit()) { } @@ -34,5 +34,5 @@ TierCard::IdType InvalidCardIdException::id() const const char* InvalidCardIdException::what() const noexcept { - return _what.c_str(); + return _what.data(); } diff --git a/invalididexception.hpp b/invalididexception.hpp index 58edd20..5a8d58b 100644 --- a/invalididexception.hpp +++ b/invalididexception.hpp @@ -21,7 +21,7 @@ public: private: const TierRow::IdType _id; - const std::string _what; + const QByteArray _what; }; class InvalidCardIdException : public InvalidIdException @@ -33,7 +33,7 @@ public: private: const TierCard::IdType _id; - const std::string _what; + const QByteArray _what; }; #endif // INVALIDIDEXCEPTION_HPP diff --git a/ircexception.cpp b/ircexception.cpp new file mode 100644 index 0000000..3ee0ad2 --- /dev/null +++ b/ircexception.cpp @@ -0,0 +1,40 @@ +#include "ircexception.hpp" + +IrcException::IrcException(QString _what) + : _what(_what.toLocal8Bit()) +{ + +} + +const char* IrcException::what() const noexcept +{ + return _what.data(); +} + +IrcTagNotFoundException::IrcTagNotFoundException(QString tag) + : IrcException(QString("missing tag: %1").arg(tag)), tag(tag) +{ + +} + +IrcParseException::IrcParseException(QString message, QString description) + : IrcException(_makeWhat(message, description)), + message(message), description(description) +{ + +} + +QString IrcParseException::_makeWhat(QString message, QString description) +{ + QString s = ""; + if (!message.isEmpty()) + s = QString("Description: %1\n").arg(description); + return s + QString("Message: %1").arg(message); +} + +IrcIncorrectTypeException::IrcIncorrectTypeException(IrcMessageType expected, IrcMessageType actual) + : IrcException(QString("Expected: %1 Actual: %2").arg(expected.toString(), actual.toString())), + expected(expected), actual(actual) +{ + +} diff --git a/ircexception.hpp b/ircexception.hpp new file mode 100644 index 0000000..7e50848 --- /dev/null +++ b/ircexception.hpp @@ -0,0 +1,43 @@ +#ifndef IRCEXCEPTIONS_HPP +#define IRCEXCEPTIONS_HPP + +#include "ircmessagetype.hpp" + +#include + +class IrcException : public QException +{ +public: + const char* what() const noexcept override; +protected: + IrcException(QString _what); + const QByteArray _what; +}; + +class IrcParseException : public IrcException +{ +public: + IrcParseException(QString message, QString description); + const QString message; + const QString description; + +private: + static QString _makeWhat(QString message, QString description=""); +}; + +class IrcTagNotFoundException : public IrcException +{ +public: + IrcTagNotFoundException(QString tag); + const QString tag; +}; + +class IrcIncorrectTypeException : public IrcException +{ +public: + IrcIncorrectTypeException(IrcMessageType expected, IrcMessageType actual); + const IrcMessageType expected; + const IrcMessageType actual; +}; + +#endif // IRCEXCEPTIONS_HPP diff --git a/ircmessagetype.cpp b/ircmessagetype.cpp new file mode 100644 index 0000000..ff0b2e6 --- /dev/null +++ b/ircmessagetype.cpp @@ -0,0 +1,45 @@ +#include "ircmessagetype.hpp" + +#include +#include + +IrcMessageType::MessageType IrcMessageType::getType() const +{ + return _type; +} + + +IrcMessageType IrcMessageType::parse(QString str) +{ + // QVariant var = QVariant::fromValue(str); + // if (var.convert(QMetaType::fromType())) + // return var.value(); + // return IrcMessageType::MessageType::UNKNOWN; + auto&& meta = QMetaEnum::fromType(); + bool ok = false; + auto value = static_cast(meta.keyToValue(str.toLocal8Bit().data(), &ok)); + if (ok) + return value; + auto num = str.toInt(&ok); + if (ok) + return num; + return Unknown(); +} + +IrcMessageType::IrcMessageType(const IrcMessageType& other) + : _type(other._type) +{ + +} + +IrcMessageType::IrcMessageType(int type) + : _type(static_cast(type)) +{ + +} + +QString IrcMessageType::toString() const +{ + auto&& metaEnum = QMetaEnum::fromType(); + return metaEnum.valueToKey(static_cast(_type)); +} diff --git a/ircmessagetype.hpp b/ircmessagetype.hpp new file mode 100644 index 0000000..f5f9eae --- /dev/null +++ b/ircmessagetype.hpp @@ -0,0 +1,34 @@ +#ifndef IRCMESSAGETYPE_HPP +#define IRCMESSAGETYPE_HPP + +#include +#include + +class IrcMessageType +{ + Q_GADGET +public: + enum MessageType + { + UNKNOWN, + PRIVMSG, + PING + }; + Q_ENUM(MessageType); + Q_PROPERTY(MessageType type READ getType) + static IrcMessageType Unknown() { return UNKNOWN; } + constexpr IrcMessageType(MessageType type) : _type(type) { } + IrcMessageType(int type); + IrcMessageType(const IrcMessageType& other); + bool operator ==(const IrcMessageType& other) + { return this->_type == other._type; } + static IrcMessageType parse(QString str); + MessageType getType() const; + QString toString() const; + +private: + const MessageType _type; +}; +Q_DECLARE_METATYPE(IrcMessageType) + +#endif // IRCMESSAGETYPE_HPP diff --git a/messagetype.cpp b/messagetype.cpp new file mode 100644 index 0000000..f17f730 --- /dev/null +++ b/messagetype.cpp @@ -0,0 +1,17 @@ +#include "messagetype.hpp" + +MessageType::MessageType(QObject *parent) + : QObject{parent} +{ + +} + +void MessageType::setType(MsgType type) +{ + +} + +MessageType::MsgType MessageType::type() const +{ + return UNKNOWN; +} diff --git a/messagetype.hpp b/messagetype.hpp new file mode 100644 index 0000000..b9a6397 --- /dev/null +++ b/messagetype.hpp @@ -0,0 +1,25 @@ +#ifndef MESSAGETYPE_HPP +#define MESSAGETYPE_HPP + +#include + +class MessageType : public QObject +{ + Q_GADGET +public: + explicit MessageType(QObject *parent = nullptr); + MessageType(MessageType& other); + enum MsgType + { + UNKNOWN, + PRIVMSG, + }; + Q_ENUM(MsgType) + MessageType(MsgType type); + void setType(MsgType type); + MsgType type() const; + +signals: +}; + +#endif // MESSAGETYPE_HPP