Merge branch 'master' into dependabot/submodules/lib/lua/src-7923dbb

This commit is contained in:
Felanbird 2024-01-17 20:45:54 -05:00 committed by GitHub
commit f0c5a781d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
282 changed files with 6489 additions and 3628 deletions

View file

@ -45,11 +45,9 @@ CheckOptions:
- key: readability-identifier-naming.MemberCase
value: camelBack
- key: readability-identifier-naming.PrivateMemberIgnoredRegexp
value: .*
- key: readability-identifier-naming.PrivateMemberSuffix
value: _
- key: readability-identifier-naming.ProtectedMemberSuffix
value: _
value: ^.*_$
- key: readability-identifier-naming.ProtectedMemberIgnoredRegexp
value: ^.*_$
- key: readability-identifier-naming.UnionCase
value: CamelCase
- key: readability-identifier-naming.GlobalConstantCase
@ -65,5 +63,11 @@ CheckOptions:
- key: readability-identifier-naming.LocalPointerIgnoredRegexp
value: ^L$
# Benchmarks
- key: readability-identifier-naming.FunctionIgnoredRegexp
value: ^BM_[^_]+$
- key: readability-identifier-naming.ClassIgnoredRegexp
value: ^BM_[^_]+$
- key: misc-const-correctness.AnalyzeValues
value: false

View file

@ -151,7 +151,7 @@ jobs:
# WINDOWS
- name: Enable Developer Command Prompt (Windows)
if: startsWith(matrix.os, 'windows')
uses: ilammy/msvc-dev-cmd@v1.12.1
uses: ilammy/msvc-dev-cmd@v1.13.0
- name: Setup conan variables (Windows)
if: startsWith(matrix.os, 'windows')
@ -174,7 +174,7 @@ jobs:
- name: Cache conan packages (Windows)
if: startsWith(matrix.os, 'windows')
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: ${{ runner.os }}-conan-user-${{ hashFiles('**/conanfile.py') }}${{ env.C2_CONAN_CACHE_SUFFIX }}
path: ~/.conan2/

View file

@ -119,7 +119,7 @@ jobs:
- name: clang-tidy review
timeout-minutes: 20
uses: ZedThree/clang-tidy-review@v0.14.0
uses: ZedThree/clang-tidy-review@v0.16.0
with:
build_dir: build-clang-tidy
config_file: ".clang-tidy"
@ -145,4 +145,4 @@ jobs:
libbenchmark-dev
- name: clang-tidy-review upload
uses: ZedThree/clang-tidy-review/upload@v0.14.0
uses: ZedThree/clang-tidy-review/upload@v0.16.0

View file

@ -43,7 +43,7 @@ jobs:
run: echo "C:\Program Files (x86)\Inno Setup 6\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Enable Developer Command Prompt
uses: ilammy/msvc-dev-cmd@v1.12.1
uses: ilammy/msvc-dev-cmd@v1.13.0
- name: Build installer
id: build-installer

View file

@ -14,6 +14,6 @@ jobs:
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: ZedThree/clang-tidy-review/post@v0.14.0
- uses: ZedThree/clang-tidy-review/post@v0.16.0
with:
lgtm_comment_body: ""

View file

@ -63,7 +63,7 @@ jobs:
version: ${{ matrix.qt-version }}
- name: Enable Developer Command Prompt
uses: ilammy/msvc-dev-cmd@v1.12.1
uses: ilammy/msvc-dev-cmd@v1.13.0
- name: Setup conan variables
if: startsWith(matrix.os, 'windows')
@ -83,7 +83,7 @@ jobs:
sccache-test-${{ matrix.os }}-${{ matrix.qt-version }}
- name: Cache conan packages
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: ${{ runner.os }}-conan-user-${{ hashFiles('**/conanfile.py') }}${{ env.C2_CONAN_CACHE_SUFFIX }}
path: ~/.conan2/

View file

@ -1,5 +1,7 @@
# JSON resources should not be prettified...
resources/*.json
benchmarks/resources/*.json
tests/resources/*.json
# ...themes should be prettified for readability.
!resources/themes/*.json
@ -7,6 +9,7 @@ resources/*.json
lib/*/
conan-pkgs/*/
cmake/sanitizers-cmake/
tools/crash-handler
# Build folders
*build-*/

View file

