From 5451a1151c35c13298393eb8e115c3601348d8c4 Mon Sep 17 00:00:00 2001 From: Ikatono Date: Mon, 27 May 2024 13:53:31 -0500 Subject: [PATCH] starting work on websocket chat reader --- CMakeLists.txt | 7 ++ chatmessage.cpp | 253 +++++++++++++++++++++++++++++++++++++++++ chatmessage.hpp | 67 +++++++++++ chatreader.cpp | 53 +++++++++ chatreader.hpp | 30 +++++ invalididexception.cpp | 8 +- invalididexception.hpp | 4 +- ircexception.cpp | 40 +++++++ ircexception.hpp | 43 +++++++ ircmessagetype.cpp | 45 ++++++++ ircmessagetype.hpp | 34 ++++++ messagetype.cpp | 17 +++ messagetype.hpp | 25 ++++ 13 files changed, 620 insertions(+), 6 deletions(-) create mode 100644 chatmessage.cpp create mode 100644 chatmessage.hpp create mode 100644 chatreader.cpp create mode 100644 chatreader.hpp create mode 100644 ircexception.cpp create mode 100644 ircexception.hpp create mode 100644 ircmessagetype.cpp create mode 100644 ircmessagetype.hpp create mode 100644 messagetype.cpp create mode 100644 messagetype.hpp 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