2018-06-26 14:09:39 +02:00
|
|
|
#include "MessageBuilder.hpp"
|
2018-06-28 00:24:21 +02:00
|
|
|
|
2019-01-20 14:45:59 +01:00
|
|
|
#include "Application.hpp"
|
2018-06-28 00:24:21 +02:00
|
|
|
#include "common/LinkParser.hpp"
|
2019-01-20 14:45:59 +01:00
|
|
|
#include "messages/Image.hpp"
|
2018-08-11 22:23:06 +02:00
|
|
|
#include "messages/Message.hpp"
|
|
|
|
#include "messages/MessageElement.hpp"
|
2019-03-13 15:26:55 +01:00
|
|
|
#include "providers/LinkResolver.hpp"
|
2018-08-11 22:23:06 +02:00
|
|
|
#include "providers/twitch/PubsubActions.hpp"
|
2018-06-28 19:46:45 +02:00
|
|
|
#include "singletons/Emotes.hpp"
|
|
|
|
#include "singletons/Resources.hpp"
|
2018-06-28 20:03:04 +02:00
|
|
|
#include "singletons/Theme.hpp"
|
2018-08-07 01:35:24 +02:00
|
|
|
#include "util/FormatTime.hpp"
|
|
|
|
#include "util/IrcHelpers.hpp"
|
2017-04-12 17:46:44 +02:00
|
|
|
|
2017-12-27 01:22:12 +01:00
|
|
|
#include <QDateTime>
|
2019-01-20 14:45:59 +01:00
|
|
|
#include <QImageReader>
|
2017-12-27 01:22:12 +01:00
|
|
|
|
2017-04-14 17:52:22 +02:00
|
|
|
namespace chatterino {
|
2017-04-12 17:46:44 +02:00
|
|
|
|
2018-08-07 01:35:24 +02:00
|
|
|
MessagePtr makeSystemMessage(const QString &text)
|
|
|
|
{
|
|
|
|
return MessageBuilder(systemMessage, text).release();
|
|
|
|
}
|
|
|
|
|
2019-01-20 01:02:04 +01:00
|
|
|
std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
|
|
|
|
const AutomodAction &action)
|
|
|
|
{
|
|
|
|
auto builder = MessageBuilder();
|
|
|
|
|
|
|
|
builder.emplace<TimestampElement>();
|
|
|
|
builder.message().flags.set(MessageFlag::PubSub);
|
|
|
|
|
2019-01-20 14:45:59 +01:00
|
|
|
builder
|
|
|
|
.emplace<ImageElement>(
|
|
|
|
Image::fromPixmap(getApp()->resources->twitch.automod),
|
|
|
|
MessageElementFlag::BadgeChannelAuthority)
|
|
|
|
->setTooltip("AutoMod");
|
2019-01-25 14:19:09 +01:00
|
|
|
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::BoldUsername,
|
|
|
|
MessageColor(QColor("blue")),
|
|
|
|
FontStyle::ChatMediumBold);
|
2019-01-20 01:02:04 +01:00
|
|
|
builder.emplace<TextElement>(
|
|
|
|
"AutoMod:", MessageElementFlag::NonBoldUsername,
|
2019-01-20 14:45:59 +01:00
|
|
|
MessageColor(QColor("blue")));
|
2019-01-20 01:02:04 +01:00
|
|
|
builder.emplace<TextElement>(
|
|
|
|
("Held a message for reason: " + action.reason +
|
2019-01-21 18:33:57 +01:00
|
|
|
". Allow will post it in chat. "),
|
2019-01-20 01:02:04 +01:00
|
|
|
MessageElementFlag::Text, MessageColor::Text);
|
2019-01-20 14:45:59 +01:00
|
|
|
builder
|
2019-01-21 18:33:57 +01:00
|
|
|
.emplace<TextElement>("Allow", MessageElementFlag::Text,
|
2019-01-20 14:45:59 +01:00
|
|
|
MessageColor(QColor("green")),
|
|
|
|
FontStyle::ChatMediumBold)
|
|
|
|
->setLink({Link::AutoModAllow, action.msgID});
|
|
|
|
builder
|
|
|
|
.emplace<TextElement>(" Deny", MessageElementFlag::Text,
|
|
|
|
MessageColor(QColor("red")),
|
|
|
|
FontStyle::ChatMediumBold)
|
|
|
|
->setLink({Link::AutoModDeny, action.msgID});
|
2019-01-21 18:33:57 +01:00
|
|
|
// builder.emplace<TextElement>(action.msgID,
|
|
|
|
// MessageElementFlag::Text,
|
|
|
|
// MessageColor::Text);
|
2019-01-20 01:02:04 +01:00
|
|
|
builder.message().flags.set(MessageFlag::AutoMod);
|
|
|
|
|
|
|
|
auto message1 = builder.release();
|
|
|
|
|
|
|
|
builder = MessageBuilder();
|
|
|
|
builder.emplace<TimestampElement>();
|
|
|
|
builder.message().flags.set(MessageFlag::PubSub);
|
|
|
|
|
2019-01-25 14:19:09 +01:00
|
|
|
builder
|
|
|
|
.emplace<TextElement>(
|
|
|
|
action.target.name + ":", MessageElementFlag::BoldUsername,
|
|
|
|
MessageColor(QColor("red")), FontStyle::ChatMediumBold)
|
|
|
|
->setLink({Link::UserInfo, action.target.name});
|
2019-01-20 01:02:04 +01:00
|
|
|
builder
|
|
|
|
.emplace<TextElement>(action.target.name + ":",
|
|
|
|
MessageElementFlag::NonBoldUsername,
|
|
|
|
MessageColor(QColor("red")))
|
|
|
|
->setLink({Link::UserInfo, action.target.name});
|
|
|
|
builder.emplace<TextElement>(action.message, MessageElementFlag::Text,
|
|
|
|
MessageColor::Text);
|
|
|
|
builder.message().flags.set(MessageFlag::AutoMod);
|
|
|
|
|
|
|
|
auto message2 = builder.release();
|
|
|
|
|
|
|
|
return std::make_pair(message1, message2);
|
|
|
|
}
|
|
|
|
|
2017-04-12 17:46:44 +02:00
|
|
|
MessageBuilder::MessageBuilder()
|
2018-08-11 22:23:06 +02:00
|
|
|
: message_(std::make_shared<Message>())
|
2018-08-07 01:35:24 +02:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
MessageBuilder::MessageBuilder(SystemMessageTag, const QString &text)
|
|
|
|
: MessageBuilder()
|
2017-04-12 17:46:44 +02:00
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
this->emplace<TimestampElement>();
|
2019-05-01 12:07:35 +02:00
|
|
|
|
|
|
|
// check system message for links
|
|
|
|
// (e.g. needed for sub ticket message in sub only mode)
|
|
|
|
QStringList textFragments = text.split(QRegularExpression("\\s"));
|
|
|
|
for (const auto &word : textFragments)
|
|
|
|
{
|
|
|
|
auto linkString = this->matchLink(word);
|
|
|
|
if (linkString.isEmpty())
|
|
|
|
{
|
|
|
|
this->emplace<TextElement>(word, MessageElementFlag::Text,
|
|
|
|
MessageColor::System);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
this->addLink(word, linkString);
|
|
|
|
}
|
|
|
|
}
|
2018-08-07 07:55:31 +02:00
|
|
|
this->message().flags.set(MessageFlag::System);
|
|
|
|
this->message().flags.set(MessageFlag::DoNotTriggerNotification);
|
2019-05-01 16:43:52 +02:00
|
|
|
this->message().messageText = text;
|
2018-08-07 01:35:24 +02:00
|
|
|
this->message().searchText = text;
|
2017-04-12 17:46:44 +02:00
|
|
|
}
|
|
|
|
|
2018-08-07 01:35:24 +02:00
|
|
|
MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
|
|
|
|
const QString &durationInSeconds,
|
|
|
|
const QString &reason, bool multipleTimes)
|
|
|
|
: MessageBuilder()
|
2017-04-12 17:46:44 +02:00
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
QString text;
|
|
|
|
|
|
|
|
text.append(username);
|
2018-10-21 13:43:02 +02:00
|
|
|
if (!durationInSeconds.isEmpty())
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text.append(" has been timed out");
|
|
|
|
|
|
|
|
// TODO: Implement who timed the user out
|
|
|
|
|
|
|
|
text.append(" for ");
|
|
|
|
bool ok = true;
|
|
|
|
int timeoutSeconds = durationInSeconds.toInt(&ok);
|
2018-10-21 13:43:02 +02:00
|
|
|
if (ok)
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text.append(formatTime(timeoutSeconds));
|
|
|
|
}
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text.append(" has been permanently banned");
|
|
|
|
}
|
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (reason.length() > 0)
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text.append(": \"");
|
|
|
|
text.append(parseTagString(reason));
|
|
|
|
text.append("\"");
|
|
|
|
}
|
|
|
|
text.append(".");
|
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (multipleTimes)
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text.append(" (multiple times)");
|
|
|
|
}
|
|
|
|
|
2018-08-07 07:55:31 +02:00
|
|
|
this->message().flags.set(MessageFlag::System);
|
|
|
|
this->message().flags.set(MessageFlag::Timeout);
|
|
|
|
this->message().flags.set(MessageFlag::DoNotTriggerNotification);
|
2018-08-07 01:35:24 +02:00
|
|
|
this->message().timeoutUser = username;
|
2018-09-30 12:50:47 +02:00
|
|
|
this->emplace<TimestampElement>();
|
|
|
|
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
|
|
|
MessageColor::System);
|
2019-05-01 16:43:52 +02:00
|
|
|
this->message().messageText = text;
|
2018-09-30 12:50:47 +02:00
|
|
|
this->message().searchText = text;
|
2017-04-12 17:46:44 +02:00
|
|
|
}
|
|
|
|
|
2018-08-07 01:35:24 +02:00
|
|
|
MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count)
|
2018-08-08 15:35:54 +02:00
|
|
|
: MessageBuilder()
|
2017-04-12 17:46:44 +02:00
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
this->emplace<TimestampElement>();
|
2018-08-07 07:55:31 +02:00
|
|
|
this->message().flags.set(MessageFlag::System);
|
|
|
|
this->message().flags.set(MessageFlag::Timeout);
|
2018-08-07 01:35:24 +02:00
|
|
|
this->message().timeoutUser = action.target.name;
|
|
|
|
this->message().count = count;
|
|
|
|
|
|
|
|
QString text;
|
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (action.isBan())
|
|
|
|
{
|
|
|
|
if (action.reason.isEmpty())
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text = QString("%1 banned %2.") //
|
|
|
|
.arg(action.source.name)
|
|
|
|
.arg(action.target.name);
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text = QString("%1 banned %2: \"%3\".") //
|
|
|
|
.arg(action.source.name)
|
|
|
|
.arg(action.target.name)
|
|
|
|
.arg(action.reason);
|
|
|
|
}
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (action.reason.isEmpty())
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text = QString("%1 timed out %2 for %3.") //
|
|
|
|
.arg(action.source.name)
|
|
|
|
.arg(action.target.name)
|
|
|
|
.arg(formatTime(action.duration));
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text = QString("%1 timed out %2 for %3: \"%4\".") //
|
|
|
|
.arg(action.source.name)
|
|
|
|
.arg(action.target.name)
|
|
|
|
.arg(formatTime(action.duration))
|
|
|
|
.arg(action.reason);
|
|
|
|
}
|
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (count > 1)
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text.append(QString(" (%1 times)").arg(count));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-07 07:55:31 +02:00
|
|
|
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
2018-08-07 01:35:24 +02:00
|
|
|
MessageColor::System);
|
2019-05-01 16:43:52 +02:00
|
|
|
this->message().messageText = text;
|
2018-08-07 01:35:24 +02:00
|
|
|
this->message().searchText = text;
|
2017-04-12 17:46:44 +02:00
|
|
|
}
|
|
|
|
|
2018-08-07 01:35:24 +02:00
|
|
|
MessageBuilder::MessageBuilder(const UnbanAction &action)
|
2018-08-08 15:35:54 +02:00
|
|
|
: MessageBuilder()
|
2017-07-31 00:57:42 +02:00
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
this->emplace<TimestampElement>();
|
2018-08-07 07:55:31 +02:00
|
|
|
this->message().flags.set(MessageFlag::System);
|
|
|
|
this->message().flags.set(MessageFlag::Untimeout);
|
2018-08-07 01:35:24 +02:00
|
|
|
|
|
|
|
this->message().timeoutUser = action.target.name;
|
|
|
|
|
|
|
|
QString text;
|
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (action.wasBan())
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text = QString("%1 unbanned %2.") //
|
|
|
|
.arg(action.source.name)
|
|
|
|
.arg(action.target.name);
|
2018-10-21 13:43:02 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
text = QString("%1 untimedout %2.") //
|
|
|
|
.arg(action.source.name)
|
|
|
|
.arg(action.target.name);
|
2018-01-11 20:16:25 +01:00
|
|
|
}
|
2018-08-07 01:35:24 +02:00
|
|
|
|
2018-08-07 07:55:31 +02:00
|
|
|
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
2018-08-07 01:35:24 +02:00
|
|
|
MessageColor::System);
|
2019-05-01 16:43:52 +02:00
|
|
|
this->message().messageText = text;
|
2018-08-07 01:35:24 +02:00
|
|
|
this->message().searchText = text;
|
|
|
|
}
|
|
|
|
|
2019-01-21 18:33:57 +01:00
|
|
|
MessageBuilder::MessageBuilder(const AutomodUserAction &action)
|
|
|
|
: MessageBuilder()
|
|
|
|
{
|
|
|
|
this->emplace<TimestampElement>();
|
|
|
|
this->message().flags.set(MessageFlag::System);
|
|
|
|
|
|
|
|
QString text;
|
2019-01-22 23:20:43 +01:00
|
|
|
switch (action.type)
|
2019-01-21 18:33:57 +01:00
|
|
|
{
|
2019-01-23 18:07:36 +01:00
|
|
|
case AutomodUserAction::AddPermitted:
|
2019-01-22 23:20:43 +01:00
|
|
|
{
|
|
|
|
text = QString("%1 added %2 as a permitted term on AutoMod.")
|
|
|
|
.arg(action.source.name)
|
|
|
|
.arg(action.message);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2019-01-23 18:07:36 +01:00
|
|
|
case AutomodUserAction::AddBlocked:
|
2019-01-22 23:20:43 +01:00
|
|
|
{
|
|
|
|
text = QString("%1 added %2 as a blocked term on AutoMod.")
|
|
|
|
.arg(action.source.name)
|
|
|
|
.arg(action.message);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2019-01-23 18:07:36 +01:00
|
|
|
case AutomodUserAction::RemovePermitted:
|
2019-01-22 23:20:43 +01:00
|
|
|
{
|
|
|
|
text = QString("%1 removed %2 as a permitted term term on AutoMod.")
|
|
|
|
.arg(action.source.name)
|
|
|
|
.arg(action.message);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2019-01-23 18:07:36 +01:00
|
|
|
case AutomodUserAction::RemoveBlocked:
|
2019-01-22 23:20:43 +01:00
|
|
|
{
|
|
|
|
text = QString("%1 removed %2 as a blocked term on AutoMod.")
|
|
|
|
.arg(action.source.name)
|
|
|
|
.arg(action.message);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2019-01-23 18:07:36 +01:00
|
|
|
case AutomodUserAction::Properties:
|
2019-01-22 23:20:43 +01:00
|
|
|
{
|
|
|
|
text = QString("%1 modified the AutoMod properties.")
|
|
|
|
.arg(action.source.name);
|
|
|
|
}
|
|
|
|
break;
|
2019-01-21 18:33:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
this->emplace<TextElement>(text, MessageElementFlag::Text,
|
|
|
|
MessageColor::System);
|
|
|
|
}
|
|
|
|
|
2018-08-07 01:35:24 +02:00
|
|
|
Message *MessageBuilder::operator->()
|
|
|
|
{
|
|
|
|
return this->message_.get();
|
|
|
|
}
|
|
|
|
|
|
|
|
Message &MessageBuilder::message()
|
|
|
|
{
|
|
|
|
return *this->message_;
|
|
|
|
}
|
|
|
|
|
|
|
|
MessagePtr MessageBuilder::release()
|
|
|
|
{
|
2018-08-11 22:23:06 +02:00
|
|
|
std::shared_ptr<Message> ptr;
|
|
|
|
this->message_.swap(ptr);
|
|
|
|
return ptr;
|
2017-07-31 00:37:22 +02:00
|
|
|
}
|
|
|
|
|
2018-10-24 11:36:36 +02:00
|
|
|
std::weak_ptr<Message> MessageBuilder::weakOf()
|
|
|
|
{
|
|
|
|
return this->message_;
|
|
|
|
}
|
|
|
|
|
2018-08-07 01:35:24 +02:00
|
|
|
void MessageBuilder::append(std::unique_ptr<MessageElement> element)
|
2017-04-12 17:46:44 +02:00
|
|
|
{
|
2018-08-07 01:35:24 +02:00
|
|
|
this->message().elements.push_back(std::move(element));
|
2017-04-12 17:46:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
QString MessageBuilder::matchLink(const QString &string)
|
|
|
|
{
|
2018-06-28 00:24:21 +02:00
|
|
|
LinkParser linkParser(string);
|
|
|
|
|
2018-08-06 21:17:03 +02:00
|
|
|
static QRegularExpression httpRegex(
|
|
|
|
"\\bhttps?://", QRegularExpression::CaseInsensitiveOption);
|
|
|
|
static QRegularExpression ftpRegex(
|
|
|
|
"\\bftps?://", QRegularExpression::CaseInsensitiveOption);
|
|
|
|
static QRegularExpression spotifyRegex(
|
|
|
|
"\\bspotify:", QRegularExpression::CaseInsensitiveOption);
|
2017-08-12 12:09:26 +02:00
|
|
|
|
2018-10-21 13:43:02 +02:00
|
|
|
if (!linkParser.hasMatch())
|
|
|
|
{
|
2017-08-12 12:09:26 +02:00
|
|
|
return QString();
|
|
|
|
}
|
2017-08-05 18:44:14 +02:00
|
|
|
|
2018-06-28 00:24:21 +02:00
|
|
|
QString captured = linkParser.getCaptured();
|
2017-07-31 14:23:23 +02:00
|
|
|
|
2018-08-06 21:17:03 +02:00
|
|
|
if (!captured.contains(httpRegex) && !captured.contains(ftpRegex) &&
|
2018-10-21 13:43:02 +02:00
|
|
|
!captured.contains(spotifyRegex))
|
|
|
|
{
|
2018-07-11 13:50:05 +02:00
|
|
|
captured.insert(0, "http://");
|
2017-07-26 12:01:23 +02:00
|
|
|
}
|
2017-09-24 18:43:24 +02:00
|
|
|
|
2017-07-31 14:23:23 +02:00
|
|
|
return captured;
|
2017-04-12 17:46:44 +02:00
|
|
|
}
|
2017-06-11 20:53:43 +02:00
|
|
|
|
2019-03-13 15:26:55 +01:00
|
|
|
void MessageBuilder::addLink(const QString &origLink,
|
|
|
|
const QString &matchedLink)
|
|
|
|
{
|
|
|
|
static QRegularExpression domainRegex(
|
|
|
|
R"(^(?:(?:ftp|http)s?:\/\/)?([^\/]+)(?:\/.*)?$)",
|
|
|
|
QRegularExpression::CaseInsensitiveOption);
|
|
|
|
|
|
|
|
QString lowercaseLinkString;
|
|
|
|
auto match = domainRegex.match(origLink);
|
|
|
|
if (match.isValid())
|
|
|
|
{
|
|
|
|
lowercaseLinkString = origLink.mid(0, match.capturedStart(1)) +
|
|
|
|
match.captured(1).toLower() +
|
|
|
|
origLink.mid(match.capturedEnd(1));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
lowercaseLinkString = origLink;
|
|
|
|
}
|
|
|
|
auto linkElement = Link(Link::Url, matchedLink);
|
|
|
|
|
|
|
|
auto textColor = MessageColor(MessageColor::Link);
|
|
|
|
auto linkMELowercase =
|
|
|
|
this->emplace<TextElement>(lowercaseLinkString,
|
|
|
|
MessageElementFlag::LowercaseLink, textColor)
|
|
|
|
->setLink(linkElement);
|
|
|
|
auto linkMEOriginal =
|
|
|
|
this->emplace<TextElement>(origLink, MessageElementFlag::OriginalLink,
|
|
|
|
textColor)
|
|
|
|
->setLink(linkElement);
|
|
|
|
|
|
|
|
LinkResolver::getLinkInfo(matchedLink, [weakMessage = this->weakOf(),
|
|
|
|
linkMELowercase, linkMEOriginal,
|
|
|
|
matchedLink](QString tooltipText,
|
|
|
|
Link originalLink) {
|
|
|
|
auto shared = weakMessage.lock();
|
|
|
|
if (!shared)
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!tooltipText.isEmpty())
|
|
|
|
{
|
|
|
|
linkMELowercase->setTooltip(tooltipText);
|
|
|
|
linkMEOriginal->setTooltip(tooltipText);
|
|
|
|
}
|
|
|
|
if (originalLink.value != matchedLink && !originalLink.value.isEmpty())
|
|
|
|
{
|
|
|
|
linkMELowercase->setLink(originalLink)->updateLink();
|
|
|
|
linkMEOriginal->setLink(originalLink)->updateLink();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-06-11 20:53:43 +02:00
|
|
|
} // namespace chatterino
|