starting work on websocket chat reader

This commit is contained in:
Ikatono
2024-05-27 13:53:31 -05:00
parent 230f382015
commit 5451a1151c
13 changed files with 620 additions and 6 deletions

View File

@@ -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

253
chatmessage.cpp Normal file
View File

@@ -0,0 +1,253 @@
#include "chatmessage.hpp"
#include "ircexception.hpp"
#include <QException>
std::unordered_map<QString, QString> ChatMessage::parseTags(QString tagStr)
{
enum
{
FindingKey,
FindingValue,
ValueEscaped,
} state;
QString s = tagStr;
if (s.startsWith('@'))
s = s.mid(1);
std::unordered_map<QString, QString> 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<QString, QString> tags;
QString sender = "";
QList<QString> 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<QString, QString> tags,
QString sender, QList<QString> 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<QString, QString>* ChatMessage::tags() const
{
return &_tags;
}
QString ChatMessage::sender() const
{
return _sender;
}
const QList<QString>* 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");
}

67
chatmessage.hpp Normal file
View File

@@ -0,0 +1,67 @@
#ifndef CHATMESSAGE_HPP
#define CHATMESSAGE_HPP
// #include "messagetype.hpp"
#include "ircmessagetype.hpp"
#include <QObject>
#include <QList>
#include <unordered_map>
class ChatMessage
{
Q_GADGET
public:
static ChatMessage* Parse(QString message);
QString rawMessage() const;
const std::unordered_map<QString, QString>* tags() const;
QString sender() const;
const QList<QString>* 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<QString, QString> tags,
QString sender, QList<QString> parameters, QString prefix,
IrcMessageType type);
const QString _rawMessage;
const std::unordered_map<QString, QString> _tags;
const QString _sender;
const QList<QString> _parameters;
const QString _prefix;
const IrcMessageType _type;
static std::unordered_map<QString, QString> 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

53
chatreader.cpp Normal file
View File

@@ -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");

30
chatreader.hpp Normal file
View File

@@ -0,0 +1,30 @@
#ifndef CHATREADER_HPP
#define CHATREADER_HPP
#include "chatmessage.hpp"
#include <QWebSocket>
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

View File

@@ -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();
}

View File

@@ -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

40
ircexception.cpp Normal file
View File

@@ -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)
{
}

43
ircexception.hpp Normal file
View File

@@ -0,0 +1,43 @@
#ifndef IRCEXCEPTIONS_HPP
#define IRCEXCEPTIONS_HPP
#include "ircmessagetype.hpp"
#include <QException>
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

45
ircmessagetype.cpp Normal file
View File

@@ -0,0 +1,45 @@
#include "ircmessagetype.hpp"
#include <QMetaObject>
#include <QMetaEnum>
IrcMessageType::MessageType IrcMessageType::getType() const
{
return _type;
}
IrcMessageType IrcMessageType::parse(QString str)
{
// QVariant var = QVariant::fromValue(str);
// if (var.convert(QMetaType::fromType<IrcMessageType>()))
// return var.value<IrcMessageType>();
// return IrcMessageType::MessageType::UNKNOWN;
auto&& meta = QMetaEnum::fromType<IrcMessageType::MessageType>();
bool ok = false;
auto value = static_cast<IrcMessageType>(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<MessageType>(type))
{
}
QString IrcMessageType::toString() const
{
auto&& metaEnum = QMetaEnum::fromType<MessageType>();
return metaEnum.valueToKey(static_cast<int>(_type));
}

34
ircmessagetype.hpp Normal file
View File

@@ -0,0 +1,34 @@
#ifndef IRCMESSAGETYPE_HPP
#define IRCMESSAGETYPE_HPP
#include <QMetaObject>
#include <QMetaType>
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

17
messagetype.cpp Normal file
View File

@@ -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;
}

25
messagetype.hpp Normal file
View File

@@ -0,0 +1,25 @@
#ifndef MESSAGETYPE_HPP
#define MESSAGETYPE_HPP
#include <QObject>
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