@ -3,7 +3,8 @@
## Unversioned
- Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922)
- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986)
- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026)
- Major: Show restricted chat messages and suspicious treatment updates. (#5056, #5060)
- Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809)
- Minor: The account switcher is now styled to match your theme. (#4817)
- Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795)
@ -16,8 +17,13 @@
- Minor: Add an option to use new experimental smarter emote completion. (#4987)
- Minor: Add `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985)
- Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008)
- Minor: Add a new completion API for experimental plugins feature. (#5000)
- Minor: Add a new completion API for experimental plugins feature. (#5000, #5047)
- Minor: Re-enabled _Restart on crash_ option on Windows. (#5012)
- Minor: The whisper highlight color can now be configured through the settings. (#5053)
- Minor: Added missing periods at various moderator messages and commands. (#5061)
- Minor: Improved color selection and display. (#5057)
- Minor: Improved Streamlink documentation in the settings dialog. (#5076)
- Minor: Normalized the input padding between light & dark themes. (#5095)
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848)
- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834)
@ -52,11 +58,16 @@
- Bugfix: Hide the Usercard button in the User Info Popup when in special channels. (#4972)
- Bugfix: Fixed support for Windows 11 Snap layouts. (#4994)
- Bugfix: Fixed some windows appearing between screens. (#4797)
- Bugfix: Fixed a crash that could occur when using certain features in a Usercard after closing the split from which it was created. (#5034)
- Bugfix: Fixed a crash that could occur when using certain features in a Reply popup after closing the split from which it was created. (#5036)
- Bugfix: Fixed a bug on Wayland where tooltips would spawn as separate windows instead of behaving like tooltips. (#4998)
- Bugfix: Fixed a crash that could occur when using certain features in a Usercard after closing the split from which it was created. (#5034, #5051)
- Bugfix: Fixed a crash that could occur when using certain features in a Reply popup after closing the split from which it was created. (#5036, #5051)
- Bugfix: Fixed a bug on Wayland where tooltips would spawn as separate windows instead of behaving like tooltips. (#4998, #5040)
- Bugfix: Fixes to section deletion in text input fields. (#5013)
- Bugfix: Show user text input within watch streak notices. (#5029)
- Bugfix: Fixed avatar in usercard and moderation button triggering when releasing the mouse outside their area. (#5052)
- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056)
- Bugfix: Fixed a bug where buttons would remain in a hovered state after leaving them. (#5077)
- Bugfix: Fixed popup windows not persisting between restarts. (#5081)
- Bugfix: Fixed splits not retaining their focus after minimizing. (#5080)
- Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978)
- Dev: Change clang-format from v14 to v16. (#4929)
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)
@ -83,6 +94,8 @@
- Dev: Refactor `Emoji`'s EmojiMap into a vector. (#4980)
- Dev: Refactor `DebugCount` and add copy button to debug popup. (#4921)
- Dev: Refactor `common/Credentials`. (#4979)
- Dev: Refactor chat logger. (#5058)
- Dev: Refactor Twitch PubSub client. (#5059)
- Dev: Changed lifetime of context menus. (#4924)
- Dev: Renamed `tools` directory to `scripts`. (#5035)
- Dev: Refactor `ChannelView`, removing a bunch of clang-tidy warnings. (#4926)
@ -92,13 +105,24 @@
- Dev: Added Tests for Windows and MacOS in CI. (#4970, #5032)
- Dev: Move `clang-tidy` checker to its own CI job. (#4996)
- Dev: Refactored the Image Uploader feature. (#4971)
- Dev: Refactored the SplitOverlay code. (#5082)
- Dev: Refactored the TwitchBadges structure, making it less of a singleton. (#5096)
- Dev: Moved the Network files to their own folder. (#5089)
- Dev: Fixed deadlock and use-after-free in tests. (#4981)
- Dev: Moved all `.clang-format` files to the root directory. (#5037)
- Dev: Load less message history upon reconnects. (#5001, #5018)
- Dev: Load less message history upon reconnects. (#5001)
- Dev: Removed the `NullablePtr` class. (#5091)
- Dev: BREAKING: Replace custom `import()` with normal Lua `require()`. (#5014)
- Dev: Fixed most compiler warnings. (#5028)
- Dev: Added the ability to show `ChannelView`s without a `Split`. (#4747)
- Dev: Refactor Args to be less of a singleton. (#5041)
- Dev: Channels without any animated elements on screen will skip updates from the GIF timer. (#5042, #5043, #5045)
- Dev: Autogenerate docs/plugin-meta.lua. (#5055)
- Dev: Refactor `NetworkPrivate`. (#5063)
- Dev: Refactor `Paths` & `Updates`, focusing on reducing their singletoniability. (#5092)
- Dev: Removed duplicate scale in settings dialog. (#5069)
- Dev: Fix `NotebookTab` emitting updates for every message. (#5068)
- Dev: Added benchmark for parsing and building recent messages. (#5071)
## 2.4.6

View file

@ -1,13 +1,16 @@
project(chatterino-benchmark)
set(benchmark_SOURCES
${CMAKE_CURRENT_LIST_DIR}/src/main.cpp
${CMAKE_CURRENT_LIST_DIR}/src/Emojis.cpp
${CMAKE_CURRENT_LIST_DIR}/src/Highlights.cpp
${CMAKE_CURRENT_LIST_DIR}/src/FormatTime.cpp
${CMAKE_CURRENT_LIST_DIR}/src/Helpers.cpp
${CMAKE_CURRENT_LIST_DIR}/src/LimitedQueue.cpp
${CMAKE_CURRENT_LIST_DIR}/src/LinkParser.cpp
src/main.cpp
resources/bench.qrc
src/Emojis.cpp
src/Highlights.cpp
src/FormatTime.cpp
src/Helpers.cpp
src/LimitedQueue.cpp
src/LinkParser.cpp
src/RecentMessages.cpp
# Add your new file above this line!
)
@ -27,4 +30,5 @@ set_target_properties(${PROJECT_NAME}
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin"
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin"
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin"
AUTORCC ON
)

View file

@ -0,0 +1,6 @@
<RCC>
<qresource prefix="/bench">
<file>recentmessages-nymn.json</file>
<file>seventvemotes-nymn.json</file>
</qresource>
</RCC>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -145,16 +145,13 @@ static void BM_EmojiParsing(benchmark::State &state)
BENCHMARK(BM_EmojiParsing);
template <class... Args>
static void BM_EmojiParsing2(benchmark::State &state, Args &&...args)
static void BM_EmojiParsing2(benchmark::State &state, const QString &input,
int expectedNumEmojis)
{
Emojis emojis;
emojis.load();
auto argsTuple = std::make_tuple(std::move(args)...);
auto input = std::get<0>(argsTuple);
auto expectedNumEmojis = std::get<1>(argsTuple);
for (auto _ : state)
{
auto output = emojis.parse(input);

View file

@ -4,35 +4,41 @@
using namespace chatterino;
template <class... Args>
void BM_TimeFormatting(benchmark::State &state, Args &&...args)
void BM_TimeFormattingQString(benchmark::State &state, const QString &v)
{
auto args_tuple = std::make_tuple(std::move(args)...);
for (auto _ : state)
{
formatTime(std::get<0>(args_tuple));
formatTime(v);
}
}
BENCHMARK_CAPTURE(BM_TimeFormatting, 0, 0);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs0, "0");
BENCHMARK_CAPTURE(BM_TimeFormatting, 1337, 1337);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs1337, "1337");
BENCHMARK_CAPTURE(BM_TimeFormatting, 623452, 623452);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs623452, "623452");
BENCHMARK_CAPTURE(BM_TimeFormatting, 8345, 8345);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs8345, "8345");
BENCHMARK_CAPTURE(BM_TimeFormatting, 314034, 314034);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs314034, "314034");
BENCHMARK_CAPTURE(BM_TimeFormatting, 27, 27);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs27, "27");
BENCHMARK_CAPTURE(BM_TimeFormatting, 34589, 34589);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs34589, "34589");
BENCHMARK_CAPTURE(BM_TimeFormatting, 3659, 3659);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs3659, "3659");
BENCHMARK_CAPTURE(BM_TimeFormatting, 1045345, 1045345);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs1045345, "1045345");
BENCHMARK_CAPTURE(BM_TimeFormatting, 86432, 86432);
BENCHMARK_CAPTURE(BM_TimeFormatting, qs86432, "86432");
BENCHMARK_CAPTURE(BM_TimeFormatting, qsempty, "");
BENCHMARK_CAPTURE(BM_TimeFormatting, qsinvalid, "asd");
void BM_TimeFormattingInt(benchmark::State &state, int v)
{
for (auto _ : state)
{
formatTime(v);
}
}
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 0, 0);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 1045345, 1045345);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 1337, 1337);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 27, 27);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 314034, 314034);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 34589, 34589);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 3659, 3659);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 623452, 623452);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 8345, 8345);
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 86432, 86432);
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs0, "0");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs1045345, "1045345");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs1337, "1337");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs27, "27");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs314034, "314034");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs34589, "34589");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs3659, "3659");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs623452, "623452");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs8345, "8345");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs86432, "86432");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qsempty, "");
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qsinvalid, "asd");

View file

@ -0,0 +1,223 @@
#include "common/Literals.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp"
#include "messages/Emote.hpp"
#include "mocks/EmptyApplication.hpp"
#include "mocks/TwitchIrcServer.hpp"
#include "mocks/UserData.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/ffz/FfzBadges.hpp"
#include "providers/ffz/FfzEmotes.hpp"
#include "providers/recentmessages/Impl.hpp"
#include "providers/seventv/SeventvBadges.hpp"
#include "providers/seventv/SeventvEmotes.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Resources.hpp"
#include <benchmark/benchmark.h>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QString>
#include <optional>
using namespace chatterino;
using namespace literals;
namespace {
class MockApplication : mock::EmptyApplication
{
public:
IEmotes *getEmotes() override
{
return &this->emotes;
}
IUserDataController *getUserData() override
{
return &this->userData;
}
AccountController *getAccounts() override
{
return &this->accounts;
}
ITwitchIrcServer *getTwitch() override
{
return &this->twitch;
}
ChatterinoBadges *getChatterinoBadges() override
{
return &this->chatterinoBadges;
}
FfzBadges *getFfzBadges() override
{
return &this->ffzBadges;
}
SeventvBadges *getSeventvBadges() override
{
return &this->seventvBadges;
}
HighlightController *getHighlights() override
{
return &this->highlights;
}
AccountController accounts;
Emotes emotes;
mock::UserDataController userData;
mock::MockTwitchIrcServer twitch;
ChatterinoBadges chatterinoBadges;
FfzBadges ffzBadges;
SeventvBadges seventvBadges;
HighlightController highlights;
};
std::optional<QJsonDocument> tryReadJsonFile(const QString &path)
{
QFile file(path);
if (!file.open(QFile::ReadOnly))
{
return std::nullopt;
}
QJsonParseError e;
auto doc = QJsonDocument::fromJson(file.readAll(), &e);
if (e.error != QJsonParseError::NoError)
{
return std::nullopt;
}
return doc;
}
QJsonDocument readJsonFile(const QString &path)
{
auto opt = tryReadJsonFile(path);
if (!opt)
{
_exit(1);
}
return *opt;
}
class RecentMessages
{
public:
explicit RecentMessages(const QString &name_)
: name(name_)
, chan(this->name)
{
const auto seventvEmotes =
tryReadJsonFile(u":/bench/seventvemotes-%1.json"_s.arg(this->name));
const auto bttvEmotes =
tryReadJsonFile(u":/bench/bttvemotes-%1.json"_s.arg(this->name));
const auto ffzEmotes =
tryReadJsonFile(u":/bench/ffzemotes-%1.json"_s.arg(this->name));
if (seventvEmotes)
{
this->chan.setSeventvEmotes(
std::make_shared<const EmoteMap>(seventv::detail::parseEmotes(
seventvEmotes->object()["emote_set"_L1]
.toObject()["emotes"_L1]
.toArray(),
false)));
}
if (bttvEmotes)
{
this->chan.setBttvEmotes(std::make_shared<const EmoteMap>(
bttv::detail::parseChannelEmotes(bttvEmotes->object(),
this->name)));
}
if (ffzEmotes)
{
this->chan.setFfzEmotes(std::make_shared<const EmoteMap>(
ffz::detail::parseChannelEmotes(ffzEmotes->object())));
}
this->messages =
readJsonFile(u":/bench/recentmessages-%1.json"_s.arg(this->name));
}
~RecentMessages()
{
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
}
virtual void run(benchmark::State &state) = 0;
protected:
QString name;
MockApplication app;
TwitchChannel chan;
QJsonDocument messages;
};
class ParseRecentMessages : public RecentMessages
{
public:
explicit ParseRecentMessages(const QString &name_)
: RecentMessages(name_)
{
}
void run(benchmark::State &state)
{
for (auto _ : state)
{
auto parsed = recentmessages::detail::parseRecentMessages(
this->messages.object());
benchmark::DoNotOptimize(parsed);
}
}
};
class BuildRecentMessages : public RecentMessages
{
public:
explicit BuildRecentMessages(const QString &name_)
: RecentMessages(name_)
{
}
void run(benchmark::State &state)
{
auto parsed = recentmessages::detail::parseRecentMessages(
this->messages.object());
for (auto _ : state)
{
auto built = recentmessages::detail::buildRecentMessages(
parsed, &this->chan);
benchmark::DoNotOptimize(built);
}
}
};
void BM_ParseRecentMessages(benchmark::State &state, const QString &name)
{
ParseRecentMessages bench(name);
bench.run(state);
}
void BM_BuildRecentMessages(benchmark::State &state, const QString &name)
{
BuildRecentMessages bench(name);
bench.run(state);
}
} // namespace
BENCHMARK_CAPTURE(BM_ParseRecentMessages, nymn, u"nymn"_s);
BENCHMARK_CAPTURE(BM_BuildRecentMessages, nymn, u"nymn"_s);

View file

@ -1,3 +1,4 @@
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
#include <benchmark/benchmark.h>
@ -11,6 +12,8 @@ int main(int argc, char **argv)
{
QApplication app(argc, argv);
initResources();
::benchmark::Initialize(&argc, argv);
// Ensure settings are initialized before any benchmarks are run

View file

@ -26,7 +26,7 @@ declare module c2 {
}
enum EventType {
RegisterCompletions = "RegisterCompletions",
CompletionRequested = "CompletionRequested",
}
type CbFuncCompletionsRequested = (
@ -35,7 +35,7 @@ declare module c2 {
cursor_position: number,
is_first_word: boolean
) => CompletionList;
type CbFunc<T> = T extends EventType.RegisterCompletions
type CbFunc<T> = T extends EventType.CompletionRequested
? CbFuncCompletionsRequested
: never;

View file

@ -43,7 +43,8 @@
"type": "string",
"description": "A small description of your license.",
"examples": ["MIT", "GPL-2.0-or-later"]
}
},
"$schema": { "type": "string" }
},
"required": ["name", "description", "authors", "version", "license"]
}

58
docs/plugin-meta.lua Normal file
View file

@ -0,0 +1,58 @@
---@meta Chatterino2
-- This file is automatically generated from src/controllers/plugins/LuaAPI.hpp by the scripts/make_luals_meta.py script
-- This file is intended to be used with LuaLS (https://luals.github.io/).
-- Add the folder this file is in to "Lua.workspace.library".
c2 = {}
---@alias LogLevel integer
---@type { Debug: LogLevel, Info: LogLevel, Warning: LogLevel, Critical: LogLevel }
c2.LogLevel = {}
---@alias EventType integer
---@type { CompletionRequested: EventType }
c2.EventType = {}
---@class CommandContext
---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`.
---@field channel_name string The name of the channel the command was executed in.
---@class CompletionList
---@field values string[] The completions
---@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored.
--- Registers a new command called `name` which when executed will call `handler`.
---
---@param name string The name of the command.
---@param handler fun(ctx: CommandContext) The handler to be invoked when the command gets executed.
---@return boolean ok Returns `true` if everything went ok, `false` if a command with this name exists.
function c2.register_command(name, handler) end
--- Registers a callback to be invoked when completions for a term are requested.
---
---@param type "CompletionRequested"
---@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked.
function c2.register_callback(type, func) end
--- Sends a message to `channel` with the specified text. Also executes commands.
---
--- **Warning**: It is possible to trigger your own Lua command with this causing a potentially infinite loop.
---
---@param channel string The name of the Twitch channel
---@param text string The text to be sent
---@return boolean ok
function c2.send_msg(channel, text) end
--- Creates a system message (gray message) and adds it to the Twitch channel specified by `channel`.
---
---@param channel string
---@param text string
---@return boolean ok
function c2.system_msg(channel, text) end
--- Writes a message to the Chatterino log.
---
---@param level LogLevel The desired level.
---@param ... any Values to log. Should be convertible to a string with `tostring()`.
function c2.log(level, ...) end

View file

@ -113,6 +113,42 @@ Limitations/known issues:
rebuilding the window content caused by reloading another plugin will solve this.
- Spaces in command names aren't handled very well (https://github.com/Chatterino/chatterino2/issues/1517).
#### `register_callback("CompletionRequested", handler)`
Registers a callback (`handler`) to process completions. The callback gets the following parameters:
- `query`: The queried word.
- `full_text_content`: The whole input.
- `cursor_position`: The position of the cursor in the input.
- `is_first_word`: Flag whether `query` is the first word in the input.
Example:
| Input | `query` | `full_text_content` | `cursor_position` | `is_first_word` |
| ---------- | ------- | ------------------- | ----------------- | --------------- |
| `foo│` | `foo` | `foo` | 3 | `true` |
| `fo│o` | `fo` | `foo` | 2 | `true` |
| `foo bar│` | `bar` | `foo bar` | 7 | `false` |
| `foo │bar` | `foo` | `foo bar` | 4 | `false` |
```lua
function string.startswith(s, other)
return string.sub(s, 1, string.len(other)) == other
end
c2.register_callback(
"CompletionRequested",
function(query, full_text_content, cursor_position, is_first_word)
if ("!join"):startswith(query) then
---@type CompletionList
return { hide_others = true, values = { "!join" } }
end
---@type CompletionList
return { hide_others = false, values = {} }
end
)
```
#### `send_msg(channel, text)`
Sends a message to `channel` with the specified text. Also executes commands.

@ -1 +1 @@
Subproject commit bbf0a34260a3e8d6e6c48be57653840ac3fa8c30
Subproject commit 17946d65a41a72b447da37df6e314cded9650c32

@ -1 +1 @@
Subproject commit f92bc7bc4940bf58b7f03cefa81a78ef09752007
Subproject commit 87ed4d950319d8da1191431f5c8c84d1fdcb92a5

@ -1 +1 @@
Subproject commit ca452a811d684db42f93d6352301406754d0c536
Subproject commit d06770649a7e83db780865d09c313a876bf0f4eb

View file

@ -1,81 +1,150 @@
#pragma once
#include "Application.hpp"
#include "common/Args.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Updates.hpp"
namespace chatterino::mock {
class EmptyApplication : public IApplication
{
public:
EmptyApplication()
: updates_(this->paths_)
{
}
virtual ~EmptyApplication() = default;
const Paths &getPaths() override
{
return this->paths_;
}
const Args &getArgs() override
{
return this->args_;
}
Theme *getThemes() override
{
assert(
false &&
"EmptyApplication::getThemes was called without being initialized");
return nullptr;
}
Fonts *getFonts() override
{
assert(
false &&
"EmptyApplication::getFonts was called without being initialized");
return nullptr;
}
IEmotes *getEmotes() override
{
assert(
false &&
"EmptyApplication::getEmotes was called without being initialized");
return nullptr;
}
AccountController *getAccounts() override
{
assert(false && "EmptyApplication::getAccounts was called without "
"being initialized");
return nullptr;
}
HotkeyController *getHotkeys() override
{
assert(false && "EmptyApplication::getHotkeys was called without being "
"initialized");
return nullptr;
}
WindowManager *getWindows() override
{
assert(false && "EmptyApplication::getWindows was called without being "
"initialized");
return nullptr;
}
Toasts *getToasts() override
{
assert(
false &&
"EmptyApplication::getToasts was called without being initialized");
return nullptr;
}
CrashHandler *getCrashHandler() override
{
assert(false && "EmptyApplication::getCrashHandler was called without "
"being initialized");
return nullptr;
}
CommandController *getCommands() override
{
assert(false && "EmptyApplication::getCommands was called without "
"being initialized");
return nullptr;
}
NotificationController *getNotifications() override
{
assert(false && "EmptyApplication::getNotifications was called without "
"being initialized");
return nullptr;
}
HighlightController *getHighlights() override
{
assert(false && "EmptyApplication::getHighlights was called without "
"being initialized");
return nullptr;
}
ITwitchIrcServer *getTwitch() override
{
assert(
false &&
"EmptyApplication::getTwitch was called without being initialized");
return nullptr;
}
PubSub *getTwitchPubSub() override
{
assert(false && "getTwitchPubSub was called without being initialized");
return nullptr;
}
TwitchBadges *getTwitchBadges() override
{
assert(false && "getTwitchBadges was called without being initialized");
return nullptr;
}
Logging *getChatLogger() override
{
assert(!"getChatLogger was called without being initialized");
return nullptr;
}
ChatterinoBadges *getChatterinoBadges() override
{
assert(false && "EmptyApplication::getChatterinoBadges was called "
"without being initialized");
return nullptr;
}
FfzBadges *getFfzBadges() override
{
assert(false && "EmptyApplication::getFfzBadges was called without "
"being initialized");
return nullptr;
}
@ -87,6 +156,8 @@ public:
IUserDataController *getUserData() override
{
assert(false && "EmptyApplication::getUserData was called without "
"being initialized");
return nullptr;
}
@ -98,11 +169,15 @@ public:
ITwitchLiveController *getTwitchLiveController() override
{
assert(false && "EmptyApplication::getTwitchLiveController was called "
"without being initialized");
return nullptr;
}
ImageUploader *getImageUploader() override
{
assert(false && "EmptyApplication::getImageUploader was called without "
"being initialized");
return nullptr;
}
@ -110,6 +185,16 @@ public:
{
return nullptr;
}
Updates &getUpdates() override
{
return this->updates_;
}
private:
Paths paths_;
Args args_;
Updates updates_;
};
} // namespace chatterino::mock

BIN
resources/avatars/fraxx.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

View file

@ -66,6 +66,8 @@ olafyang | https://github.com/olafyang | | Contributor
chrrs | https://github.com/chrrs | | Contributor
4rneee | https://github.com/4rneee | | Contributor
crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png | Contributor
SputNikPlop | https://github.com/SputNikPlop | | Contributor
fraxx | https://github.com/fraxxio | :/avatars/fraxx.png | Contributor
# If you are a contributor add yourself above this line

12
scripts/check-clang-tidy.sh Executable file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
set -eu
clang-tidy --version
find \
src/ \
tests/src/ \
benchmarks/src/ \
mocks/include/ \
-type f \( -name "*.hpp" -o -name "*.cpp" \) -print0 | parallel -0 -j16 -I {} clang-tidy --quiet "$@" "{}"

142
scripts/make_luals_meta.py Normal file
View file

@ -0,0 +1,142 @@
"""
This script generates docs/plugin-meta.lua. It accepts no arguments
It assumes comments look like:
/**
* Thing
*
* @lua@param thing boolean
* @lua@returns boolean
* @exposed name
*/
- Do not have any useful info on '/**' and '*/' lines.
- Class members are not allowed to have non-@command lines and commands different from @lua@field
Valid commands are:
1. @exposeenum [dotted.name.in_lua.last_part]
Define a table with keys of the enum. Values behind those keys aren't
written on purpose.
This generates three lines:
- An type alias of [last_part] to integer,
- A type description that describes available values of the enum,
- A global table definition for the num
2. @lua[@command]
Writes [@command] to the file as a comment, usually this is @class, @param, @return, ...
@lua@class and @lua@field have special treatment when it comes to generation of spacing new lines
3. @exposed [c2.name]
Generates a function definition line from the last `@lua@param`s.
Non-command lines of comments are written with a space after '---'
"""
from pathlib import Path
BOILERPLATE = """
---@meta Chatterino2
-- This file is automatically generated from src/controllers/plugins/LuaAPI.hpp by the scripts/make_luals_meta.py script
-- This file is intended to be used with LuaLS (https://luals.github.io/).
-- Add the folder this file is in to "Lua.workspace.library".
c2 = {}
"""
repo_root = Path(__file__).parent.parent
lua_api_file = repo_root / "src" / "controllers" / "plugins" / "LuaAPI.hpp"
lua_meta = repo_root / "docs" / "plugin-meta.lua"
print("Reading from", lua_api_file.relative_to(repo_root))
print("Writing to", lua_meta.relative_to(repo_root))
with lua_api_file.open("r") as f:
lines = f.read().splitlines()
# Are we in a doc comment?
comment: bool = False
# Last `@lua@param`s seen - for @exposed generation
last_params_names: list[str] = []
# Are we in a `@lua@class` definition? - makes newlines around @lua@class and @lua@field prettier
is_class = False
# The name of the next enum in lua world
expose_next_enum_as: str | None = None
# Name of the current enum in c++ world, used to generate internal typenames for
current_enum_name: str | None = None
with lua_meta.open("w") as out:
out.write(BOILERPLATE[1:]) # skip the newline after triple quote
for line in lines:
line = line.strip()
if line.startswith("enum class "):
line = line.removeprefix("enum class ")
temp = line.split(" ", 2)
current_enum_name = temp[0]
if not expose_next_enum_as:
print(
f"Skipping enum {current_enum_name}, there wasn't a @exposeenum command"
)
current_enum_name = None
continue
current_enum_name = expose_next_enum_as.split(".", 1)[-1]
out.write("---@alias " + current_enum_name + " integer\n")
out.write("---@type { ")
# temp[1] is '{'
if len(temp) == 2: # no values on this line
continue
line = temp[2]
if current_enum_name is not None:
for i, tok in enumerate(line.split(" ")):
if tok == "};":
break
entry = tok.removesuffix(",")
if i != 0:
out.write(", ")
out.write(entry + ": " + current_enum_name)
out.write(" }\n" f"{expose_next_enum_as} = {{}}\n")
print(f"Wrote enum {expose_next_enum_as} => {current_enum_name}")
current_enum_name = None
expose_next_enum_as = None
continue
if line.startswith("/**"):
comment = True
continue
elif "*/" in line:
comment = False
if not is_class:
out.write("\n")
continue
if not comment:
continue
line = line.replace("*", "", 1).lstrip()
if line == "":
out.write("---\n")
elif line.startswith("@exposeenum "):
expose_next_enum_as = line.split(" ", 1)[1]
elif line.startswith("@exposed "):
exp = line.replace("@exposed ", "", 1)
params = ", ".join(last_params_names)
out.write(f"function {exp}({params}) end\n")
print(f"Wrote function {exp}(...)")
last_params_names = []
elif line.startswith("@lua"):
command = line.replace("@lua", "", 1)
if command.startswith("@param"):
last_params_names.append(command.split(" ", 2)[1])
elif command.startswith("@class"):
print(f"Writing {command}")
if is_class:
out.write("\n")
is_class = True
elif not command.startswith("@field"):
is_class = False
out.write("---" + command + "\n")
else:
if is_class:
is_class = False
out.write("\n")
# note the space difference from the branch above
out.write("--- " + line + "\n")

View file

@ -12,6 +12,7 @@
#include "controllers/notifications/NotificationController.hpp"
#include "controllers/sound/ISoundController.hpp"
#include "providers/seventv/SeventvAPI.hpp"
#include "providers/twitch/TwitchBadges.hpp"
#include "singletons/ImageUploader.hpp"
#ifdef CHATTERINO_HAVE_PLUGINS
# include "controllers/plugins/PluginController.hpp"
@ -35,6 +36,7 @@
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/PubSubManager.hpp"
#include "providers/twitch/PubSubMessages.hpp"
#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp"
@ -86,6 +88,8 @@ ISoundController *makeSoundController(Settings &settings)
}
}
const QString TWITCH_PUBSUB_URL = "wss://pubsub-edge.twitch.tv";
} // namespace
namespace chatterino {
@ -104,17 +108,20 @@ IApplication::IApplication()
// It will create the instances of the major classes, and connect their signals
// to each other
Application::Application(Settings &_settings, Paths &_paths)
: themes(&this->emplace<Theme>())
Application::Application(Settings &_settings, const Paths &paths,
const Args &_args, Updates &_updates)
: paths_(paths)
, args_(_args)
, themes(&this->emplace<Theme>())
, fonts(&this->emplace<Fonts>())
, emotes(&this->emplace<Emotes>())
, accounts(&this->emplace<AccountController>())
, hotkeys(&this->emplace<HotkeyController>())
, windows(&this->emplace<WindowManager>())
, windows(&this->emplace(new WindowManager(paths)))
, toasts(&this->emplace<Toasts>())
, imageUploader(&this->emplace<ImageUploader>())
, seventvAPI(&this->emplace<SeventvAPI>())
, crashHandler(&this->emplace<CrashHandler>())
, crashHandler(&this->emplace(new CrashHandler(paths)))
, commands(&this->emplace<CommandController>())
, notifications(&this->emplace<NotificationController>())
@ -123,15 +130,18 @@ Application::Application(Settings &_settings, Paths &_paths)
, chatterinoBadges(&this->emplace<ChatterinoBadges>())
, ffzBadges(&this->emplace<FfzBadges>())
, seventvBadges(&this->emplace<SeventvBadges>())
, userData(&this->emplace<UserDataController>())
, userData(&this->emplace(new UserDataController(paths)))
, sound(&this->emplace<ISoundController>(makeSoundController(_settings)))
, twitchLiveController(&this->emplace<TwitchLiveController>())
, twitchPubSub(new PubSub(TWITCH_PUBSUB_URL))
, twitchBadges(new TwitchBadges)
, logging(new Logging(_settings))
#ifdef CHATTERINO_HAVE_PLUGINS
, plugins(&this->emplace<PluginController>())
, plugins(&this->emplace(new PluginController(paths)))
#endif
, logging(&this->emplace<Logging>())
, updates(_updates)
{
this->instance = this;
Application::instance = this;
// We can safely ignore this signal's connection since the Application will always
// be destroyed after fonts
@ -140,19 +150,27 @@ Application::Application(Settings &_settings, Paths &_paths)
});
}
void Application::initialize(Settings &settings, Paths &paths)
Application::~Application() = default;
void Application::fakeDtor()
{
this->twitchPubSub.reset();
this->twitchBadges.reset();
}
void Application::initialize(Settings &settings, const Paths &paths)
{
assert(isAppInitialized == false);
isAppInitialized = true;
// Show changelog
if (!getArgs().isFramelessEmbed &&
if (!this->args_.isFramelessEmbed &&
getSettings()->currentVersion.getValue() != "" &&
getSettings()->currentVersion.getValue() != CHATTERINO_VERSION)
{
auto box = new QMessageBox(QMessageBox::Information, "Chatterino 2",
"Show changelog?",
QMessageBox::Yes | QMessageBox::No);
auto *box = new QMessageBox(QMessageBox::Information, "Chatterino 2",
"Show changelog?",
QMessageBox::Yes | QMessageBox::No);
box->setAttribute(Qt::WA_DeleteOnClose);
if (box->exec() == QMessageBox::Yes)
{
@ -161,7 +179,7 @@ void Application::initialize(Settings &settings, Paths &paths)
}
}
if (!getArgs().isFramelessEmbed)
if (!this->args_.isFramelessEmbed)
{
getSettings()->currentVersion.setValue(CHATTERINO_VERSION);
@ -179,12 +197,12 @@ void Application::initialize(Settings &settings, Paths &paths)
// Show crash message.
// On Windows, the crash message was already shown.
#ifndef Q_OS_WIN
if (!getArgs().isFramelessEmbed && getArgs().crashRecovery)
if (!this->args_.isFramelessEmbed && this->args_.crashRecovery)
{
if (auto selected =
if (auto *selected =
this->windows->getMainWindow().getNotebook().getSelectedPage())
{
if (auto container = dynamic_cast<SplitContainer *>(selected))
if (auto *container = dynamic_cast<SplitContainer *>(selected))
{
for (auto &&split : container->getSplits())
{
@ -203,7 +221,7 @@ void Application::initialize(Settings &settings, Paths &paths)
this->windows->updateWordTypeMask();
if (!getArgs().isFramelessEmbed)
if (!this->args_.isFramelessEmbed)
{
this->initNm(paths);
}
@ -219,14 +237,14 @@ int Application::run(QApplication &qtApp)
this->twitch->connect();
if (!getArgs().isFramelessEmbed)
if (!this->args_.isFramelessEmbed)
{
this->windows->getMainWindow().show();
}
getSettings()->betaUpdates.connect(
[] {
Updates::instance().checkForUpdates();
[this] {
this->updates.checkForUpdates();
},
false);
@ -305,11 +323,28 @@ ITwitchLiveController *Application::getTwitchLiveController()
return this->twitchLiveController;
}
TwitchBadges *Application::getTwitchBadges()
{
assert(this->twitchBadges);
return this->twitchBadges.get();
}
ITwitchIrcServer *Application::getTwitch()
{
return this->twitch;
}
PubSub *Application::getTwitchPubSub()
{
return this->twitchPubSub.get();
}
Logging *Application::getChatLogger()
{
return this->logging.get();
}
void Application::save()
{
for (auto &singleton : this->singletons_)
@ -318,7 +353,7 @@ void Application::save()
}
}
void Application::initNm(Paths &paths)
void Application::initNm(const Paths &paths)
{
(void)paths;
@ -334,7 +369,7 @@ void Application::initPubSub()
{
// We can safely ignore these signal connections since the twitch object will always
// be destroyed before the Application
std::ignore = this->twitch->pubsub->signals_.moderation.chatCleared.connect(
std::ignore = this->twitchPubSub->moderation.chatCleared.connect(
[this](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
@ -343,7 +378,7 @@ void Application::initPubSub()
}
QString text =
QString("%1 cleared the chat").arg(action.source.login);
QString("%1 cleared the chat.").arg(action.source.login);
auto msg = makeSystemMessage(text);
postToThread([chan, msg] {
@ -351,7 +386,7 @@ void Application::initPubSub()
});
});
std::ignore = this->twitch->pubsub->signals_.moderation.modeChanged.connect(
std::ignore = this->twitchPubSub->moderation.modeChanged.connect(
[this](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
@ -360,7 +395,7 @@ void Application::initPubSub()
}
QString text =
QString("%1 turned %2 %3 mode")
QString("%1 turned %2 %3 mode.")
.arg(action.source.login)
.arg(action.state == ModeChangedAction::State::On ? "on"
: "off")
@ -377,29 +412,28 @@ void Application::initPubSub()
});
});
std::ignore =
this->twitch->pubsub->signals_.moderation.moderationStateChanged
.connect([this](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
std::ignore = this->twitchPubSub->moderation.moderationStateChanged.connect(
[this](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
QString text;
QString text;
text = QString("%1 %2 %3")
.arg(action.source.login,
(action.modded ? "modded" : "unmodded"),
action.target.login);
text = QString("%1 %2 %3.")
.arg(action.source.login,
(action.modded ? "modded" : "unmodded"),
action.target.login);
auto msg = makeSystemMessage(text);
postToThread([chan, msg] {
chan->addMessage(msg);
});
auto msg = makeSystemMessage(text);
postToThread([chan, msg] {
chan->addMessage(msg);
});
});
std::ignore = this->twitch->pubsub->signals_.moderation.userBanned.connect(
std::ignore = this->twitchPubSub->moderation.userBanned.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
@ -414,225 +448,307 @@ void Application::initPubSub()
chan->addOrReplaceTimeout(msg.release());
});
});
std::ignore =
this->twitch->pubsub->signals_.moderation.messageDeleted.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
std::ignore = this->twitchPubSub->moderation.messageDeleted.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty() || getSettings()->hideDeletionActions)
if (chan->isEmpty() || getSettings()->hideDeletionActions)
{
return;
}
MessageBuilder msg;
TwitchMessageBuilder::deletionMessage(action, &msg);
msg->flags.set(MessageFlag::PubSub);
postToThread([chan, msg = msg.release()] {
auto replaced = false;
LimitedQueueSnapshot<MessagePtr> snapshot =
chan->getMessageSnapshot();
int snapshotLength = snapshot.size();
// without parens it doesn't build on windows
int end = (std::max)(0, snapshotLength - 200);
for (int i = snapshotLength - 1; i >= end; --i)
{
return;
const auto &s = snapshot[i];
if (!s->flags.has(MessageFlag::PubSub) &&
s->timeoutUser == msg->timeoutUser)
{
chan->replaceMessage(s, msg);
replaced = true;
break;
}
}
MessageBuilder msg;
TwitchMessageBuilder::deletionMessage(action, &msg);
msg->flags.set(MessageFlag::PubSub);
postToThread([chan, msg = msg.release()] {
auto replaced = false;
LimitedQueueSnapshot<MessagePtr> snapshot =
chan->getMessageSnapshot();
int snapshotLength = snapshot.size();
// without parens it doesn't build on windows
int end = (std::max)(0, snapshotLength - 200);
for (int i = snapshotLength - 1; i >= end; --i)
{
auto &s = snapshot[i];
if (!s->flags.has(MessageFlag::PubSub) &&
s->timeoutUser == msg->timeoutUser)
{
chan->replaceMessage(s, msg);
replaced = true;
break;
}
}
if (!replaced)
{
chan->addMessage(msg);
}
});
});
std::ignore =
this->twitch->pubsub->signals_.moderation.userUnbanned.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
if (!replaced)
{
return;
}
auto msg = MessageBuilder(action).release();
postToThread([chan, msg] {
chan->addMessage(msg);
});
}
});
});
std::ignore = this->twitchPubSub->moderation.userUnbanned.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
auto msg = MessageBuilder(action).release();
postToThread([chan, msg] {
chan->addMessage(msg);
});
});
std::ignore =
this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect(
[&](const auto &msg, const QString &channelID) {
auto chan = this->twitch->getChannelOrEmptyByID(channelID);
if (chan->isEmpty())
this->twitchPubSub->moderation.suspiciousMessageReceived.connect(
[&](const auto &action) {
if (action.treatment ==
PubSubLowTrustUsersMessage::Treatment::INVALID)
{
qCWarning(chatterinoTwitch)
<< "Received suspicious message with unknown "
"treatment:"
<< action.treatmentString;
return;
}
// monitored chats are received over irc; in the future, we will use pubsub instead
if (action.treatment !=
PubSubLowTrustUsersMessage::Treatment::Restricted)
{
return;
}
switch (msg.type)
{
case PubSubAutoModQueueMessage::Type::
AutoModCaughtMessage: {
if (msg.status == "PENDING")
{
AutomodAction action(msg.data, channelID);
action.reason = QString("%1 level %2")
.arg(msg.contentCategory)
.arg(msg.contentLevel);
action.msgID = msg.messageID;
action.message = msg.messageText;
// this message also contains per-word automod data, which could be implemented
// extract sender data manually because Twitch loves not being consistent
QString senderDisplayName =
msg.senderUserDisplayName; // Might be transformed later
bool hasLocalizedName = false;
if (!msg.senderUserDisplayName.isEmpty())
{
// check for non-ascii display names
if (QString::compare(msg.senderUserDisplayName,
msg.senderUserLogin,
Qt::CaseInsensitive) != 0)
{
hasLocalizedName = true;
}
}
QColor senderColor = msg.senderUserChatColor;
QString senderColor_;
if (!senderColor.isValid() &&
getSettings()->colorizeNicknames)
{
// color may be not present if user is a grey-name
senderColor = getRandomColor(msg.senderUserID);
}
// handle username style based on prefered setting
switch (
getSettings()->usernameDisplayMode.getValue())
{
case UsernameDisplayMode::Username: {
if (hasLocalizedName)
{
senderDisplayName = msg.senderUserLogin;
}
break;
}
case UsernameDisplayMode::LocalizedName: {
break;
}
case UsernameDisplayMode::
UsernameAndLocalizedName: {
if (hasLocalizedName)
{
senderDisplayName =
QString("%1(%2)").arg(
msg.senderUserLogin,
msg.senderUserDisplayName);
}
break;
}
}
action.target = ActionUser{
msg.senderUserID, msg.senderUserLogin,
senderDisplayName, senderColor};
postToThread([chan, action] {
const auto p =
makeAutomodMessage(action, chan->getName());
chan->addMessage(p.first);
chan->addMessage(p.second);
getApp()->twitch->automodChannel->addMessage(
p.first);
getApp()->twitch->automodChannel->addMessage(
p.second);
});
}
// "ALLOWED" and "DENIED" statuses remain unimplemented
// They are versions of automod_message_(denied|approved) but for mods.
}
break;
case PubSubAutoModQueueMessage::Type::INVALID:
default: {
}
break;
}
});
std::ignore =
this->twitch->pubsub->signals_.moderation.autoModMessageBlocked.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p = makeAutomodMessage(action, chan->getName());
chan->addMessage(p.first);
chan->addMessage(p.second);
});
});
std::ignore =
this->twitch->pubsub->signals_.moderation.automodUserMessage.connect(
[&](const auto &action) {
// This condition has been set up to execute isInStreamerMode() as the last thing
// as it could end up being expensive.
if (getSettings()->streamerModeHideModActions &&
isInStreamerMode())
{
return;
}
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
auto chan =
this->twitch->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
auto msg = MessageBuilder(action).release();
auto twitchChannel =
std::dynamic_pointer_cast<TwitchChannel>(chan);
if (!twitchChannel)
{
return;
}
postToThread([chan, msg] {
chan->addMessage(msg);
postToThread([twitchChannel, action] {
const auto p =
TwitchMessageBuilder::makeLowTrustUserMessage(
action, twitchChannel->getName(),
twitchChannel.get());
twitchChannel->addMessage(p.first);
twitchChannel->addMessage(p.second);
});
chan->deleteMessage(msg->id);
});
std::ignore =
this->twitch->pubsub->signals_.moderation.automodInfoMessage.connect(
this->twitchPubSub->moderation.suspiciousTreatmentUpdated.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (action.treatment ==
PubSubLowTrustUsersMessage::Treatment::INVALID)
{
qCWarning(chatterinoTwitch)
<< "Received suspicious user update with unknown "
"treatment:"
<< action.treatmentString;
return;
}
if (action.updatedByUserLogin.isEmpty())
{
return;
}
if (getSettings()->streamerModeHideModActions &&
isInStreamerMode())
{
return;
}
auto chan =
this->twitch->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p = makeAutomodInfoMessage(action);
chan->addMessage(p);
auto msg =
TwitchMessageBuilder::makeLowTrustUpdateMessage(action);
chan->addMessage(msg);
});
});
std::ignore = this->twitch->pubsub->signals_.pointReward.redeemed.connect(
[&](auto &data) {
std::ignore = this->twitchPubSub->moderation.autoModMessageCaught.connect(
[&](const auto &msg, const QString &channelID) {
auto chan = this->twitch->getChannelOrEmptyByID(channelID);
if (chan->isEmpty())
{
return;
}
switch (msg.type)
{
case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: {
if (msg.status == "PENDING")
{
AutomodAction action(msg.data, channelID);
action.reason = QString("%1 level %2")
.arg(msg.contentCategory)
.arg(msg.contentLevel);
action.msgID = msg.messageID;
action.message = msg.messageText;
// this message also contains per-word automod data, which could be implemented
// extract sender data manually because Twitch loves not being consistent
QString senderDisplayName =
msg.senderUserDisplayName; // Might be transformed later
bool hasLocalizedName = false;
if (!msg.senderUserDisplayName.isEmpty())
{
// check for non-ascii display names
if (QString::compare(msg.senderUserDisplayName,
msg.senderUserLogin,
Qt::CaseInsensitive) != 0)
{
hasLocalizedName = true;
}
}
QColor senderColor = msg.senderUserChatColor;
QString senderColor_;
if (!senderColor.isValid() &&
getSettings()->colorizeNicknames)
{
// color may be not present if user is a grey-name
senderColor = getRandomColor(msg.senderUserID);
}
// handle username style based on prefered setting
switch (getSettings()->usernameDisplayMode.getValue())
{
case UsernameDisplayMode::Username: {
if (hasLocalizedName)
{
senderDisplayName = msg.senderUserLogin;
}
break;
}
case UsernameDisplayMode::LocalizedName: {
break;
}
case UsernameDisplayMode::
UsernameAndLocalizedName: {
if (hasLocalizedName)
{
senderDisplayName = QString("%1(%2)").arg(
msg.senderUserLogin,
msg.senderUserDisplayName);
}
break;
}
}
action.target =
ActionUser{msg.senderUserID, msg.senderUserLogin,
senderDisplayName, senderColor};
postToThread([chan, action] {
const auto p =
TwitchMessageBuilder::makeAutomodMessage(
action, chan->getName());
chan->addMessage(p.first);
chan->addMessage(p.second);
getApp()->twitch->automodChannel->addMessage(
p.first);
getApp()->twitch->automodChannel->addMessage(
p.second);
});
}
// "ALLOWED" and "DENIED" statuses remain unimplemented
// They are versions of automod_message_(denied|approved) but for mods.
}
break;
case PubSubAutoModQueueMessage::Type::INVALID:
default: {
}
break;
}
});
std::ignore = this->twitchPubSub->moderation.autoModMessageBlocked.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p = TwitchMessageBuilder::makeAutomodMessage(
action, chan->getName());
chan->addMessage(p.first);
chan->addMessage(p.second);
});
});
std::ignore = this->twitchPubSub->moderation.automodUserMessage.connect(
[&](const auto &action) {
// This condition has been set up to execute isInStreamerMode() as the last thing
// as it could end up being expensive.
if (getSettings()->streamerModeHideModActions && isInStreamerMode())
{
return;
}
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
auto msg = MessageBuilder(action).release();
postToThread([chan, msg] {
chan->addMessage(msg);
});
chan->deleteMessage(msg->id);
});
std::ignore = this->twitchPubSub->moderation.automodInfoMessage.connect(
[&](const auto &action) {
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p =
TwitchMessageBuilder::makeAutomodInfoMessage(action);
chan->addMessage(p);
});
});
std::ignore =
this->twitchPubSub->pointReward.redeemed.connect([&](auto &data) {
QString channelId = data.value("channel_id").toString();
if (channelId.isEmpty())
{
@ -646,35 +762,26 @@ void Application::initPubSub()
auto reward = ChannelPointReward(data);
postToThread([chan, reward] {
if (auto channel = dynamic_cast<TwitchChannel *>(chan.get()))
if (auto *channel = dynamic_cast<TwitchChannel *>(chan.get()))
{
channel->addChannelPointReward(reward);
}
});
});
this->twitch->pubsub->start();
auto RequestModerationActions = [this]() {
this->twitch->pubsub->setAccount(
getApp()->accounts->twitch.getCurrent());
// TODO(pajlada): Unlisten to all authed topics instead of only
// moderation topics this->twitch->pubsub->UnlistenAllAuthedTopics();
this->twitch->pubsub->listenToWhispers();
};
this->twitchPubSub->start();
this->twitchPubSub->setAccount(this->accounts->twitch.getCurrent());
this->accounts->twitch.currentUserChanged.connect(
[this] {
this->twitch->pubsub->unlistenAllModerationActions();
this->twitch->pubsub->unlistenAutomod();
this->twitch->pubsub->unlistenWhispers();
this->twitchPubSub->unlistenChannelModerationActions();
this->twitchPubSub->unlistenAutomod();
this->twitchPubSub->unlistenLowTrustUsers();
this->twitchPubSub->unlistenChannelPointRewards();
this->twitchPubSub->setAccount(this->accounts->twitch.getCurrent());
},
boost::signals2::at_front);
this->accounts->twitch.currentUserChanged.connect(RequestModerationActions);
RequestModerationActions();
}
void Application::initBttvLiveUpdates()

