mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-26 17:37:07 +00:00
Merge branch 'sentry' (fix #2857)
This commit is contained in:
commit
364f62ee4a
@ -79,8 +79,14 @@ option(ENABLE_UI "Compile UI (turn off to compile CLI-only version)" o
|
||||
option(FULLSCREEN_PLATFORM "Enable fullscreen by default" off)
|
||||
option(ENABLE_CLANG_TIDY "Enable static analysis" off)
|
||||
option(ENABLE_CCACHE "Use CCache to improve recompilation speed (optional)" on)
|
||||
option(ENABLE_SENTRY "Use Sentry SDK to report crashes" off)
|
||||
set(CUSTOM_WEBSITE_URL "" CACHE STRING "Enable custom local webserver to check updates")
|
||||
|
||||
if(ENABLE_SENTRY)
|
||||
set(SENTRY_DIR "" CACHE STRING "Sentry native location")
|
||||
set(SENTRY_DNS "" CACHE STRING "Sentry DNS URL")
|
||||
endif()
|
||||
|
||||
if(ENABLE_NEWS OR ENABLE_UPDATER)
|
||||
set(REQUIRE_CURL ON)
|
||||
else()
|
||||
|
@ -723,6 +723,9 @@
|
||||
<style id="workspace_tabs">
|
||||
<background color="workspace" />
|
||||
</style>
|
||||
<style id="workspace_check_box" extends="check_box" padding="4">
|
||||
<text color="workspace_text" align="left middle" x="14" />
|
||||
</style>
|
||||
<style id="tab">
|
||||
<background part="tab_normal" align="middle" />
|
||||
<background part="tab_active" align="middle" state="focus" />
|
||||
|
@ -731,11 +731,16 @@ Recover files from crashed sessions or
|
||||
closed sprite that were not saved in
|
||||
previous sessions.
|
||||
END
|
||||
share_crashdb = Share crash data with Aseprite developers
|
||||
share_crashdb_tooltip = <<<END
|
||||
Check to share crash data with Aseprite developers automatically.
|
||||
This will help to find new bugs and improve the general stability
|
||||
of Aseprite for all users.
|
||||
END
|
||||
recent_files = Recent files:
|
||||
recent_folders = Recent folders:
|
||||
news = News:
|
||||
checking_updates = Checking Updates...
|
||||
is_up_to_date = {0} is up to date
|
||||
new_version_available = New {0} v{1} available!
|
||||
|
||||
[import_sprite_sheet]
|
||||
|
@ -1,5 +1,5 @@
|
||||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2019 Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2019-2021 Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2001-2017 David Capello -->
|
||||
<gui>
|
||||
<vbox noborders="true" id="home_view" border="4" childspacing="2" expansive="true">
|
||||
@ -14,6 +14,8 @@
|
||||
</vbox>
|
||||
<boxfiller />
|
||||
<vbox border="4">
|
||||
<check id="share_crashdb" text="@.share_crashdb"
|
||||
tooltip="@.share_crashdb_tooltip" style="workspace_check_box" />
|
||||
<link id="check_update" text="" style="workspace_link" />
|
||||
</vbox>
|
||||
</hbox>
|
||||
|
@ -68,6 +68,9 @@
|
||||
text="@.color_bar_entries_separator"
|
||||
tooltip="@.color_bar_entries_separator"
|
||||
pref="color_bar.entries_separator" />
|
||||
<check id="share_crashdb"
|
||||
text="@home_view.share_crashdb"
|
||||
tooltip="@home_view.share_crashdb_tooltip" />
|
||||
|
||||
<separator horizontal="true" />
|
||||
<link id="locate_file" text="@.locate_file" />
|
||||
|
@ -951,6 +951,31 @@ possible. They may also add themselves to the list below.
|
||||
*/
|
||||
```
|
||||
|
||||
# [Sentry](https://sentry.io)
|
||||
|
||||
```
|
||||
Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors.
|
||||
All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
# [skia](https://skia.org)
|
||||
|
||||
```
|
||||
|
@ -401,6 +401,13 @@ if(ENABLE_UI)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(send_crash_files)
|
||||
if(ENABLE_SENTRY)
|
||||
set(send_crash_files sentry_wrapper.cpp)
|
||||
else()
|
||||
set(send_crash_files send_crash.cpp)
|
||||
endif()
|
||||
|
||||
add_library(app-lib
|
||||
active_site_handler.cpp
|
||||
app.cpp
|
||||
@ -575,7 +582,6 @@ add_library(app-lib
|
||||
res/resources_loader.cpp
|
||||
resource_finder.cpp
|
||||
restore_visible_layers.cpp
|
||||
send_crash.cpp
|
||||
shade.cpp
|
||||
site.cpp
|
||||
snap_to_grid.cpp
|
||||
@ -617,6 +623,7 @@ add_library(app-lib
|
||||
util/wrap_point.cpp
|
||||
xml_document.cpp
|
||||
xml_exception.cpp
|
||||
${send_crash_files}
|
||||
${ui_app_files}
|
||||
${app_platform_files}
|
||||
${data_recovery_files}
|
||||
@ -677,3 +684,12 @@ if(ENABLE_STEAM)
|
||||
add_definitions(-DENABLE_STEAM)
|
||||
target_link_libraries(app-lib steam-lib)
|
||||
endif()
|
||||
|
||||
if(ENABLE_SENTRY)
|
||||
target_compile_definitions(app-lib PUBLIC
|
||||
-DENABLE_SENTRY
|
||||
-DSENTRY_BUILD_STATIC=1
|
||||
-DSENTRY_DNS="${SENTRY_DNS}")
|
||||
add_subdirectory(${SENTRY_DIR} sentry)
|
||||
target_link_libraries(app-lib sentry)
|
||||
endif()
|
||||
|
@ -418,8 +418,10 @@ void App::run()
|
||||
checkUpdate.launch();
|
||||
#endif
|
||||
|
||||
#if !ENABLE_SENTRY
|
||||
app::SendCrash sendCrash;
|
||||
sendCrash.search();
|
||||
#endif
|
||||
|
||||
// Keep the console alive the whole program execute (just in case
|
||||
// we've to print errors).
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2020 Igara Studio S.A.
|
||||
// Copyright (C) 2020-2021 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -21,6 +21,10 @@
|
||||
#include "base/version.h"
|
||||
#include "ver/info.h"
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
#include "app/sentry_wrapper.h"
|
||||
#endif
|
||||
|
||||
#include <ctime>
|
||||
#include <sstream>
|
||||
|
||||
@ -113,6 +117,14 @@ CheckUpdateThreadLauncher::~CheckUpdateThreadLauncher()
|
||||
|
||||
void CheckUpdateThreadLauncher::launch()
|
||||
{
|
||||
if (m_uuid.empty())
|
||||
m_uuid = m_preferences.updater.uuid();
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
if (!m_uuid.empty())
|
||||
Sentry::setUserID(m_uuid);
|
||||
#endif
|
||||
|
||||
// In this case we are in the "wait days" period, so we don't check
|
||||
// for updates.
|
||||
if (!m_doCheck) {
|
||||
@ -120,9 +132,6 @@ void CheckUpdateThreadLauncher::launch()
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_uuid.empty())
|
||||
m_uuid = m_preferences.updater.uuid();
|
||||
|
||||
m_delegate->onCheckingUpdates();
|
||||
|
||||
m_bgJob.reset(new CheckUpdateBackgroundJob);
|
||||
@ -168,6 +177,11 @@ void CheckUpdateThreadLauncher::onMonitoringTick()
|
||||
if (!m_response.getUuid().empty()) {
|
||||
m_uuid = m_response.getUuid();
|
||||
m_preferences.updater.uuid(m_uuid);
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
if (!m_uuid.empty())
|
||||
Sentry::setUserID(m_uuid);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Set the date of the last "check for updates" and the "WaitDays" parameter.
|
||||
|
@ -27,6 +27,7 @@
|
||||
#include "app/resource_finder.h"
|
||||
#include "app/tx.h"
|
||||
#include "app/ui/color_button.h"
|
||||
#include "app/ui/main_window.h"
|
||||
#include "app/ui/pref_widget.h"
|
||||
#include "app/ui/separator_in_view.h"
|
||||
#include "app/ui/skin/skin_theme.h"
|
||||
@ -42,6 +43,10 @@
|
||||
#include "render/render.h"
|
||||
#include "ui/ui.h"
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
#include "app/sentry_wrapper.h"
|
||||
#endif
|
||||
|
||||
#include "options.xml.h"
|
||||
|
||||
namespace app {
|
||||
@ -490,6 +495,13 @@ public:
|
||||
else
|
||||
locateCrashFolder()->setVisible(false);
|
||||
|
||||
// Share crashdb
|
||||
#if ENABLE_SENTRY
|
||||
shareCrashdb()->setSelected(Sentry::consentGiven());
|
||||
#else
|
||||
shareCrashdb()->setVisible(false);
|
||||
#endif
|
||||
|
||||
// Undo preferences
|
||||
limitUndo()->Click.connect([this]{ onLimitUndoCheck(); });
|
||||
limitUndo()->setSelected(m_pref.undo.sizeLimit() != 0);
|
||||
@ -541,6 +553,15 @@ public:
|
||||
sendMessage(msg);
|
||||
}
|
||||
|
||||
// Share crashdb
|
||||
#if ENABLE_SENTRY
|
||||
if (shareCrashdb()->isSelected())
|
||||
Sentry::giveConsent();
|
||||
else
|
||||
Sentry::revokeConsent();
|
||||
App::instance()->mainWindow()->updateConsentCheckbox();
|
||||
#endif
|
||||
|
||||
// Update language
|
||||
Strings::instance()->setCurrentLanguage(
|
||||
language()->getItemText(language()->getSelectedItemIndex()));
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2020 Igara Studio S.A.
|
||||
// Copyright (C) 2020-2021 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -16,6 +16,8 @@
|
||||
|
||||
namespace app {
|
||||
|
||||
#if !ENABLE_SENTRY
|
||||
|
||||
class SendCrash
|
||||
#ifdef ENABLE_UI
|
||||
: public INotificationDelegate
|
||||
@ -43,6 +45,8 @@ namespace app {
|
||||
std::string m_dumpFilename;
|
||||
};
|
||||
|
||||
#endif // !ENABLE_SENTRY
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // APP_SEND_CRASH_H_INCLUDED
|
||||
|
113
src/app/sentry_wrapper.cpp
Normal file
113
src/app/sentry_wrapper.cpp
Normal file
@ -0,0 +1,113 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2021 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "app/sentry_wrapper.h"
|
||||
|
||||
#include "app/resource_finder.h"
|
||||
#include "base/fs.h"
|
||||
#include "base/string.h"
|
||||
#include "ver/info.h"
|
||||
|
||||
#include "sentry.h"
|
||||
|
||||
namespace app {
|
||||
|
||||
// Directory where Sentry database is saved.
|
||||
std::string Sentry::m_dbdir;
|
||||
|
||||
void Sentry::init()
|
||||
{
|
||||
sentry_options_t* options = sentry_options_new();
|
||||
sentry_options_set_dsn(options, SENTRY_DNS);
|
||||
|
||||
std::string release = "aseprite@";
|
||||
release += get_app_version();
|
||||
sentry_options_set_release(options, release.c_str());
|
||||
|
||||
#if _DEBUG
|
||||
sentry_options_set_debug(options, 1);
|
||||
#endif
|
||||
|
||||
setupDirs(options);
|
||||
|
||||
// We require the user consent to upload files.
|
||||
sentry_options_set_require_user_consent(options, 1);
|
||||
|
||||
if (sentry_init(options) == 0)
|
||||
m_init = true;
|
||||
}
|
||||
|
||||
Sentry::~Sentry()
|
||||
{
|
||||
if (m_init)
|
||||
sentry_close();
|
||||
}
|
||||
|
||||
// static
|
||||
void Sentry::setUserID(const std::string& uuid)
|
||||
{
|
||||
sentry_value_t user = sentry_value_new_object();
|
||||
sentry_value_set_by_key(user, "id", sentry_value_new_string(uuid.c_str()));
|
||||
sentry_set_user(user);
|
||||
}
|
||||
|
||||
// static
|
||||
bool Sentry::requireConsent()
|
||||
{
|
||||
return (sentry_user_consent_get() != SENTRY_USER_CONSENT_GIVEN);
|
||||
}
|
||||
|
||||
// static
|
||||
bool Sentry::consentGiven()
|
||||
{
|
||||
return (sentry_user_consent_get() == SENTRY_USER_CONSENT_GIVEN);
|
||||
}
|
||||
|
||||
// static
|
||||
void Sentry::giveConsent()
|
||||
{
|
||||
sentry_user_consent_give();
|
||||
}
|
||||
|
||||
// static
|
||||
void Sentry::revokeConsent()
|
||||
{
|
||||
sentry_user_consent_revoke();
|
||||
}
|
||||
|
||||
// static
|
||||
bool Sentry::areThereCrashesToReport()
|
||||
{
|
||||
if (m_dbdir.empty())
|
||||
return false;
|
||||
|
||||
for (auto f : base::list_files(base::join_path(m_dbdir, "completed"))) {
|
||||
if (base::get_file_extension(f) == "dmp")
|
||||
return true; // At least one .dmp file in the completed/ directory
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Sentry::setupDirs(sentry_options_t* options)
|
||||
{
|
||||
ResourceFinder rf;
|
||||
rf.includeUserDir("crashdb");
|
||||
const std::string dir = rf.getFirstOrCreateDefault();
|
||||
|
||||
#if SENTRY_PLATFORM_WINDOWS
|
||||
sentry_options_set_database_pathw(options, base::from_utf8(dir).c_str());
|
||||
#else
|
||||
sentry_options_set_database_path(options, dir.c_str());
|
||||
#endif
|
||||
|
||||
m_dbdir = dir;
|
||||
}
|
||||
|
||||
} // namespace app
|
45
src/app/sentry_wrapper.h
Normal file
45
src/app/sentry_wrapper.h
Normal file
@ -0,0 +1,45 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2021 Igara Studio S.A.
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
// the End-User License Agreement for Aseprite.
|
||||
|
||||
#ifndef APP_SENTRY_WRAPPER_H
|
||||
#define APP_SENTRY_WRAPPER_H
|
||||
|
||||
#if !ENABLE_SENTRY
|
||||
#error ENABLE_SENTRY must be defined
|
||||
#endif
|
||||
|
||||
#include "sentry.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace app {
|
||||
|
||||
class Sentry {
|
||||
public:
|
||||
void init();
|
||||
~Sentry();
|
||||
|
||||
static void setUserID(const std::string& uuid);
|
||||
|
||||
static bool requireConsent();
|
||||
static bool consentGiven();
|
||||
static void giveConsent();
|
||||
static void revokeConsent();
|
||||
|
||||
// Returns true if there are some crash to report. Used to display
|
||||
// the "give consent" check box for first time.
|
||||
static bool areThereCrashesToReport();
|
||||
|
||||
private:
|
||||
void setupDirs(sentry_options_t* options);
|
||||
|
||||
bool m_init = false;
|
||||
static std::string m_dbdir;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
||||
#endif // APP_SENTRY_WRAPPER_H
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2019-2020 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2021 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2018 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -38,6 +38,10 @@
|
||||
#include "app/ui/news_listbox.h"
|
||||
#endif
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
#include "app/sentry_wrapper.h"
|
||||
#endif
|
||||
|
||||
namespace app {
|
||||
|
||||
using namespace ui;
|
||||
@ -66,6 +70,23 @@ HomeView::HomeView()
|
||||
#endif
|
||||
|
||||
checkUpdate()->setVisible(false);
|
||||
shareCrashdb()->setVisible(false);
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
// Show this option in home tab only when we require consent for the
|
||||
// first time and there is crash data available to report
|
||||
if (Sentry::requireConsent() &&
|
||||
Sentry::areThereCrashesToReport()) {
|
||||
shareCrashdb()->setVisible(true);
|
||||
shareCrashdb()->Click.connect(
|
||||
[this]{
|
||||
if (shareCrashdb()->isSelected())
|
||||
Sentry::giveConsent();
|
||||
else
|
||||
Sentry::revokeConsent();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
InitTheme.connect(
|
||||
[this]{
|
||||
@ -101,6 +122,21 @@ void HomeView::dataRecoverySessionsAreReady()
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
void HomeView::updateConsentCheckbox()
|
||||
{
|
||||
if (Sentry::requireConsent()) {
|
||||
shareCrashdb()->setVisible(true);
|
||||
shareCrashdb()->setSelected(false);
|
||||
}
|
||||
else if (Sentry::consentGiven()) {
|
||||
shareCrashdb()->setVisible(false);
|
||||
shareCrashdb()->setSelected(true);
|
||||
}
|
||||
layout();
|
||||
}
|
||||
#endif
|
||||
|
||||
std::string HomeView::getTabText()
|
||||
{
|
||||
return Strings::home_view_title();
|
||||
@ -176,9 +212,7 @@ void HomeView::onCheckingUpdates()
|
||||
|
||||
void HomeView::onUpToDate()
|
||||
{
|
||||
checkUpdate()->setText(
|
||||
fmt::format(Strings::home_view_is_up_to_date(), get_app_name()));
|
||||
checkUpdate()->setVisible(true);
|
||||
checkUpdate()->setVisible(false);
|
||||
|
||||
layout();
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2019 Igara Studio S.A.
|
||||
// Copyright (C) 2019-2021 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2016 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -46,6 +46,10 @@ namespace app {
|
||||
// function is called.
|
||||
void dataRecoverySessionsAreReady();
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
void updateConsentCheckbox();
|
||||
#endif
|
||||
|
||||
// TabView implementation
|
||||
std::string getTabText() override;
|
||||
TabIcon getTabIcon() override;
|
||||
|
@ -223,6 +223,13 @@ CheckUpdateDelegate* MainWindow::getCheckUpdateDelegate()
|
||||
}
|
||||
#endif
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
void MainWindow::updateConsentCheckbox()
|
||||
{
|
||||
getHomeView()->updateConsentCheckbox();
|
||||
}
|
||||
#endif
|
||||
|
||||
void MainWindow::showNotification(INotificationDelegate* del)
|
||||
{
|
||||
m_notifications->addLink(del);
|
||||
|
@ -66,6 +66,9 @@ namespace app {
|
||||
#ifdef ENABLE_UPDATER
|
||||
CheckUpdateDelegate* getCheckUpdateDelegate();
|
||||
#endif
|
||||
#if ENABLE_SENTRY
|
||||
void updateConsentCheckbox();
|
||||
#endif
|
||||
|
||||
void start();
|
||||
void showNotification(INotificationDelegate* del);
|
||||
|
@ -16,10 +16,16 @@
|
||||
#include "app/send_crash.h"
|
||||
#include "base/exception.h"
|
||||
#include "base/memory.h"
|
||||
#include "base/memory_dump.h"
|
||||
#include "base/system_console.h"
|
||||
#include "os/error.h"
|
||||
#include "os/system.h"
|
||||
#include "ver/info.h"
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
#include "app/sentry_wrapper.h"
|
||||
#else
|
||||
#include "base/memory_dump.h"
|
||||
#endif
|
||||
|
||||
#include <clocale>
|
||||
#include <cstdlib>
|
||||
@ -78,13 +84,20 @@ int app_main(int argc, char* argv[])
|
||||
#endif
|
||||
|
||||
try {
|
||||
#if ENABLE_SENTRY
|
||||
app::Sentry sentry;
|
||||
#else
|
||||
base::MemoryDump memoryDump;
|
||||
#endif
|
||||
MemLeak memleak;
|
||||
base::SystemConsole systemConsole;
|
||||
app::AppOptions options(argc, const_cast<const char**>(argv));
|
||||
os::SystemRef system(os::make_system());
|
||||
app::App app;
|
||||
|
||||
#if ENABLE_SENTRY
|
||||
sentry.init();
|
||||
#else
|
||||
// Change the memory dump filename to save on disk (.dmp
|
||||
// file). Note: Only useful on Windows.
|
||||
{
|
||||
@ -92,6 +105,7 @@ int app_main(int argc, char* argv[])
|
||||
if (!fn.empty())
|
||||
memoryDump.setFileName(fn);
|
||||
}
|
||||
#endif
|
||||
|
||||
const int code = app.initialize(options);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user