View file

@ -11,9 +11,11 @@
namespace chatterino {
class Args;
class TwitchIrcServer;
class ITwitchIrcServer;
class PubSub;
class Updates;
class CommandController;
class AccountController;
@ -26,6 +28,7 @@ class ISoundController;
class SoundController;
class ITwitchLiveController;
class TwitchLiveController;
class TwitchBadges;
#ifdef CHATTERINO_HAVE_PLUGINS
class PluginController;
#endif
@ -54,6 +57,8 @@ public:
static IApplication *instance;
virtual const Paths &getPaths() = 0;
virtual const Args &getArgs() = 0;
virtual Theme *getThemes() = 0;
virtual Fonts *getFonts() = 0;
virtual IEmotes *getEmotes() = 0;
@ -66,18 +71,24 @@ public:
virtual HighlightController *getHighlights() = 0;
virtual NotificationController *getNotifications() = 0;
virtual ITwitchIrcServer *getTwitch() = 0;
virtual PubSub *getTwitchPubSub() = 0;
virtual Logging *getChatLogger() = 0;
virtual ChatterinoBadges *getChatterinoBadges() = 0;
virtual FfzBadges *getFfzBadges() = 0;
virtual SeventvBadges *getSeventvBadges() = 0;
virtual IUserDataController *getUserData() = 0;
virtual ISoundController *getSound() = 0;
virtual ITwitchLiveController *getTwitchLiveController() = 0;
virtual TwitchBadges *getTwitchBadges() = 0;
virtual ImageUploader *getImageUploader() = 0;
virtual SeventvAPI *getSeventvAPI() = 0;
virtual Updates &getUpdates() = 0;
};
class Application : public IApplication
{
const Paths &paths_;
const Args &args_;
std::vector<std::unique_ptr<Singleton>> singletons_;
int argc_{};
char **argv_{};
@ -85,9 +96,22 @@ class Application : public IApplication
public:
static Application *instance;
Application(Settings &settings, Paths &paths);
Application(Settings &_settings, const Paths &paths, const Args &_args,
Updates &_updates);
~Application() override;
void initialize(Settings &settings, Paths &paths);
Application(const Application &) = delete;
Application(Application &&) = delete;
Application &operator=(const Application &) = delete;
Application &operator=(Application &&) = delete;
/**
* In the interim, before we remove _exit(0); from RunGui.cpp,
* this will destroy things we know can be destroyed
*/
void fakeDtor();
void initialize(Settings &settings, const Paths &paths);
void load();
void save();
@ -118,14 +142,23 @@ public:
private:
TwitchLiveController *const twitchLiveController{};
std::unique_ptr<PubSub> twitchPubSub;
std::unique_ptr<TwitchBadges> twitchBadges;
const std::unique_ptr<Logging> logging;
public:
#ifdef CHATTERINO_HAVE_PLUGINS
PluginController *const plugins{};
#endif
/*[[deprecated]]*/ Logging *const logging{};
const Paths &getPaths() override
{
return this->paths_;
}
const Args &getArgs() override
{
return this->args_;
}
Theme *getThemes() override
{
return this->themes;
@ -168,6 +201,8 @@ public:
return this->highlights;
}
ITwitchIrcServer *getTwitch() override;
PubSub *getTwitchPubSub() override;
Logging *getChatLogger() override;
ChatterinoBadges *getChatterinoBadges() override
{
return this->chatterinoBadges;
@ -183,6 +218,7 @@ public:
IUserDataController *getUserData() override;
ISoundController *getSound() override;
ITwitchLiveController *getTwitchLiveController() override;
TwitchBadges *getTwitchBadges() override;
ImageUploader *getImageUploader() override
{
return this->imageUploader;
@ -191,6 +227,10 @@ public:
{
return this->seventvAPI;
}
Updates &getUpdates() override
{
return this->updates;
}
pajlada::Signals::NoArgSignal streamerModeChanged;
@ -199,7 +239,7 @@ private:
void initPubSub();
void initBttvLiveUpdates();
void initSeventvEventAPI();
void initNm(Paths &paths);
void initNm(const Paths &paths);
template <typename T,
typename = std::enable_if_t<std::is_base_of<Singleton, T>::value>>
@ -219,6 +259,7 @@ private:
}
NativeMessagingServer nmServer{};
Updates &updates;
};
Application *getApp();

View file

@ -33,16 +33,6 @@ set(SOURCE_FILES
common/Literals.hpp
common/Modes.cpp
common/Modes.hpp
common/NetworkCommon.cpp
common/NetworkCommon.hpp
common/NetworkManager.cpp
common/NetworkManager.hpp
common/NetworkPrivate.cpp
common/NetworkPrivate.hpp
common/NetworkRequest.cpp
common/NetworkRequest.hpp
common/NetworkResult.cpp
common/NetworkResult.hpp
common/QLogging.cpp
common/QLogging.hpp
common/WindowDescriptors.cpp
@ -50,6 +40,19 @@ set(SOURCE_FILES
common/enums/MessageOverflow.hpp
common/network/NetworkCommon.cpp
common/network/NetworkCommon.hpp
common/network/NetworkManager.cpp
common/network/NetworkManager.hpp
common/network/NetworkPrivate.cpp
common/network/NetworkPrivate.hpp
common/network/NetworkRequest.cpp
common/network/NetworkRequest.hpp
common/network/NetworkResult.cpp
common/network/NetworkResult.hpp
common/network/NetworkTask.cpp
common/network/NetworkTask.hpp
controllers/accounts/Account.cpp
controllers/accounts/Account.hpp
controllers/accounts/AccountController.cpp
@ -412,6 +415,8 @@ set(SOURCE_FILES
providers/twitch/pubsubmessages/ChatModeratorAction.hpp
providers/twitch/pubsubmessages/Listen.cpp
providers/twitch/pubsubmessages/Listen.hpp
providers/twitch/pubsubmessages/LowTrustUsers.cpp
providers/twitch/pubsubmessages/LowTrustUsers.hpp
providers/twitch/pubsubmessages/Message.hpp
providers/twitch/pubsubmessages/Unlisten.cpp
providers/twitch/pubsubmessages/Unlisten.hpp
@ -455,6 +460,7 @@ set(SOURCE_FILES
singletons/helper/LoggingChannel.cpp
singletons/helper/LoggingChannel.hpp
util/AbandonObject.hpp
util/AttachToConsole.cpp
util/AttachToConsole.hpp
util/CancellationToken.hpp
@ -588,12 +594,25 @@ set(SOURCE_FILES
widgets/dialogs/switcher/SwitchSplitItem.cpp
widgets/dialogs/switcher/SwitchSplitItem.hpp
widgets/helper/color/AlphaSlider.cpp
widgets/helper/color/AlphaSlider.hpp
widgets/helper/color/Checkerboard.cpp
widgets/helper/color/Checkerboard.hpp
widgets/helper/color/ColorButton.cpp
widgets/helper/color/ColorButton.hpp
widgets/helper/color/ColorInput.cpp
widgets/helper/color/ColorInput.hpp
widgets/helper/color/ColorItemDelegate.cpp
widgets/helper/color/ColorItemDelegate.hpp
widgets/helper/color/HueSlider.cpp
widgets/helper/color/HueSlider.hpp
widgets/helper/color/SBCanvas.cpp
widgets/helper/color/SBCanvas.hpp
widgets/helper/Button.cpp
widgets/helper/Button.hpp
widgets/helper/ChannelView.cpp
widgets/helper/ChannelView.hpp
widgets/helper/ColorButton.cpp
widgets/helper/ColorButton.hpp
widgets/helper/ComboBoxItemDelegate.cpp
widgets/helper/ComboBoxItemDelegate.hpp
widgets/helper/DebugPopup.cpp
@ -608,8 +627,6 @@ set(SOURCE_FILES
widgets/helper/NotebookButton.hpp
widgets/helper/NotebookTab.cpp
widgets/helper/NotebookTab.hpp
widgets/helper/QColorPicker.cpp
widgets/helper/QColorPicker.hpp
widgets/helper/RegExpItemDelegate.cpp
widgets/helper/RegExpItemDelegate.hpp
widgets/helper/TrimRegExpValidator.cpp
@ -679,6 +696,7 @@ set(SOURCE_FILES
widgets/splits/InputCompletionPopup.hpp
widgets/splits/Split.cpp
widgets/splits/Split.hpp
widgets/splits/SplitCommon.hpp
widgets/splits/SplitContainer.cpp
widgets/splits/SplitContainer.hpp
widgets/splits/SplitHeader.cpp
@ -918,6 +936,7 @@ target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
IRC_STATIC
IRC_NAMESPACE=Communi
)
if (USE_SYSTEM_QTKEYCHAIN)
target_compile_definitions(${LIBRARY_PROJECT} PUBLIC
CMAKE_BUILD

View file

@ -3,7 +3,7 @@
#include "Application.hpp"
#include "common/Args.hpp"
#include "common/Modes.hpp"
#include "common/NetworkManager.hpp"
#include "common/network/NetworkManager.hpp"
#include "common/QLogging.hpp"
#include "singletons/CrashHandler.hpp"
#include "singletons/Paths.hpp"
@ -98,9 +98,9 @@ namespace {
installCustomPalette();
}
void showLastCrashDialog()
void showLastCrashDialog(const Args &args, const Paths &paths)
{
auto *dialog = new LastRunCrashDialog;
auto *dialog = new LastRunCrashDialog(args, paths);
// Use exec() over open() to block the app from being loaded
// and to be able to set the safe mode.
dialog->exec();
@ -223,16 +223,17 @@ namespace {
}
} // namespace
void runGui(QApplication &a, Paths &paths, Settings &settings)
void runGui(QApplication &a, const Paths &paths, Settings &settings,
const Args &args, Updates &updates)
{
initQt();
initResources();
initSignalHandler();
#ifdef Q_OS_WIN
if (getArgs().crashRecovery)
if (args.crashRecovery)
{
showLastCrashDialog();
showLastCrashDialog(args, paths);
}
#endif
@ -269,14 +270,14 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
});
chatterino::NetworkManager::init();
chatterino::Updates::instance().checkForUpdates();
updates.checkForUpdates();
Application app(settings, paths);
Application app(settings, paths, args, updates);
app.initialize(settings, paths);
app.run(a);
app.save();
if (!getArgs().dontSaveSettings)
if (!args.dontSaveSettings)
{
pajlada::Settings::SettingManager::gSave();
}
@ -288,6 +289,8 @@ void runGui(QApplication &a, Paths &paths, Settings &settings)
flushClipboard();
#endif
app.fakeDtor();
_exit(0);
}
} // namespace chatterino

View file

@ -3,8 +3,13 @@
class QApplication;
namespace chatterino {
class Args;
class Paths;
class Settings;
class Updates;
void runGui(QApplication &a, const Paths &paths, Settings &settings,
const Args &args, Updates &updates);
void runGui(QApplication &a, Paths &paths, Settings &settings);
} // namespace chatterino

View file

@ -1,4 +1,4 @@
#include "Args.hpp"
#include "common/Args.hpp"
#include "common/QLogging.hpp"
#include "debug/AssertInGuiThread.hpp"
@ -66,7 +66,7 @@ QStringList extractCommandLine(
namespace chatterino {
Args::Args(const QApplication &app)
Args::Args(const QApplication &app, const Paths &paths)
{
QCommandLineParser parser;
parser.setApplicationDescription("Chatterino 2 Client for Twitch Chat");
@ -132,7 +132,7 @@ Args::Args(const QApplication &app)
if (parser.isSet(channelLayout))
{
this->applyCustomChannelLayout(parser.value(channelLayout));
this->applyCustomChannelLayout(parser.value(channelLayout), paths);
}
this->verbose = parser.isSet(verboseOption);
@ -175,7 +175,7 @@ QStringList Args::currentArguments() const
return this->currentArguments_;
}
void Args::applyCustomChannelLayout(const QString &argValue)
void Args::applyCustomChannelLayout(const QString &argValue, const Paths &paths)
{
WindowLayout layout;
WindowDescriptor window;
@ -187,10 +187,9 @@ void Args::applyCustomChannelLayout(const QString &argValue)
window.type_ = WindowType::Main;
// Load main window layout from config file so we can use the same geometry
const QRect configMainLayout = [] {
const QString windowLayoutFile =
combinePath(getPaths()->settingsDirectory,
WindowManager::WINDOW_LAYOUT_FILENAME);
const QRect configMainLayout = [paths] {
const QString windowLayoutFile = combinePath(
paths.settingsDirectory, WindowManager::WINDOW_LAYOUT_FILENAME);
const WindowLayout configLayout =
WindowLayout::loadFromFile(windowLayoutFile);
@ -198,7 +197,9 @@ void Args::applyCustomChannelLayout(const QString &argValue)
for (const WindowDescriptor &window : configLayout.windows_)
{
if (window.type_ != WindowType::Main)
{
continue;
}
return window.geometry_;
}
@ -212,7 +213,9 @@ void Args::applyCustomChannelLayout(const QString &argValue)
for (const QString &channelArg : channelArgList)
{
if (channelArg.isEmpty())
{
continue;
}
// Twitch is default platform
QString platform = "t";
@ -248,18 +251,4 @@ void Args::applyCustomChannelLayout(const QString &argValue)
}
}
static Args *instance = nullptr;
void initArgs(const QApplication &app)
{
instance = new Args(app);
}
const Args &getArgs()
{
assert(instance);
return *instance;
}
} // namespace chatterino

View file

@ -8,6 +8,8 @@
namespace chatterino {
class Paths;
/// Command line arguments passed to Chatterino.
///
/// All accepted arguments:
@ -30,7 +32,8 @@ namespace chatterino {
class Args
{
public:
Args(const QApplication &app);
Args() = default;
Args(const QApplication &app, const Paths &paths);
bool printVersion{};
@ -55,12 +58,9 @@ public:
QStringList currentArguments() const;
private:
void applyCustomChannelLayout(const QString &argValue);
void applyCustomChannelLayout(const QString &argValue, const Paths &paths);
QStringList currentArguments_;
};
void initArgs(const QApplication &app);
const Args &getArgs();
} // namespace chatterino

View file

@ -82,7 +82,7 @@ LimitedQueueSnapshot<MessagePtr> Channel::getMessageSnapshot()
void Channel::addMessage(MessagePtr message,
std::optional<MessageFlags> overridingFlags)
{
auto app = getApp();
auto *app = getApp();
MessagePtr deleted;
if (!overridingFlags || !overridingFlags->has(MessageFlag::DoNotLog))
@ -101,7 +101,8 @@ void Channel::addMessage(MessagePtr message,
{
channelPlatform = "twitch";
}
app->logging->addMessage(this->name_, message, channelPlatform);
getIApp()->getChatLogger()->addMessage(this->name_, message,
channelPlatform);
}
if (this->messages_.pushBack(message, deleted))
@ -134,7 +135,7 @@ void Channel::disableAllMessages()
int snapshotLength = snapshot.size();
for (int i = 0; i < snapshotLength; i++)
{
auto &message = snapshot[i];
const auto &message = snapshot[i];
if (message->flags.hasAny({MessageFlag::System, MessageFlag::Timeout,
MessageFlag::Whisper}))
{
@ -178,7 +179,7 @@ void Channel::fillInMissingMessages(const std::vector<MessagePtr> &messages)
existingMessageIds.reserve(snapshot.size());
// First, collect the ids of every message already present in the channel
for (auto &msg : snapshot)
for (const auto &msg : snapshot)
{
if (msg->flags.has(MessageFlag::System) || msg->id.isEmpty())
{
@ -195,7 +196,7 @@ void Channel::fillInMissingMessages(const std::vector<MessagePtr> &messages)
// being able to insert just-loaded historical messages at the end
// in the correct place.
auto lastMsg = snapshot[snapshot.size() - 1];
for (auto &msg : messages)
for (const auto &msg : messages)
{
// check if message already exists
if (existingMessageIds.count(msg->id) != 0)
@ -207,7 +208,7 @@ void Channel::fillInMissingMessages(const std::vector<MessagePtr> &messages)
anyInserted = true;
bool insertedFlag = false;
for (auto &snapshotMsg : snapshot)
for (const auto &snapshotMsg : snapshot)
{
if (snapshotMsg->flags.has(MessageFlag::System))
{
@ -315,7 +316,6 @@ bool Channel::isBroadcaster() const
bool Channel::hasModRights() const
{
// fourtf: check if staff
return this->isMod() || this->isBroadcaster();
}

View file

@ -27,11 +27,15 @@ void ChatterSet::updateOnlineChatters(
for (auto &&chatter : lowerCaseUsernames)
{
if (this->items.exists(chatter))
{
tmp.put(chatter, this->items.get(chatter));
// Less chatters than the limit => try to preserve as many as possible.
// Less chatters than the limit => try to preserve as many as possible.
}
else if (lowerCaseUsernames.size() < chatterLimit)
{
tmp.put(chatter, chatter);
}
}
this->items = std::move(tmp);
@ -50,7 +54,9 @@ std::vector<QString> ChatterSet::filterByPrefix(const QString &prefix) const
for (auto &&item : this->items)
{
if (item.first.startsWith(lowerPrefix))
{
result.push_back(item.second);
}
}
return result;

View file

@ -1,5 +1,6 @@
#include "common/Credentials.hpp"
#include "Application.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
@ -40,7 +41,7 @@ bool useKeyring()
#ifdef NO_QTKEYCHAIN
return false;
#endif
if (getPaths()->isPortable())
if (getIApp()->getPaths().isPortable())
{
return false;
}
@ -55,7 +56,8 @@ bool useKeyring()
// Insecure storage:
QString insecurePath()
{
return combinePath(getPaths()->settingsDirectory, "credentials.json");
return combinePath(getIApp()->getPaths().settingsDirectory,
"credentials.json");
}
QJsonDocument loadInsecure()

View file

@ -56,9 +56,13 @@ public:
void set(T flag, bool value)
{
if (value)
{
this->set(flag);
}
else
{
this->unset(flag);
}
}
bool has(T flag) const

View file

@ -1,413 +0,0 @@
#include "common/NetworkPrivate.hpp"
#include "common/NetworkManager.hpp"
#include "common/NetworkResult.hpp"
#include "common/QLogging.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "singletons/Paths.hpp"
#include "util/DebugCount.hpp"
#include "util/PostToThread.hpp"
#include <QCryptographicHash>
#include <QFile>
#include <QNetworkReply>
#include <QtConcurrent>
namespace chatterino {
NetworkData::NetworkData()
: lifetimeManager_(new QObject)
{
DebugCount::increase("NetworkData");
}
NetworkData::~NetworkData()
{
this->lifetimeManager_->deleteLater();
DebugCount::decrease("NetworkData");
}
QString NetworkData::getHash()
{
static std::mutex mu;
std::lock_guard lock(mu);
if (this->hash_.isEmpty())
{
QByteArray bytes;
bytes.append(this->request_.url().toString().toUtf8());
for (const auto &header : this->request_.rawHeaderList())
{
bytes.append(header);
}
QByteArray hashBytes(
QCryptographicHash::hash(bytes, QCryptographicHash::Sha256));
this->hash_ = hashBytes.toHex();
}
return this->hash_;
}
void writeToCache(const std::shared_ptr<NetworkData> &data,
const QByteArray &bytes)
{
if (data->cache_)
{
QtConcurrent::run([data, bytes] {
QFile cachedFile(getPaths()->cacheDirectory() + "/" +
data->getHash());
if (cachedFile.open(QIODevice::WriteOnly))
{
cachedFile.write(bytes);
}
});
}
}
void loadUncached(std::shared_ptr<NetworkData> &&data)
{
DebugCount::increase("http request started");
NetworkRequester requester;
auto *worker = new NetworkWorker;
worker->moveToThread(&NetworkManager::workerThread);
auto onUrlRequested = [data, worker]() mutable {
if (data->hasTimeout_)
{
data->timer_ = new QTimer();
data->timer_->setSingleShot(true);
data->timer_->start(data->timeoutMS_);
}
auto *reply = [&]() -> QNetworkReply * {
switch (data->requestType_)
{
case NetworkRequestType::Get:
return NetworkManager::accessManager.get(data->request_);
case NetworkRequestType::Put:
return NetworkManager::accessManager.put(data->request_,
data->payload_);
case NetworkRequestType::Delete:
return NetworkManager::accessManager.deleteResource(
data->request_);
case NetworkRequestType::Post:
if (data->multiPartPayload_)
{
assert(data->payload_.isNull());
return NetworkManager::accessManager.post(
data->request_, data->multiPartPayload_);
}
else
{
return NetworkManager::accessManager.post(
data->request_, data->payload_);
}
case NetworkRequestType::Patch:
if (data->multiPartPayload_)
{
assert(data->payload_.isNull());
return NetworkManager::accessManager.sendCustomRequest(
data->request_, "PATCH", data->multiPartPayload_);
}
else
{
return NetworkManager::accessManager.sendCustomRequest(
data->request_, "PATCH", data->payload_);
}
}
return nullptr;
}();
if (reply == nullptr)
{
qCDebug(chatterinoCommon) << "Unhandled request type";
return;
}
if (data->timer_ != nullptr && data->timer_->isActive())
{
QObject::connect(
data->timer_, &QTimer::timeout, worker, [reply, data]() {
qCDebug(chatterinoCommon) << "Aborted!";
reply->abort();
qCDebug(chatterinoHTTP)
<< QString("%1 [timed out] %2")
.arg(networkRequestTypes.at(
int(data->requestType_)),
data->request_.url().toString());
if (data->onError_)
{
postToThread([data] {
data->onError_(NetworkResult(
NetworkResult::NetworkError::TimeoutError, {},
{}));
});
}
if (data->finally_)
{
postToThread([data] {
data->finally_();
});
}
});
}
if (data->onReplyCreated_)
{
data->onReplyCreated_(reply);
}
auto handleReply = [data, reply]() mutable {
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
// TODO(pajlada): A reply was received, kill the timeout timer
if (reply->error() != QNetworkReply::NetworkError::NoError)
{
if (reply->error() ==
QNetworkReply::NetworkError::OperationCanceledError)
{
// Operation cancelled, most likely timed out
qCDebug(chatterinoHTTP)
<< QString("%1 [cancelled] %2")
.arg(networkRequestTypes.at(
int(data->requestType_)),
data->request_.url().toString());
return;
}
if (data->onError_)
{
auto status = reply->attribute(
QNetworkRequest::HttpStatusCodeAttribute);
if (data->requestType_ == NetworkRequestType::Get)
{
qCDebug(chatterinoHTTP)
<< QString("%1 %2 %3")
.arg(networkRequestTypes.at(
int(data->requestType_)),
QString::number(status.toInt()),
data->request_.url().toString());
}
else
{
qCDebug(chatterinoHTTP)
<< QString("%1 %2 %3 %4")
.arg(networkRequestTypes.at(
int(data->requestType_)),
QString::number(status.toInt()),
data->request_.url().toString(),
QString(data->payload_));
}
// TODO: Should this always be run on the GUI thread?
postToThread([data, status, reply] {
data->onError_(NetworkResult(reply->error(), status,
reply->readAll()));
});
}
if (data->finally_)
{
postToThread([data] {
data->finally_();
});
}
return;
}
QByteArray bytes = reply->readAll();
writeToCache(data, bytes);
auto status =
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
NetworkResult result(reply->error(), status, bytes);
DebugCount::increase("http request success");
// log("starting {}", data->request_.url().toString());
if (data->onSuccess_)
{
if (data->executeConcurrently_)
{
QtConcurrent::run([onSuccess = std::move(data->onSuccess_),
result = std::move(result)] {
onSuccess(result);
});
}
else
{
data->onSuccess_(result);
}
}
// log("finished {}", data->request_.url().toString());
reply->deleteLater();
if (data->requestType_ == NetworkRequestType::Get)
{
qCDebug(chatterinoHTTP)
<< QString("%1 %2 %3")
.arg(networkRequestTypes.at(int(data->requestType_)),
QString::number(status.toInt()),
data->request_.url().toString());
}
else
{
qCDebug(chatterinoHTTP)
<< QString("%1 %3 %2 %4")
.arg(networkRequestTypes.at(int(data->requestType_)),
data->request_.url().toString(),
QString::number(status.toInt()),
QString(data->payload_));
}
if (data->finally_)
{
if (data->executeConcurrently_)
{
QtConcurrent::run([finally = std::move(data->finally_)] {
finally();
});
}
else
{
data->finally_();
}
}
};
if (data->timer_ != nullptr)
{
QObject::connect(reply, &QNetworkReply::finished, data->timer_,
&QObject::deleteLater);
}
QObject::connect(
reply, &QNetworkReply::finished, worker,
[data, handleReply, worker]() mutable {
if (data->executeConcurrently_ || isGuiThread())
{
handleReply();
delete worker;
}
else
{
postToThread(
[worker, cb = std::move(handleReply)]() mutable {
cb();
delete worker;
});
}
});
};
QObject::connect(&requester, &NetworkRequester::requestUrl, worker,
onUrlRequested);
emit requester.requestUrl();
}
// First tried to load cached, then uncached.
void loadCached(std::shared_ptr<NetworkData> &&data)
{
QFile cachedFile(getPaths()->cacheDirectory() + "/" + data->getHash());
if (!cachedFile.exists() || !cachedFile.open(QIODevice::ReadOnly))
{
// File didn't exist OR File could not be opened
loadUncached(std::move(data));
return;
}
// XXX: check if bytes is empty?
QByteArray bytes = cachedFile.readAll();
NetworkResult result(NetworkResult::NetworkError::NoError, QVariant(200),
bytes);
qCDebug(chatterinoHTTP)
<< QString("%1 [CACHED] 200 %2")
.arg(networkRequestTypes.at(int(data->requestType_)),
data->request_.url().toString());
if (data->onSuccess_)
{
if (data->executeConcurrently_ || isGuiThread())
{
// XXX: If outcome is Failure, we should invalidate the cache file
// somehow/somewhere
/*auto outcome =*/
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
data->onSuccess_(result);
}
else
{
postToThread([data, result]() {
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
data->onSuccess_(result);
});
}
}
if (data->finally_)
{
if (data->executeConcurrently_ || isGuiThread())
{
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
data->finally_();
}
else
{
postToThread([data]() {
if (data->hasCaller_ && data->caller_.isNull())
{
return;
}
data->finally_();
});
}
}
}
void load(std::shared_ptr<NetworkData> &&data)
{
if (data->cache_)
{
QtConcurrent::run([data = std::move(data)]() mutable {
loadCached(std::move(data));
});
}
else
{
loadUncached(std::move(data));
}
}
} // namespace chatterino

View file

@ -1,73 +0,0 @@
#pragma once
#include "common/NetworkCommon.hpp"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include <QPointer>
#include <QTimer>
#include <functional>
#include <memory>
class QNetworkReply;
namespace chatterino {
class NetworkResult;
class NetworkRequester : public QObject
{
Q_OBJECT
signals:
void requestUrl();
};
class NetworkWorker : public QObject
{
Q_OBJECT
signals:
void doneUrl();
};
struct NetworkData {
NetworkData();
~NetworkData();
QNetworkRequest request_;
bool hasCaller_{};
QPointer<QObject> caller_;
bool cache_{};
bool executeConcurrently_{};
NetworkReplyCreatedCallback onReplyCreated_;
NetworkErrorCallback onError_;
NetworkSuccessCallback onSuccess_;
NetworkFinallyCallback finally_;
NetworkRequestType requestType_ = NetworkRequestType::Get;
QByteArray payload_;
// lifetime secured by lifetimeManager_
QHttpMultiPart *multiPartPayload_{};
// Timer that tracks the timeout
// By default, there's no explicit timeout for the request
// to enable the timer, the "setTimeout" function needs to be called before
// execute is called
bool hasTimeout_{};
int timeoutMS_{};
QTimer *timer_ = nullptr;
QObject *lifetimeManager_;
QString getHash();
private:
QString hash_;
};
void load(std::shared_ptr<NetworkData> &&data);
} // namespace chatterino

View file

@ -1,73 +0,0 @@
#pragma once
#include <type_traits>
namespace chatterino {
template <typename T>
class NullablePtr
{
public:
NullablePtr()
: element_(nullptr)
{
}
NullablePtr(T *element)
: element_(element)
{
}
T *operator->() const
{
assert(this->hasElement());
return element_;
}
typename std::add_lvalue_reference<T>::type operator*() const
{
assert(this->hasElement());
return *element_;
}
T *get() const
{
assert(this->hasElement());
return this->element_;
}
bool isNull() const
{
return this->element_ == nullptr;
}
bool hasElement() const
{
return this->element_ != nullptr;
}
operator bool() const
{
return this->hasElement();
}
bool operator!() const
{
return !this->hasElement();
}
template <typename X = T,
typename = std::enable_if_t<!std::is_const<X>::value>>
operator NullablePtr<const T>() const
{
return NullablePtr<const T>(this->element_);
}
private:
T *element_;
};
} // namespace chatterino

View file

@ -308,10 +308,12 @@ public:
for (auto &&x : list)
{
if (x.row() != list.first().row())
{
return nullptr;
}
}
auto data = new QMimeData;
auto *data = new QMimeData;
data->setData("chatterino_row_id", QByteArray::number(list[0].row()));
return data;
}

View file

@ -17,7 +17,7 @@ public:
Singleton(Singleton &&) = delete;
Singleton &operator=(Singleton &&) = delete;
virtual void initialize(Settings &settings, Paths &paths)
virtual void initialize(Settings &settings, const Paths &paths)
{
(void)(settings);
(void)(paths);

View file

@ -1,4 +1,4 @@
#include "common/NetworkCommon.hpp"
#include "common/network/NetworkCommon.hpp"
#include <QStringList>

View file

@ -13,7 +13,6 @@ class NetworkResult;
using NetworkSuccessCallback = std::function<void(NetworkResult)>;
using NetworkErrorCallback = std::function<void(NetworkResult)>;
using NetworkReplyCreatedCallback = std::function<void(QNetworkReply *)>;
using NetworkFinallyCallback = std::function<void()>;
enum class NetworkRequestType {
@ -23,13 +22,6 @@ enum class NetworkRequestType {
Delete,
Patch,
};
const static std::vector<QString> networkRequestTypes{
"GET", //
"POST", //
"PUT", //
"DELETE", //
"PATCH", //
};
// parseHeaderList takes a list of headers in string form,
// where each header pair is separated by semicolons (;) and the header name and value is divided by a colon (:)

View file

@ -1,4 +1,4 @@
#include "common/NetworkManager.hpp"
#include "common/network/NetworkManager.hpp"
#include <QNetworkAccessManager>

View file

@ -0,0 +1,205 @@
#include "common/network/NetworkPrivate.hpp"
#include "Application.hpp"
#include "common/network/NetworkManager.hpp"
#include "common/network/NetworkResult.hpp"
#include "common/network/NetworkTask.hpp"
#include "common/QLogging.hpp"
#include "singletons/Paths.hpp"
#include "util/AbandonObject.hpp"
#include "util/DebugCount.hpp"
#include "util/PostToThread.hpp"
#include <magic_enum/magic_enum.hpp>
#include <QCryptographicHash>
#include <QElapsedTimer>
#include <QFile>
#include <QNetworkReply>
#include <QtConcurrent>
#ifdef NDEBUG
constexpr qsizetype SLOW_HTTP_THRESHOLD = 30;
#else
constexpr qsizetype SLOW_HTTP_THRESHOLD = 90;
#endif
using namespace chatterino::network::detail;
namespace {
using namespace chatterino;
void runCallback(bool concurrent, auto &&fn)
{
if (concurrent)
{
std::ignore = QtConcurrent::run(std::forward<decltype(fn)>(fn));
}
else
{
runInGuiThread(std::forward<decltype(fn)>(fn));
}
}
void loadUncached(std::shared_ptr<NetworkData> &&data)
{
DebugCount::increase("http request started");
NetworkRequester requester;
auto *worker = new NetworkTask(std::move(data));
worker->moveToThread(&NetworkManager::workerThread);
QObject::connect(&requester, &NetworkRequester::requestUrl, worker,
&NetworkTask::run);
emit requester.requestUrl();
}
void loadCached(std::shared_ptr<NetworkData> &&data)
{
QFile cachedFile(getIApp()->getPaths().cacheDirectory() + "/" +
data->getHash());
if (!cachedFile.exists() || !cachedFile.open(QIODevice::ReadOnly))
{
loadUncached(std::move(data));
return;
}
// XXX: check if bytes is empty?
QByteArray bytes = cachedFile.readAll();
qCDebug(chatterinoHTTP).noquote() << data->typeString() << "[CACHED] 200"
<< data->request.url().toString();
data->emitSuccess(
{NetworkResult::NetworkError::NoError, QVariant(200), bytes});
data->emitFinally();
}
} // namespace
namespace chatterino {
NetworkData::NetworkData()
{
DebugCount::increase("NetworkData");
}
NetworkData::~NetworkData()
{
DebugCount::decrease("NetworkData");
}
QString NetworkData::getHash()
{
if (this->hash_.isEmpty())
{
QByteArray bytes;
bytes.append(this->request.url().toString().toUtf8());
for (const auto &header : this->request.rawHeaderList())
{
bytes.append(header);
}
QByteArray hashBytes(
QCryptographicHash::hash(bytes, QCryptographicHash::Sha256));
this->hash_ = hashBytes.toHex();
}
return this->hash_;
}
void NetworkData::emitSuccess(NetworkResult &&result)
{
if (!this->onSuccess)
{
return;
}
runCallback(this->executeConcurrently,
[cb = std::move(this->onSuccess), result = std::move(result),
url = this->request.url(), hasCaller = this->hasCaller,
caller = this->caller]() {
if (hasCaller && caller.isNull())
{
return;
}
QElapsedTimer timer;
timer.start();
cb(result);
if (timer.elapsed() > SLOW_HTTP_THRESHOLD)
{
qCWarning(chatterinoHTTP)
<< "Slow HTTP success handler for" << url.toString()
<< timer.elapsed()
<< "ms (threshold:" << SLOW_HTTP_THRESHOLD << "ms)";
}
});
}
void NetworkData::emitError(NetworkResult &&result)
{
if (!this->onError)
{
return;
}
runCallback(this->executeConcurrently,
[cb = std::move(this->onError), result = std::move(result),
hasCaller = this->hasCaller, caller = this->caller]() {
if (hasCaller && caller.isNull())
{
return;
}
cb(result);
});
}
void NetworkData::emitFinally()
{
if (!this->finally)
{
return;
}
runCallback(this->executeConcurrently,
[cb = std::move(this->finally), hasCaller = this->hasCaller,
caller = this->caller]() {
if (hasCaller && caller.isNull())
{
return;
}
cb();
});
}
QLatin1String NetworkData::typeString() const
{
auto view = magic_enum::enum_name<NetworkRequestType>(this->requestType);
return QLatin1String{view.data(),
static_cast<QLatin1String::size_type>(view.size())};
}
void load(std::shared_ptr<NetworkData> &&data)
{
if (data->cache)
{
std::ignore = QtConcurrent::run([data = std::move(data)]() mutable {
loadCached(std::move(data));
});
}
else
{
loadUncached(std::move(data));
}
}
} // namespace chatterino

View file

@ -0,0 +1,71 @@
#pragma once
#include "common/Common.hpp"
#include "common/network/NetworkCommon.hpp"
#include <QHttpMultiPart>
#include <QNetworkRequest>
#include <QPointer>
#include <QTimer>
#include <memory>
#include <optional>
class QNetworkReply;
namespace chatterino {
class NetworkResult;
class NetworkRequester : public QObject
{
Q_OBJECT
signals:
void requestUrl();
};
class NetworkData
{
public:
NetworkData();
~NetworkData();
NetworkData(const NetworkData &) = delete;
NetworkData(NetworkData &&) = delete;
NetworkData &operator=(const NetworkData &) = delete;
NetworkData &operator=(NetworkData &&) = delete;
QNetworkRequest request;
bool hasCaller{};
QPointer<QObject> caller;
bool cache{};
bool executeConcurrently{};
NetworkSuccessCallback onSuccess;
NetworkErrorCallback onError;
NetworkFinallyCallback finally;
NetworkRequestType requestType = NetworkRequestType::Get;
QByteArray payload;
std::unique_ptr<QHttpMultiPart, DeleteLater> multiPartPayload;
/// By default, there's no explicit timeout for the request.
/// To set a timeout, use NetworkRequest's timeout method
std::optional<std::chrono::milliseconds> timeout{};
QString getHash();
void emitSuccess(NetworkResult &&result);
void emitError(NetworkResult &&result);
void emitFinally();
QLatin1String typeString() const;
private:
QString hash_;
};
void load(std::shared_ptr<NetworkData> &&data);
} // namespace chatterino

View file

@ -1,6 +1,6 @@
#include "common/NetworkRequest.hpp"
#include "common/network/NetworkRequest.hpp"
#include "common/NetworkPrivate.hpp"
#include "common/network/NetworkPrivate.hpp"
#include "common/QLogging.hpp"
#include "common/Version.hpp"
@ -16,8 +16,8 @@ NetworkRequest::NetworkRequest(const std::string &url,
NetworkRequestType requestType)
: data(new NetworkData)
{
this->data->request_.setUrl(QUrl(QString::fromStdString(url)));
this->data->requestType_ = requestType;
this->data->request.setUrl(QUrl(QString::fromStdString(url)));
this->data->requestType = requestType;
this->initializeDefaultValues();
}
@ -25,8 +25,8 @@ NetworkRequest::NetworkRequest(const std::string &url,
NetworkRequest::NetworkRequest(const QUrl &url, NetworkRequestType requestType)
: data(new NetworkData)
{
this->data->request_.setUrl(url);
this->data->requestType_ = requestType;
this->data->request.setUrl(url);
this->data->requestType = requestType;
this->initializeDefaultValues();
}
@ -35,7 +35,7 @@ NetworkRequest::~NetworkRequest() = default;
NetworkRequest NetworkRequest::type(NetworkRequestType newRequestType) &&
{
this->data->requestType_ = newRequestType;
this->data->requestType = newRequestType;
return std::move(*this);
}
@ -46,61 +46,55 @@ NetworkRequest NetworkRequest::caller(const QObject *caller) &&
// Caller must be in gui thread
assert(caller->thread() == qApp->thread());
this->data->caller_ = const_cast<QObject *>(caller);
this->data->hasCaller_ = true;
this->data->caller = const_cast<QObject *>(caller);
this->data->hasCaller = true;
}
return std::move(*this);
}
NetworkRequest NetworkRequest::onReplyCreated(NetworkReplyCreatedCallback cb) &&
{
this->data->onReplyCreated_ = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::onError(NetworkErrorCallback cb) &&
{
this->data->onError_ = std::move(cb);
this->data->onError = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::onSuccess(NetworkSuccessCallback cb) &&
{
this->data->onSuccess_ = std::move(cb);
this->data->onSuccess = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::finally(NetworkFinallyCallback cb) &&
{
this->data->finally_ = std::move(cb);
this->data->finally = std::move(cb);
return std::move(*this);
}
NetworkRequest NetworkRequest::header(const char *headerName,
const char *value) &&
{
this->data->request_.setRawHeader(headerName, value);
this->data->request.setRawHeader(headerName, value);
return std::move(*this);
}
NetworkRequest NetworkRequest::header(const char *headerName,
const QByteArray &value) &&
{
this->data->request_.setRawHeader(headerName, value);
this->data->request.setRawHeader(headerName, value);
return std::move(*this);
}
NetworkRequest NetworkRequest::header(const char *headerName,
const QString &value) &&
{
this->data->request_.setRawHeader(headerName, value.toUtf8());
this->data->request.setRawHeader(headerName, value.toUtf8());
return std::move(*this);
}
NetworkRequest NetworkRequest::header(QNetworkRequest::KnownHeaders header,
const QVariant &value) &&
{
this->data->request_.setHeader(header, value);
this->data->request.setHeader(header, value);
return std::move(*this);
}
@ -109,28 +103,26 @@ NetworkRequest NetworkRequest::headerList(
{
for (const auto &[headerName, headerValue] : headers)
{
this->data->request_.setRawHeader(headerName, headerValue);
this->data->request.setRawHeader(headerName, headerValue);
}
return std::move(*this);
}
NetworkRequest NetworkRequest::timeout(int ms) &&
{
this->data->hasTimeout_ = true;
this->data->timeoutMS_ = ms;
this->data->timeout = std::chrono::milliseconds(ms);
return std::move(*this);
}
NetworkRequest NetworkRequest::concurrent() &&
{
this->data->executeConcurrently_ = true;
this->data->executeConcurrently = true;
return std::move(*this);
}
NetworkRequest NetworkRequest::multiPart(QHttpMultiPart *payload) &&
{
payload->setParent(this->data->lifetimeManager_);
this->data->multiPartPayload_ = payload;
this->data->multiPartPayload = {payload, {}};
return std::move(*this);
}
@ -138,13 +130,13 @@ NetworkRequest NetworkRequest::followRedirects(bool on) &&
{
if (on)
{
this->data->request_.setAttribute(
this->data->request.setAttribute(
QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
}
else
{
this->data->request_.setAttribute(
this->data->request.setAttribute(
QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::ManualRedirectPolicy);
}
@ -154,13 +146,13 @@ NetworkRequest NetworkRequest::followRedirects(bool on) &&
NetworkRequest NetworkRequest::payload(const QByteArray &payload) &&
{
this->data->payload_ = payload;
this->data->payload = payload;
return std::move(*this);
}
NetworkRequest NetworkRequest::cache() &&
{
this->data->cache_ = true;
this->data->cache = true;
return std::move(*this);
}
@ -169,15 +161,14 @@ void NetworkRequest::execute()
this->executed_ = true;
// Only allow caching for GET request
if (this->data->cache_ &&
this->data->requestType_ != NetworkRequestType::Get)
if (this->data->cache && this->data->requestType != NetworkRequestType::Get)
{
qCDebug(chatterinoCommon) << "Can only cache GET requests!";
this->data->cache_ = false;
this->data->cache = false;
}
// Can not have a caller and be concurrent at the same time.
assert(!(this->data->caller_ && this->data->executeConcurrently_));
assert(!(this->data->caller && this->data->executeConcurrently));
load(std::move(this->data));
}
@ -189,7 +180,7 @@ void NetworkRequest::initializeDefaultValues()
Version::instance().commitHash())
.toUtf8();
this->data->request_.setRawHeader("User-Agent", userAgent);
this->data->request.setRawHeader("User-Agent", userAgent);
}
NetworkRequest NetworkRequest::json(const QJsonArray &root) &&

View file

@ -1,6 +1,6 @@
#pragma once
#include "common/NetworkCommon.hpp"
#include "common/network/NetworkCommon.hpp"
#include <QHttpMultiPart>
@ -12,7 +12,7 @@ class QJsonDocument;
namespace chatterino {
struct NetworkData;
class NetworkData;
class NetworkRequest final
{
@ -43,7 +43,6 @@ public:
NetworkRequest type(NetworkRequestType newRequestType) &&;
NetworkRequest onReplyCreated(NetworkReplyCreatedCallback cb) &&;
NetworkRequest onError(NetworkErrorCallback cb) &&;
NetworkRequest onSuccess(NetworkSuccessCallback cb) &&;
NetworkRequest finally(NetworkFinallyCallback cb) &&;

View file

@ -1,4 +1,4 @@
#include "common/NetworkResult.hpp"
#include "common/network/NetworkResult.hpp"
#include "common/QLogging.hpp"

View file

@ -0,0 +1,191 @@
#include "common/network/NetworkTask.hpp"
#include "Application.hpp"
#include "common/network/NetworkManager.hpp"
#include "common/network/NetworkPrivate.hpp"
#include "common/network/NetworkResult.hpp"
#include "common/QLogging.hpp"
#include "singletons/Paths.hpp"
#include "util/AbandonObject.hpp"
#include "util/DebugCount.hpp"
#include <QFile>
#include <QNetworkReply>
#include <QtConcurrent>
namespace chatterino::network::detail {
NetworkTask::NetworkTask(std::shared_ptr<NetworkData> &&data)
: data_(std::move(data))
{
}
NetworkTask::~NetworkTask()
{
if (this->reply_)
{
this->reply_->deleteLater();
}
}
void NetworkTask::run()
{
const auto &timeout = this->data_->timeout;
if (timeout.has_value())
{
this->timer_ = new QTimer(this);
this->timer_->setSingleShot(true);
this->timer_->start(timeout.value());
QObject::connect(this->timer_, &QTimer::timeout, this,
&NetworkTask::timeout);
}
this->reply_ = this->createReply();
if (!this->reply_)
{
this->deleteLater();
return;
}
QObject::connect(this->reply_, &QNetworkReply::finished, this,
&NetworkTask::finished);
}
QNetworkReply *NetworkTask::createReply()
{
const auto &data = this->data_;
const auto &request = this->data_->request;
auto &accessManager = NetworkManager::accessManager;
switch (this->data_->requestType)
{
case NetworkRequestType::Get:
return accessManager.get(request);
case NetworkRequestType::Put:
return accessManager.put(request, data->payload);
case NetworkRequestType::Delete:
return accessManager.deleteResource(data->request);
case NetworkRequestType::Post:
if (data->multiPartPayload)
{
assert(data->payload.isNull());
return accessManager.post(request,
data->multiPartPayload.get());
}
else
{
return accessManager.post(request, data->payload);
}
case NetworkRequestType::Patch:
if (data->multiPartPayload)
{
assert(data->payload.isNull());
return accessManager.sendCustomRequest(
request, "PATCH", data->multiPartPayload.get());
}
else
{
return NetworkManager::accessManager.sendCustomRequest(
request, "PATCH", data->payload);
}
}
return nullptr;
}
void NetworkTask::logReply()
{
auto status =
this->reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute)
.toInt();
if (this->data_->requestType == NetworkRequestType::Get)
{
qCDebug(chatterinoHTTP).noquote()
<< this->data_->typeString() << status
<< this->data_->request.url().toString();
}
else
{
qCDebug(chatterinoHTTP).noquote()
<< this->data_->typeString()
<< this->data_->request.url().toString() << status
<< QString(this->data_->payload);
}
}
void NetworkTask::writeToCache(const QByteArray &bytes) const
{
std::ignore = QtConcurrent::run([data = this->data_, bytes] {
QFile cachedFile(getIApp()->getPaths().cacheDirectory() + "/" +
data->getHash());
if (cachedFile.open(QIODevice::WriteOnly))
{
cachedFile.write(bytes);
}
});
}
void NetworkTask::timeout()
{
AbandonObject guard(this);
// prevent abort() from calling finished()
QObject::disconnect(this->reply_, &QNetworkReply::finished, this,
&NetworkTask::finished);
this->reply_->abort();
qCDebug(chatterinoHTTP).noquote()
<< this->data_->typeString() << "[timed out]"
<< this->data_->request.url().toString();
this->data_->emitError({NetworkResult::NetworkError::TimeoutError, {}, {}});
this->data_->emitFinally();
}
void NetworkTask::finished()
{
AbandonObject guard(this);
if (this->timer_)
{
this->timer_->stop();
}
auto *reply = this->reply_;
auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
if (reply->error() == QNetworkReply::OperationCanceledError)
{
// Operation cancelled, most likely timed out
qCDebug(chatterinoHTTP).noquote()
<< this->data_->typeString() << "[cancelled]"
<< this->data_->request.url().toString();
return;
}
if (reply->error() != QNetworkReply::NoError)
{
this->logReply();
this->data_->emitError({reply->error(), status, reply->readAll()});
this->data_->emitFinally();
return;
}
QByteArray bytes = reply->readAll();
if (this->data_->cache)
{
this->writeToCache(bytes);
}
DebugCount::increase("http request success");
this->logReply();
this->data_->emitSuccess({reply->error(), status, bytes});
this->data_->emitFinally();
}
} // namespace chatterino::network::detail

View file

@ -0,0 +1,51 @@
#pragma once
#include <QObject>
#include <QTimer>
#include <memory>
class QNetworkReply;
namespace chatterino {
class NetworkData;
} // namespace chatterino
namespace chatterino::network::detail {
class NetworkTask : public QObject
{
Q_OBJECT
public:
NetworkTask(std::shared_ptr<NetworkData> &&data);
~NetworkTask() override;
NetworkTask(const NetworkTask &) = delete;
NetworkTask(NetworkTask &&) = delete;
NetworkTask &operator=(const NetworkTask &) = delete;
NetworkTask &operator=(NetworkTask &&) = delete;
// NOLINTNEXTLINE(readability-redundant-access-specifiers)
public slots:
void run();
private:
QNetworkReply *createReply();
void logReply();
void writeToCache(const QByteArray &bytes) const;
std::shared_ptr<NetworkData> data_;
QNetworkReply *reply_{}; // parent: default (accessManager)
QTimer *timer_{}; // parent: this
// NOLINTNEXTLINE(readability-redundant-access-specifiers)
private slots:
void timeout();
void finished();
};
} // namespace chatterino::network::detail

View file

@ -22,7 +22,7 @@ AccountController::AccountController()
this->twitch.accounts.itemRemoved.connect([this](const auto &args) {
if (args.caller != this)
{
auto &accs = this->twitch.accounts.raw();
const auto &accs = this->twitch.accounts.raw();
auto it = std::find(accs.begin(), accs.end(), args.item);
assert(it != accs.end());
@ -47,7 +47,7 @@ AccountController::AccountController()
});
}
void AccountController::initialize(Settings &settings, Paths &paths)
void AccountController::initialize(Settings &settings, const Paths &paths)
{
this->twitch.load();
}

View file

@ -21,7 +21,7 @@ public:
AccountModel *createModel(QObject *parent);
void initialize(Settings &settings, Paths &paths) override;
void initialize(Settings &settings, const Paths &paths) override;
TwitchAccountManager twitch;

View file

@ -261,7 +261,7 @@ const std::unordered_map<QString, VariableReplacer> COMMAND_VARS{
namespace chatterino {
void CommandController::initialize(Settings &, Paths &paths)
void CommandController::initialize(Settings &, const Paths &paths)
{
// Update commands map when the vector of commands has been updated
auto addFirstMatchToMap = [this](auto args) {

View file

@ -33,7 +33,7 @@ public:
bool dryRun);
QStringList getDefaultChatterinoCommandList();
void initialize(Settings &, Paths &paths) override;
void initialize(Settings &, const Paths &paths) override;
void save() override;
CommandModel *createModel(QObject *parent);

View file

@ -66,7 +66,7 @@ QString uptime(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /uptime command only works in Twitch Channels"));
"The /uptime command only works in Twitch Channels."));
return "";
}
@ -188,14 +188,14 @@ QString clip(const CommandContext &ctx)
type != Channel::Type::Twitch && type != Channel::Type::TwitchWatching)
{
ctx.channel->addMessage(makeSystemMessage(
"The /clip command only works in Twitch Channels"));
"The /clip command only works in Twitch Channels."));
return "";
}
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /clip command only works in Twitch Channels"));
"The /clip command only works in Twitch Channels."));
return "";
}
@ -214,7 +214,7 @@ QString marker(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /marker command only works in Twitch channels"));
"The /marker command only works in Twitch channels."));
return "";
}
@ -520,7 +520,7 @@ QString unstableSetUserClientSideColor(const CommandContext &ctx)
{
ctx.channel->addMessage(
makeSystemMessage("The /unstable-set-user-color command only "
"works in Twitch channels"));
"works in Twitch channels."));
return "";
}
if (ctx.words.size() < 2)

View file

@ -23,7 +23,7 @@ QString addModerator(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /mod command only works in Twitch channels"));
"The /mod command only works in Twitch channels."));
return "";
}
if (ctx.words.size() < 2)

View file

@ -23,7 +23,7 @@ QString addVIP(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /vip command only works in Twitch channels"));
"The /vip command only works in Twitch channels."));
return "";
}
if (ctx.words.size() < 2)

View file

@ -37,7 +37,7 @@ QString sendAnnouncement(const CommandContext &ctx)
if (user->isAnon())
{
ctx.channel->addMessage(makeSystemMessage(
"You must be logged in to use the /announce command"));
"You must be logged in to use the /announce command."));
return "";
}

View file

@ -133,7 +133,7 @@ QString sendBan(const CommandContext &ctx)
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
QString("The /ban command only works in Twitch channels")));
QString("The /ban command only works in Twitch channels.")));
return "";
}
@ -196,7 +196,7 @@ QString sendBanById(const CommandContext &ctx)
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
QString("The /banid command only works in Twitch channels")));
QString("The /banid command only works in Twitch channels.")));
return "";
}
@ -241,7 +241,7 @@ QString sendTimeout(const CommandContext &ctx)
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
QString("The /timeout command only works in Twitch channels")));
QString("The /timeout command only works in Twitch channels.")));
return "";
}
const auto *usageStr =

View file

@ -27,7 +27,7 @@ QString blockUser(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /block command only works in Twitch channels"));
"The /block command only works in Twitch channels."));
return "";
}
@ -101,7 +101,7 @@ QString unblockUser(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /unblock command only works in Twitch channels"));
"The /unblock command only works in Twitch channels."));
return "";
}

View file

@ -111,7 +111,7 @@ QString emoteOnly(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /emoteonly command only works in Twitch channels"));
"The /emoteonly command only works in Twitch channels."));
return "";
}
@ -140,7 +140,7 @@ QString emoteOnlyOff(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /emoteonlyoff command only works in Twitch channels"));
"The /emoteonlyoff command only works in Twitch channels."));
return "";
}
@ -170,7 +170,7 @@ QString subscribers(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /subscribers command only works in Twitch channels"));
"The /subscribers command only works in Twitch channels."));
return "";
}
@ -200,7 +200,7 @@ QString subscribersOff(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /subscribersoff command only works in Twitch channels"));
"The /subscribersoff command only works in Twitch channels."));
return "";
}
@ -230,7 +230,7 @@ QString slow(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /slow command only works in Twitch channels"));
"The /slow command only works in Twitch channels."));
return "";
}
@ -277,7 +277,7 @@ QString slowOff(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /slowoff command only works in Twitch channels"));
"The /slowoff command only works in Twitch channels."));
return "";
}
@ -307,7 +307,7 @@ QString followers(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /followers command only works in Twitch channels"));
"The /followers command only works in Twitch channels."));
return "";
}
@ -355,7 +355,7 @@ QString followersOff(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /followersoff command only works in Twitch channels"));
"The /followersoff command only works in Twitch channels."));
return "";
}
@ -385,7 +385,7 @@ QString uniqueChat(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /uniquechat command only works in Twitch channels"));
"The /uniquechat command only works in Twitch channels."));
return "";
}
@ -415,7 +415,7 @@ QString uniqueChatOff(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /uniquechatoff command only works in Twitch channels"));
"The /uniquechatoff command only works in Twitch channels."));
return "";
}

View file

@ -70,7 +70,7 @@ QString chatters(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /chatters command only works in Twitch Channels"));
"The /chatters command only works in Twitch Channels."));
return "";
}
@ -80,7 +80,7 @@ QString chatters(const CommandContext &ctx)
getApp()->accounts->twitch.getCurrent()->getUserId(), 1,
[channel{ctx.channel}](auto result) {
channel->addMessage(
makeSystemMessage(QString("Chatter count: %1")
makeSystemMessage(QString("Chatter count: %1.")
.arg(localizeNumbers(result.total))));
},
[channel{ctx.channel}](auto error, auto message) {
@ -101,7 +101,7 @@ QString testChatters(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /test-chatters command only works in Twitch Channels"));
"The /test-chatters command only works in Twitch Channels."));
return "";
}

View file

@ -2,7 +2,7 @@
#include "Application.hpp"
#include "common/Channel.hpp"
#include "common/NetworkResult.hpp"
#include "common/network/NetworkResult.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/commands/CommandContext.hpp"
#include "messages/Message.hpp"
@ -102,7 +102,7 @@ QString deleteAllMessages(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /clear command only works in Twitch channels"));
"The /clear command only works in Twitch channels."));
return "";
}
@ -121,7 +121,7 @@ QString deleteOneMessage(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /delete command only works in Twitch channels"));
"The /delete command only works in Twitch channels."));
return "";
}

View file

@ -61,7 +61,7 @@ QString getModerators(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /mods command only works in Twitch Channels"));
"The /mods command only works in Twitch Channels."));
return "";
}

View file

@ -78,7 +78,7 @@ QString getVIPs(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /vips command only works in Twitch channels"));
"The /vips command only works in Twitch channels."));
return "";
}

View file

@ -125,7 +125,7 @@ QString startRaid(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /raid command only works in Twitch channels"));
"The /raid command only works in Twitch channels."));
return "";
}
@ -183,7 +183,7 @@ QString cancelRaid(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /unraid command only works in Twitch channels"));
"The /unraid command only works in Twitch channels."));
return "";
}

View file

@ -23,7 +23,7 @@ QString removeModerator(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /unmod command only works in Twitch channels"));
"The /unmod command only works in Twitch channels."));
return "";
}
if (ctx.words.size() < 2)

View file

@ -23,7 +23,7 @@ QString removeVIP(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /unvip command only works in Twitch channels"));
"The /unvip command only works in Twitch channels."));
return "";
}
if (ctx.words.size() < 2)

View file

@ -20,7 +20,7 @@ QString sendReply(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /reply command only works in Twitch channels"));
"The /reply command only works in Twitch channels."));
return "";
}
@ -55,7 +55,7 @@ QString sendReply(const CommandContext &ctx)
}
ctx.channel->addMessage(
makeSystemMessage("A message from that user wasn't found"));
makeSystemMessage("A message from that user wasn't found."));
return "";
}

View file

@ -18,7 +18,7 @@ QString toggleShieldMode(const CommandContext &ctx, bool isActivating)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
QStringLiteral("The %1 command only works in Twitch channels")
QStringLiteral("The %1 command only works in Twitch channels.")
.arg(command)));
return {};
}
@ -29,7 +29,7 @@ QString toggleShieldMode(const CommandContext &ctx, bool isActivating)
if (user->isAnon())
{
ctx.channel->addMessage(makeSystemMessage(
QStringLiteral("You must be logged in to use the %1 command")
QStringLiteral("You must be logged in to use the %1 command.")
.arg(command)));
return {};
}

View file

@ -15,12 +15,12 @@ QString sendShoutout(const CommandContext &ctx)
{
auto *twitchChannel = ctx.twitchChannel;
auto channel = ctx.channel;
auto words = &ctx.words;
const auto *words = &ctx.words;
if (twitchChannel == nullptr)
{
channel->addMessage(makeSystemMessage(
"The /shoutout command only works in Twitch channels"));
"The /shoutout command only works in Twitch channels."));
return "";
}
@ -28,7 +28,7 @@ QString sendShoutout(const CommandContext &ctx)
if (currentUser->isAnon())
{
channel->addMessage(
makeSystemMessage("You must be logged in to send shoutout"));
makeSystemMessage("You must be logged in to send shoutout."));
return "";
}

View file

@ -84,7 +84,7 @@ QString startCommercial(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
"The /commercial command only works in Twitch channels"));
"The /commercial command only works in Twitch channels."));
return "";
}
@ -106,7 +106,7 @@ QString startCommercial(const CommandContext &ctx)
if (user->isAnon())
{
ctx.channel->addMessage(makeSystemMessage(
"You must be logged in to use the /commercial command"));
"You must be logged in to use the /commercial command."));
return "";
}

View file

@ -94,7 +94,7 @@ QString unbanUser(const CommandContext &ctx)
if (ctx.twitchChannel == nullptr)
{
ctx.channel->addMessage(makeSystemMessage(
QString("The %1 command only works in Twitch channels")
QString("The %1 command only works in Twitch channels.")
.arg(commandName)));
return "";
}

View file

@ -1,7 +1,7 @@
#include "controllers/commands/builtin/twitch/UpdateChannel.hpp"
#include "common/Channel.hpp"
#include "common/NetworkResult.hpp"
#include "common/network/NetworkResult.hpp"
#include "controllers/commands/CommandContext.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/api/Helix.hpp"

View file

@ -22,7 +22,7 @@ QString updateUserColor(const CommandContext &ctx)
if (!ctx.channel->isTwitchChannel())
{
ctx.channel->addMessage(makeSystemMessage(
"The /color command only works in Twitch channels"));
"The /color command only works in Twitch channels."));
return "";
}
auto user = getApp()->accounts->twitch.getCurrent();
@ -31,7 +31,7 @@ QString updateUserColor(const CommandContext &ctx)
if (user->isAnon())
{
ctx.channel->addMessage(makeSystemMessage(
"You must be logged in to use the /color command"));
"You must be logged in to use the /color command."));
return "";
}

View file

@ -19,7 +19,9 @@ FilterSet::FilterSet(const QList<QUuid> &filterIds)
for (const auto &f : *filters)
{
if (filterIds.contains(f->getId()))
{
this->filters_.insert(f->getId(), f);
}
}
this->listener_ =
@ -36,13 +38,17 @@ FilterSet::~FilterSet()
bool FilterSet::filter(const MessagePtr &m, ChannelPtr channel) const
{
if (this->filters_.size() == 0)
{
return true;
}
filters::ContextMap context = filters::buildContextMap(m, channel.get());
for (const auto &f : this->filters_.values())
{
if (!f->valid() || !f->filter(context))
{
return false;
}
}
return true;

View file

@ -44,6 +44,8 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
* flags.whisper
* flags.reply
* flags.automod
* flags.restricted
* flags.monitored
*
* message.content
* message.length
@ -101,6 +103,8 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
{"flags.reply", m->flags.has(MessageFlag::ReplyMessage)},
{"flags.automod", m->flags.has(MessageFlag::AutoMod)},
{"flags.restricted", m->flags.has(MessageFlag::RestrictedMessage)},
{"flags.monitored", m->flags.has(MessageFlag::MonitoredMessage)},
{"message.content", m->messageText},
{"message.length", m->messageText.length()},

View file

@ -44,6 +44,8 @@ static const QMap<QString, Type> MESSAGE_TYPING_CONTEXT = {
{"flags.whisper", Type::Bool},
{"flags.reply", Type::Bool},
{"flags.automod", Type::Bool},
{"flags.restricted", Type::Bool},
{"flags.monitored", Type::Bool},
{"message.content", Type::String},
{"message.length", Type::Int},
};

View file

@ -105,7 +105,9 @@ QString Tokenizer::current() const
QString Tokenizer::preview() const
{
if (this->hasNext())
{
return this->tokens_.at(this->i_);
}
return "";
}
@ -172,51 +174,97 @@ const QStringList Tokenizer::allTokens()
TokenType Tokenizer::tokenize(const QString &text)
{
if (text == "&&")
{
return TokenType::AND;
}
else if (text == "||")
{
return TokenType::OR;
}
else if (text == "(")
{
return TokenType::LP;
}
else if (text == ")")
{
return TokenType::RP;
}
else if (text == "{")
{
return TokenType::LIST_START;
}
else if (text == "}")
{
return TokenType::LIST_END;
}
else if (text == ",")
{
return TokenType::COMMA;
}
else if (text == "+")
{
return TokenType::PLUS;
}
else if (text == "-")
{
return TokenType::MINUS;
}
else if (text == "*")
{
return TokenType::MULTIPLY;
}
else if (text == "/")
{
return TokenType::DIVIDE;
}
else if (text == "==")
{
return TokenType::EQ;
}
else if (text == "!=")
{
return TokenType::NEQ;
}
else if (text == "%")
{
return TokenType::MOD;
}
else if (text == "<")
{
return TokenType::LT;
}
else if (text == ">")
{
return TokenType::GT;
}
else if (text == "<=")
{
return TokenType::LTE;
}
else if (text == ">=")
{
return TokenType::GTE;
}
else if (text == "contains")
{
return TokenType::CONTAINS;
}
else if (text == "startswith")
{
return TokenType::STARTS_WITH;
}
else if (text == "endswith")
{
return TokenType::ENDS_WITH;
}
else if (text == "match")
{
return TokenType::MATCH;
}
else if (text == "!")
{
return TokenType::NOT;
}
else
{
if ((text.startsWith("r\"") || text.startsWith("ri\"")) &&
@ -226,14 +274,20 @@ TokenType Tokenizer::tokenize(const QString &text)
}
if (text.front() == '"' && text.back() == '"')
{
return TokenType::STRING;
}
if (validIdentifiersMap.keys().contains(text))
{
return TokenType::IDENTIFIER;
}
bool flag;
if (text.toInt(&flag); flag)
{
return TokenType::INT;
}
}
return TokenType::NONE;

View file

@ -32,6 +32,8 @@ static const QMap<QString, QString> validIdentifiersMap = {
{"flags.whisper", "whisper message?"},
{"flags.reply", "reply message?"},
{"flags.automod", "automod message?"},
{"flags.restricted", "restricted message?"},
{"flags.monitored", "monitored message?"},
{"message.content", "message text"},
{"message.length", "message length"}};

View file

@ -69,27 +69,39 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
return 0;
case MINUS:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() - right.toInt();
}
return 0;
case MULTIPLY:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() * right.toInt();
}
return 0;
case DIVIDE:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() / right.toInt();
}
return 0;
case MOD:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() % right.toInt();
}
return 0;
case OR:
if (convertVariantTypes(left, right, QMetaType::Bool))
{
return left.toBool() || right.toBool();
}
return false;
case AND:
if (convertVariantTypes(left, right, QMetaType::Bool))
{
return left.toBool() && right.toBool();
}
return false;
case EQ:
if (variantTypesMatch(left, right, QMetaType::QString))
@ -107,19 +119,27 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
return !looselyCompareVariants(left, right);
case LT:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() < right.toInt();
}
return false;
case GT:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() > right.toInt();
}
return false;
case LTE:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() <= right.toInt();
}
return false;
case GTE:
if (convertVariantTypes(left, right, QMetaType::Int))
{
return left.toInt() >= right.toInt();
}
return false;
case CONTAINS:
if (variantIs(left, QMetaType::QStringList) &&
@ -215,22 +235,30 @@ QVariant BinaryOperation::execute(const ContextMap &context) const
// list must be two items
if (list.size() != 2)
{
return false;
}
// list must be a regular expression and an int
if (variantIsNot(list.at(0),
QMetaType::QRegularExpression) ||
variantIsNot(list.at(1), QMetaType::Int))
{
return false;
}
auto match =
list.at(0).toRegularExpression().match(matching);
// if matched, return nth capture group. Otherwise, return ""
if (match.hasMatch())
{
return match.captured(list.at(1).toInt());
}
else
{
return "";
}
}
default:
return false;
@ -263,9 +291,13 @@ PossibleType BinaryOperation::synthesizeType(const TypingContext &context) const
{
case PLUS:
if (left == Type::String)
{
return TypeClass{Type::String}; // String concatenation
}
else if (left == Type::Int && right == Type::Int)
{
return TypeClass{Type::Int};
}
return IllTyped{this, "Can only add Ints or concatenate a String"};
case MINUS:
@ -273,13 +305,17 @@ PossibleType BinaryOperation::synthesizeType(const TypingContext &context) const
case DIVIDE:
case MOD:
if (left == Type::Int && right == Type::Int)
{
return TypeClass{Type::Int};
}
return IllTyped{this, "Can only perform operation with Ints"};
case OR:
case AND:
if (left == Type::Bool && right == Type::Bool)
{
return TypeClass{Type::Bool};
}
return IllTyped{this,
"Can only perform logical operations with Bools"};
@ -292,37 +328,53 @@ PossibleType BinaryOperation::synthesizeType(const TypingContext &context) const
case LTE:
case GTE:
if (left == Type::Int && right == Type::Int)
{
return TypeClass{Type::Bool};
}
return IllTyped{this, "Can only perform comparisons with Ints"};
case STARTS_WITH:
case ENDS_WITH:
if (isList(left))
{
return TypeClass{Type::Bool};
}
if (left == Type::String && right == Type::String)
{
return TypeClass{Type::Bool};
}
return IllTyped{
this,
"Can only perform starts/ends with a List or two Strings"};
case CONTAINS:
if (isList(left) || left == Type::Map)
{
return TypeClass{Type::Bool};
}
if (left == Type::String && right == Type::String)
{
return TypeClass{Type::Bool};
}
return IllTyped{
this,
"Can only perform contains with a List, a Map, or two Strings"};
case MATCH: {
if (left != Type::String)
{
return IllTyped{this,
"Left argument of match must be a String"};
}
if (right == Type::RegularExpression)
{
return TypeClass{Type::Bool};
if (right == Type::MatchingSpecifier) // group capturing
}
if (right == Type::MatchingSpecifier)
{ // group capturing
return TypeClass{Type::String};
}
return IllTyped{this, "Can only match on a RegularExpression or a "
"MatchingSpecifier"};

View file

@ -53,7 +53,7 @@ void BadgeHighlightModel::getRowFromItem(const HighlightBadge &item,
setFilePathItem(row[Column::SoundPath], item.getSoundUrl());
setColorItem(row[Column::Color], *item.getColor());
TwitchBadges::instance()->getBadgeIcon(
getIApp()->getTwitchBadges()->getBadgeIcon(
item.badgeName(), [item, row](QString /*name*/, const QIconPtr pixmap) {
row[Column::Badge]->setData(QVariant(*pixmap), Qt::DecorationRole);
});

View file

@ -123,7 +123,9 @@ struct Deserialize<chatterino::HighlightBadge> {
auto _color = QColor(encodedColor);
if (!_color.isValid())
{
_color = chatterino::HighlightBadge::FALLBACK_HIGHLIGHT_COLOR;
}
return chatterino::HighlightBadge(_name, _displayName, _showInMentions,
_hasAlert, _hasSound, _soundUrl,

View file

@ -204,6 +204,41 @@ void rebuildMessageHighlights(Settings &settings,
{
checks.emplace_back(highlightPhraseCheck(highlight));
}
if (settings.enableAutomodHighlight)
{
const auto highlightSound =
settings.enableAutomodHighlightSound.getValue();
const auto highlightAlert =
settings.enableAutomodHighlightTaskbar.getValue();
const auto highlightSoundUrlValue =
settings.automodHighlightSoundUrl.getValue();
checks.emplace_back(HighlightCheck{
[=](const auto & /*args*/, const auto & /*badges*/,
const auto & /*senderName*/, const auto & /*originalMessage*/,
const auto &flags,
const auto /*self*/) -> std::optional<HighlightResult> {
if (!flags.has(MessageFlag::AutoModOffendingMessage))
{
return std::nullopt;
}
std::optional<QUrl> highlightSoundUrl;
if (!highlightSoundUrlValue.isEmpty())
{
highlightSoundUrl = highlightSoundUrlValue;
}
return HighlightResult{
highlightAlert, // alert
highlightSound, // playSound
highlightSoundUrl, // customSoundUrl
nullptr, // color
false, // showInMentions
};
}});
}
}
void rebuildUserHighlights(Settings &settings,
@ -405,7 +440,8 @@ std::ostream &operator<<(std::ostream &os, const HighlightResult &result)
return os;
}
void HighlightController::initialize(Settings &settings, Paths & /*paths*/)
void HighlightController::initialize(Settings &settings,
const Paths & /*paths*/)
{
this->rebuildListener_.addSetting(settings.enableSelfHighlight);
this->rebuildListener_.addSetting(settings.enableSelfHighlightSound);
@ -434,6 +470,11 @@ void HighlightController::initialize(Settings &settings, Paths & /*paths*/)
this->rebuildListener_.addSetting(settings.threadHighlightSoundUrl);
this->rebuildListener_.addSetting(settings.showThreadHighlightInMentions);
this->rebuildListener_.addSetting(settings.enableAutomodHighlight);
this->rebuildListener_.addSetting(settings.enableAutomodHighlightSound);
this->rebuildListener_.addSetting(settings.enableAutomodHighlightTaskbar);
this->rebuildListener_.addSetting(settings.automodHighlightSoundUrl);
this->rebuildListener_.setCB([this, &settings] {
qCDebug(chatterinoHighlights)
<< "Rebuild checks because a setting changed";

View file

@ -86,7 +86,7 @@ struct HighlightCheck {
class HighlightController final : public Singleton
{
public:
void initialize(Settings &settings, Paths &paths) override;
void initialize(Settings &settings, const Paths &paths) override;
/**
* @brief Checks the given message parameters if it matches our internal checks, and returns a result

View file

@ -98,9 +98,8 @@ void HighlightModel::afterInit()
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
setFilePathItem(whisperRow[Column::SoundPath], whisperSound, false);
// auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
// setColorItem(whisperRow[Column::Color], *whisperColor, false);
whisperRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags);
auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
setColorItem(whisperRow[Column::Color], *whisperColor, false);
this->insertCustomRow(whisperRow, HighlightRowIndexes::WhisperRow);
@ -234,6 +233,30 @@ void HighlightModel::afterInit()
this->insertCustomRow(threadMessageRow,
HighlightRowIndexes::ThreadMessageRow);
// Highlight settings for automod caught messages
const std::vector<QStandardItem *> automodRow = this->createRow();
setBoolItem(automodRow[Column::Pattern],
getSettings()->enableAutomodHighlight.getValue(), true, false);
automodRow[Column::Pattern]->setData("AutoMod Caught Messages",
Qt::DisplayRole);
automodRow[Column::ShowInMentions]->setFlags({});
setBoolItem(automodRow[Column::FlashTaskbar],
getSettings()->enableAutomodHighlightTaskbar.getValue(), true,
false);
setBoolItem(automodRow[Column::PlaySound],
getSettings()->enableAutomodHighlightSound.getValue(), true,
false);
automodRow[Column::UseRegex]->setFlags({});
automodRow[Column::CaseSensitive]->setFlags({});
const auto automodSound =
QUrl(getSettings()->automodHighlightSoundUrl.getValue());
setFilePathItem(automodRow[Column::SoundPath], automodSound, false);
automodRow[Column::Color]->setFlags(Qt::ItemFlag::NoItemFlags);
this->insertCustomRow(automodRow, HighlightRowIndexes::AutomodRow);
}
void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
@ -278,6 +301,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->enableThreadHighlight.setValue(
value.toBool());
}
else if (rowIndex == HighlightRowIndexes::AutomodRow)
{
getSettings()->enableAutomodHighlight.setValue(
value.toBool());
}
}
}
break;
@ -336,6 +364,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->enableThreadHighlightTaskbar.setValue(
value.toBool());
}
else if (rowIndex == HighlightRowIndexes::AutomodRow)
{
getSettings()->enableAutomodHighlightTaskbar.setValue(
value.toBool());
}
}
}
break;
@ -377,6 +410,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->enableThreadHighlightSound.setValue(
value.toBool());
}
else if (rowIndex == HighlightRowIndexes::AutomodRow)
{
getSettings()->enableAutomodHighlightSound.setValue(
value.toBool());
}
}
}
break;
@ -412,6 +450,11 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->threadHighlightSoundUrl.setValue(
value.toString());
}
else if (rowIndex == HighlightRowIndexes::AutomodRow)
{
getSettings()->automodHighlightSoundUrl.setValue(
value.toString());
}
}
}
break;
@ -419,48 +462,47 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
// Custom color
if (role == Qt::DecorationRole)
{
auto colorName = value.value<QColor>().name(QColor::HexArgb);
const auto setColor = [&](auto &setting, ColorType ty) {
auto color = value.value<QColor>();
setting.setValue(color.name(QColor::HexArgb));
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ty, color);
};
if (rowIndex == HighlightRowIndexes::SelfHighlightRow)
{
getSettings()->selfHighlightColor.setValue(colorName);
setColor(getSettings()->selfHighlightColor,
ColorType::SelfHighlight);
}
else if (rowIndex == HighlightRowIndexes::WhisperRow)
{
setColor(getSettings()->whisperHighlightColor,
ColorType::Whisper);
}
// else if (rowIndex == HighlightRowIndexes::WhisperRow)
// {
// getSettings()->whisperHighlightColor.setValue(colorName);
// }
else if (rowIndex == HighlightRowIndexes::SubRow)
{
getSettings()->subHighlightColor.setValue(colorName);
setColor(getSettings()->subHighlightColor,
ColorType::Subscription);
}
else if (rowIndex == HighlightRowIndexes::RedeemedRow)
{
getSettings()->redeemedHighlightColor.setValue(colorName);
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ColorType::RedeemedHighlight,
QColor(colorName));
setColor(getSettings()->redeemedHighlightColor,
ColorType::RedeemedHighlight);
}
else if (rowIndex == HighlightRowIndexes::FirstMessageRow)
{
getSettings()->firstMessageHighlightColor.setValue(
colorName);
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ColorType::FirstMessageHighlight,
QColor(colorName));
setColor(getSettings()->firstMessageHighlightColor,
ColorType::FirstMessageHighlight);
}
else if (rowIndex == HighlightRowIndexes::ElevatedMessageRow)
{
getSettings()->elevatedMessageHighlightColor.setValue(
colorName);
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ColorType::ElevatedMessageHighlight,
QColor(colorName));
setColor(getSettings()->elevatedMessageHighlightColor,
ColorType::ElevatedMessageHighlight);
}
else if (rowIndex == HighlightRowIndexes::ThreadMessageRow)
{
getSettings()->threadHighlightColor.setValue(colorName);
const_cast<ColorProvider &>(ColorProvider::instance())
.updateColor(ColorType::ThreadMessageHighlight,
QColor(colorName));
setColor(getSettings()->threadHighlightColor,
ColorType::ThreadMessageHighlight);
}
}
}

View file

@ -34,6 +34,7 @@ public:
FirstMessageRow = 4,
ElevatedMessageRow = 5,
ThreadMessageRow = 6,
AutomodRow = 7,
};
enum UserHighlightRowIndexes {

View file

@ -164,7 +164,9 @@ struct Deserialize<chatterino::HighlightPhrase> {
auto _color = QColor(encodedColor);
if (!_color.isValid())
{
_color = chatterino::HighlightPhrase::FALLBACK_HIGHLIGHT_COLOR;
}
return chatterino::HighlightPhrase(_pattern, _showInMentions, _hasAlert,
_hasSound, _isRegex,

View file

@ -1,7 +1,6 @@
#include "UserHighlightModel.hpp"
#include "controllers/highlights/UserHighlightModel.hpp"
#include "Application.hpp"
#include "controllers/highlights/HighlightModel.hpp"
#include "controllers/highlights/HighlightPhrase.hpp"
#include "providers/colors/ColorProvider.hpp"
#include "singletons/Settings.hpp"
@ -10,8 +9,6 @@
namespace chatterino {
using Column = HighlightModel::Column;
// commandmodel
UserHighlightModel::UserHighlightModel(QObject *parent)
: SignalVectorModel<HighlightPhrase>(Column::COUNT, parent)

View file

@ -1,6 +1,7 @@
#pragma once
#include "common/SignalVectorModel.hpp"
#include "controllers/highlights/HighlightModel.hpp"
#include <QObject>
@ -12,6 +13,8 @@ class HighlightPhrase;
class UserHighlightModel : public SignalVectorModel<HighlightPhrase>
{
public:
using Column = HighlightModel::Column;
explicit UserHighlightModel(QObject *parent);
protected:

View file

@ -66,7 +66,7 @@ std::vector<QShortcut *> HotkeyController::shortcutsForCategory(
continue;
}
auto createShortcutFromKeySeq = [&](QKeySequence qs) {
auto s = new QShortcut(qs, parent);
auto *s = new QShortcut(qs, parent);
s->setContext(hotkey->getContext());
auto functionPointer = target->second;
QObject::connect(s, &QShortcut::activated, parent,
@ -101,7 +101,7 @@ void HotkeyController::save()
std::shared_ptr<Hotkey> HotkeyController::getHotkeyByName(QString name)
{
for (auto &hotkey : this->hotkeys_)
for (const auto &hotkey : this->hotkeys_)
{
if (hotkey->name() == name)
{
@ -115,7 +115,7 @@ int HotkeyController::replaceHotkey(QString oldName,
std::shared_ptr<Hotkey> newHotkey)
{
int i = 0;
for (auto &hotkey : this->hotkeys_)
for (const auto &hotkey : this->hotkeys_)
{
if (hotkey->name() == oldName)
{
@ -544,7 +544,7 @@ void HotkeyController::tryAddDefault(std::set<QString> &addedHotkeys,
void HotkeyController::showHotkeyError(const std::shared_ptr<Hotkey> &hotkey,
QString warning)
{
auto msgBox = new QMessageBox(
auto *msgBox = new QMessageBox(
QMessageBox::Icon::Warning, "Hotkey error",
QString(
"There was an error while executing your hotkey named \"%1\": \n%2")

View file

@ -32,9 +32,11 @@ bool isIgnoredMessage(IgnoredMessageParameters &&params)
{
auto sourceUserID = params.twitchUserID;
bool isBlocked =
getApp()->accounts->twitch.getCurrent()->blockedUserIds().contains(
sourceUserID);
bool isBlocked = getIApp()
->getAccounts()
->twitch.getCurrent()
->blockedUserIds()
.contains(sourceUserID);
if (isBlocked)
{
switch (static_cast<ShowIgnoredUsersMessages>(

View file

@ -143,11 +143,15 @@ const std::optional<ImagePtr> &ModerationAction::getImage() const
if (this->imageToLoad_ != 0)
{
if (this->imageToLoad_ == 1)
{
this->image_ =
Image::fromResourcePixmap(getResources().buttons.ban);
}
else if (this->imageToLoad_ == 2)
{
this->image_ =
Image::fromResourcePixmap(getResources().buttons.trashCan);
}
}
return this->image_;

View file

@ -26,7 +26,7 @@
namespace chatterino {
void NotificationController::initialize(Settings &settings, Paths &paths)
void NotificationController::initialize(Settings &settings, const Paths &paths)
{
this->initialized_ = true;
for (const QString &channelName : this->twitchSetting_.getValue())
@ -225,7 +225,7 @@ void NotificationController::removeFakeChannel(const QString channelName)
for (int i = snapshotLength - 1; i >= end; --i)
{
auto &s = snapshot[i];
const auto &s = snapshot[i];
if (s->messageText == liveMessageSearchText)
{

Some files were not shown because too many files have changed in this diff Show more