mirror of
https://github.com/GoldenCheetah/GoldenCheetah.git
synced 2026-04-15 05:32:21 +00:00
ShareDialog: Add upload to CyclingAnalytics and SelfLoops
This commit is contained in:
@@ -896,7 +896,7 @@ MainWindow::MainWindow(const QDir &home) :
|
||||
connect(tweetAction, SIGNAL(triggered(bool)), this, SLOT(tweetRide()));
|
||||
rideMenu->addAction(tweetAction);
|
||||
|
||||
shareAction = new QAction(tr("Share (Strava, RideWithGPS)..."), this);
|
||||
shareAction = new QAction(tr("Share..."), this);
|
||||
connect(shareAction, SIGNAL(triggered(bool)), this, SLOT(share()));
|
||||
rideMenu->addAction(shareAction);
|
||||
#endif
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
*/
|
||||
|
||||
#include "OAuthDialog.h"
|
||||
//#include "Athlete.h"
|
||||
//#include "Context.h"
|
||||
#include "Settings.h"
|
||||
#include <QHttp>
|
||||
#include <QUrl>
|
||||
@@ -35,14 +33,27 @@ OAuthDialog::OAuthDialog(MainWindow *mainWindow, OAuthSite site) :
|
||||
layout->setContentsMargins(2,0,2,2);
|
||||
setLayout(layout);
|
||||
|
||||
QString urlstr = QString("https://www.strava.com/oauth/authorize?");
|
||||
urlstr.append("client_id=83&");
|
||||
urlstr.append("scope=view_private,write&");
|
||||
QString urlstr = "";
|
||||
if (site == STRAVA) {
|
||||
urlstr = QString("https://www.strava.com/oauth/authorize?");
|
||||
urlstr.append("client_id=").append(GC_STRAVA_CLIENT_ID).append("&");
|
||||
urlstr.append("scope=view_private,write&");
|
||||
}
|
||||
else if (site == TWITTER) {
|
||||
urlstr = QString("http://api.twitter.com/oauth/request_token?");
|
||||
// TODO
|
||||
}
|
||||
else if (site == CYCLING_ANALYTICS) {
|
||||
urlstr = QString("https://www.cyclinganalytics.com/api/auth?");
|
||||
urlstr.append("client_id=").append(GC_CYCLINGANALYTICS_CLIENT_ID).append("&");
|
||||
urlstr.append("scope=modify_rides&");
|
||||
}
|
||||
urlstr.append("redirect_uri=http://www.goldencheetah.org/&");
|
||||
urlstr.append("response_type=code&");
|
||||
urlstr.append("approval_prompt=force");
|
||||
url = QUrl(urlstr);
|
||||
|
||||
|
||||
url = QUrl(urlstr);
|
||||
view = new QWebView();
|
||||
view->setContentsMargins(0,0,0,0);
|
||||
view->page()->view()->setContentsMargins(0,0,0,0);
|
||||
@@ -62,25 +73,39 @@ OAuthDialog::OAuthDialog(MainWindow *mainWindow, OAuthSite site) :
|
||||
void
|
||||
OAuthDialog::urlChanged(const QUrl &url)
|
||||
{
|
||||
//qDebug() << url.toString();
|
||||
if (url.toString().startsWith("http://www.goldencheetah.org/?state=&code=")) {
|
||||
QString code = url.toString().right(40);
|
||||
//qDebug() << "code" << code;
|
||||
|
||||
const char *request_token_uri = "https://www.strava.com/oauth/token?";
|
||||
if (url.toString().startsWith("http://www.goldencheetah.org/?state=&code=") ||
|
||||
url.toString().startsWith("http://www.goldencheetah.org/?code=")) {
|
||||
QString code = url.toString().right(url.toString().length()-url.toString().indexOf("code=")-5);
|
||||
|
||||
QByteArray data;
|
||||
QUrl params;
|
||||
QString urlstr = "";
|
||||
|
||||
params.addQueryItem("code", code);
|
||||
params.addQueryItem("client_id", GC_STRAVA_CLIENT_ID);
|
||||
if (site == STRAVA) {
|
||||
urlstr = QString("https://www.strava.com/oauth/token?");
|
||||
params.addQueryItem("client_id", GC_STRAVA_CLIENT_ID);
|
||||
#ifdef GC_STRAVA_CLIENT_SECRET
|
||||
params.addQueryItem("client_secret", GC_STRAVA_CLIENT_SECRET);
|
||||
params.addQueryItem("client_secret", GC_STRAVA_CLIENT_SECRET);
|
||||
#endif
|
||||
params.addQueryItem("redirect_uri", "http://www.goldencheetah.org/");
|
||||
params.addQueryItem("redirect_uri", "http://www.goldencheetah.org/");
|
||||
}
|
||||
else if (site == TWITTER) {
|
||||
urlstr = QString("http://api.twitter.com/oauth/token?");
|
||||
// TODO
|
||||
}
|
||||
else if (site == CYCLING_ANALYTICS) {
|
||||
urlstr = QString("https://www.cyclinganalytics.com/api/token?");
|
||||
params.addQueryItem("client_id", GC_CYCLINGANALYTICS_CLIENT_ID);
|
||||
#ifdef GC_CYCLINGANALYTICS_CLIENT_SECRET
|
||||
params.addQueryItem("client_secret", GC_CYCLINGANALYTICS_CLIENT_SECRET);
|
||||
#endif
|
||||
params.addQueryItem("grant_type", "authorization_code");
|
||||
}
|
||||
params.addQueryItem("code", code);
|
||||
|
||||
data = params.encodedQuery();
|
||||
|
||||
QUrl url = QUrl( request_token_uri);
|
||||
QUrl url = QUrl( urlstr);
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader,"application/x-www-form-urlencoded");
|
||||
|
||||
@@ -93,14 +118,23 @@ void
|
||||
OAuthDialog::loadFinished()
|
||||
{
|
||||
if (requestToken) {
|
||||
int i = view->page()->mainFrame()->toHtml().indexOf("{\"access_token\":\"")+17;
|
||||
int j = view->page()->mainFrame()->toHtml().indexOf("\"", i);
|
||||
if (i>16 && j>-1) {
|
||||
QString access_token = view->page()->mainFrame()->toHtml().mid(i,j-i);
|
||||
//qDebug() << "token" << access_token;
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_STRAVA_TOKEN, access_token);
|
||||
|
||||
accept();
|
||||
int at = view->page()->mainFrame()->toHtml().indexOf("\"access_token\":");
|
||||
if (at>-1) {
|
||||
int i = view->page()->mainFrame()->toHtml().indexOf("\"", at+15);
|
||||
int j = view->page()->mainFrame()->toHtml().indexOf("\"", i+1);
|
||||
if (i>-1 && j>-1) {
|
||||
QString access_token = view->page()->mainFrame()->toHtml().mid(i+1,j-i-1);
|
||||
if (site == STRAVA) {
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_STRAVA_TOKEN, access_token);
|
||||
}
|
||||
else if (site == TWITTER) {
|
||||
// TODO
|
||||
}
|
||||
else if (site == CYCLING_ANALYTICS) {
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_CYCLINGANALYTICS_TOKEN, access_token);
|
||||
}
|
||||
accept();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ class OAuthDialog : public QDialog
|
||||
public:
|
||||
typedef enum {
|
||||
STRAVA,
|
||||
TWITTER
|
||||
TWITTER,
|
||||
CYCLING_ANALYTICS
|
||||
} OAuthSite;
|
||||
|
||||
OAuthDialog(MainWindow *mainWindow, OAuthSite site);
|
||||
|
||||
129
src/Pages.cpp
129
src/Pages.cpp
@@ -302,7 +302,10 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr
|
||||
//XXX deprecated QLabel *struserLabel = new QLabel(tr("Username"));
|
||||
//XXX deprecated QLabel *strpassLabel = new QLabel(tr("Password"));
|
||||
QLabel *strauthLabel = new QLabel(tr("Authorise"));
|
||||
QLabel *strpinLabel = new QLabel(tr("PIN"));
|
||||
|
||||
QLabel *can = new QLabel(tr("Cycling Analytics"));
|
||||
can->setFont(current);
|
||||
QLabel *canauthLabel = new QLabel(tr("Authorise"));
|
||||
|
||||
QLabel *rwgps = new QLabel(tr("RideWithGPS"));
|
||||
rwgps->setFont(current);
|
||||
@@ -316,6 +319,12 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr
|
||||
QLabel *ttbuserLabel = new QLabel(tr("Username"));
|
||||
QLabel *ttbpassLabel = new QLabel(tr("Password"));
|
||||
|
||||
QLabel *sel = new QLabel(tr("Selfloops"));
|
||||
sel->setFont(current);
|
||||
|
||||
QLabel *seluserLabel = new QLabel(tr("Username"));
|
||||
QLabel *selpassLabel = new QLabel(tr("Password"));
|
||||
|
||||
QLabel *wip = new QLabel(tr("Withings Wifi Scales"));
|
||||
wip->setFont(current);
|
||||
|
||||
@@ -392,6 +401,18 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr
|
||||
stravaAuthorised->setFixedHeight(16);
|
||||
stravaAuthorised->setFixedWidth(16);
|
||||
|
||||
cyclingAnalyticsAuthorise = new QPushButton("Authorise", this);
|
||||
#ifndef GC_CYCLINGANALYTICS_CLIENT_SECRET
|
||||
cyclingAnalyticsAuthorise->setEnabled(false);
|
||||
#endif
|
||||
|
||||
cyclingAnalyticsAuthorised = new QPushButton(this);
|
||||
cyclingAnalyticsAuthorised->setContentsMargins(0,0,0,0);
|
||||
cyclingAnalyticsAuthorised->setIcon(passwords.scaled(16,16));
|
||||
cyclingAnalyticsAuthorised->setIconSize(QSize(16,16));
|
||||
cyclingAnalyticsAuthorised->setFixedHeight(16);
|
||||
cyclingAnalyticsAuthorised->setFixedWidth(16);
|
||||
|
||||
rideWithGPSUser = new QLineEdit(this);
|
||||
rideWithGPSUser->setText(appsettings->cvalue(mainWindow->cyclist, GC_RWGPSUSER, "").toString());
|
||||
|
||||
@@ -406,6 +427,13 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr
|
||||
ttbPass->setEchoMode(QLineEdit::Password);
|
||||
ttbPass->setText(appsettings->cvalue(mainWindow->cyclist, GC_TTBPASS, "").toString());
|
||||
|
||||
selUser = new QLineEdit(this);
|
||||
selUser->setText(appsettings->cvalue(mainWindow->cyclist, GC_SELUSER, "").toString());
|
||||
|
||||
selPass = new QLineEdit(this);
|
||||
selPass->setEchoMode(QLineEdit::Password);
|
||||
selPass->setText(appsettings->cvalue(mainWindow->cyclist, GC_SELPASS, "").toString());
|
||||
|
||||
wiURL = new QLineEdit(this);
|
||||
wiURL->setText(appsettings->cvalue(mainWindow->cyclist, GC_WIURL, "http://wbsapi.withings.net/").toString());
|
||||
|
||||
@@ -455,27 +483,32 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr
|
||||
grid->addWidget(twpinLabel, 12,0);
|
||||
grid->addWidget(str, 13,0);
|
||||
grid->addWidget(strauthLabel, 14,0);
|
||||
grid->addWidget(strpinLabel, 15,0);
|
||||
grid->addWidget(rwgps, 17,0);
|
||||
grid->addWidget(rwgpsuserLabel, 18,0);
|
||||
grid->addWidget(rwgpspassLabel, 19,0);
|
||||
grid->addWidget(wip, 20,0);
|
||||
grid->addWidget(wiurlLabel, 21,0);
|
||||
grid->addWidget(wiuserLabel, 22,0);
|
||||
grid->addWidget(wipassLabel, 23,0);
|
||||
grid->addWidget(zeo, 24,0);
|
||||
grid->addWidget(zeourlLabel, 25,0);
|
||||
grid->addWidget(zeouserLabel, 26,0);
|
||||
grid->addWidget(zeopassLabel, 27,0);
|
||||
grid->addWidget(webcal, 28, 0);
|
||||
grid->addWidget(wcurlLabel, 29, 0);
|
||||
grid->addWidget(dv, 30,0);
|
||||
grid->addWidget(dvurlLabel, 31,0);
|
||||
grid->addWidget(dvuserLabel, 32,0);
|
||||
grid->addWidget(dvpassLabel, 33,0);
|
||||
grid->addWidget(ttb, 34,0);
|
||||
grid->addWidget(ttbuserLabel, 35,0);
|
||||
grid->addWidget(ttbpassLabel, 36,0);
|
||||
grid->addWidget(can, 15,0);
|
||||
grid->addWidget(canauthLabel, 16,0);
|
||||
grid->addWidget(rwgps, 18,0);
|
||||
grid->addWidget(rwgpsuserLabel, 19,0);
|
||||
grid->addWidget(rwgpspassLabel, 20,0);
|
||||
grid->addWidget(wip, 21,0);
|
||||
grid->addWidget(wiurlLabel, 22,0);
|
||||
grid->addWidget(wiuserLabel, 23,0);
|
||||
grid->addWidget(wipassLabel, 24,0);
|
||||
grid->addWidget(zeo, 25,0);
|
||||
grid->addWidget(zeourlLabel, 26,0);
|
||||
grid->addWidget(zeouserLabel, 27,0);
|
||||
grid->addWidget(zeopassLabel, 28,0);
|
||||
grid->addWidget(webcal, 29, 0);
|
||||
grid->addWidget(wcurlLabel, 30, 0);
|
||||
grid->addWidget(dv, 31,0);
|
||||
grid->addWidget(dvurlLabel, 32,0);
|
||||
grid->addWidget(dvuserLabel, 33,0);
|
||||
grid->addWidget(dvpassLabel, 34,0);
|
||||
grid->addWidget(ttb, 35,0);
|
||||
grid->addWidget(ttbuserLabel, 36,0);
|
||||
grid->addWidget(ttbpassLabel, 37,0);
|
||||
grid->addWidget(sel, 38,0);
|
||||
grid->addWidget(seluserLabel, 39,0);
|
||||
grid->addWidget(selpassLabel, 40,0);
|
||||
|
||||
|
||||
grid->addWidget(tpURL, 1, 1, 0);
|
||||
grid->addWidget(tpUser, 2, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
@@ -493,26 +526,37 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr
|
||||
grid->addWidget(stravaAuthorise, 14, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
if (appsettings->cvalue(mainWindow->cyclist, GC_STRAVA_TOKEN, "")!="")
|
||||
grid->addWidget(stravaAuthorised, 14, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
else
|
||||
stravaAuthorised->hide(); // if no token no show
|
||||
|
||||
grid->addWidget(rideWithGPSUser, 18, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(rideWithGPSPass, 19, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(cyclingAnalyticsAuthorise, 16, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
if (appsettings->cvalue(mainWindow->cyclist, GC_CYCLINGANALYTICS_TOKEN, "")!="")
|
||||
grid->addWidget(cyclingAnalyticsAuthorised, 16, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
else
|
||||
cyclingAnalyticsAuthorised->hide();
|
||||
|
||||
grid->addWidget(wiURL, 21, 1, 0);
|
||||
grid->addWidget(wiUser, 22, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(wiPass, 23, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(rideWithGPSUser, 19, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(rideWithGPSPass, 20, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
grid->addWidget(zeoURL, 25, 1, 0);
|
||||
grid->addWidget(zeoUser, 26, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(zeoPass, 27, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(wiURL, 22, 1, 0);
|
||||
grid->addWidget(wiUser, 23, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(wiPass, 24, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
grid->addWidget(webcalURL, 29, 1, 0);
|
||||
grid->addWidget(zeoURL, 26, 1, 0);
|
||||
grid->addWidget(zeoUser, 27, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(zeoPass, 28, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
grid->addWidget(dvURL, 31, 1, 0);
|
||||
grid->addWidget(dvUser, 32, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(dvPass, 33, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(webcalURL, 30, 1, 0);
|
||||
|
||||
grid->addWidget(ttbUser, 35, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(ttbPass, 36, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(dvURL, 32, 1, 0);
|
||||
grid->addWidget(dvUser, 33, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(dvPass, 34, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
grid->addWidget(ttbUser, 36, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(ttbPass, 37, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
grid->addWidget(selUser, 39, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
grid->addWidget(selPass, 40, 1, Qt::AlignLeft | Qt::AlignVCenter);
|
||||
|
||||
grid->setColumnStretch(0,0);
|
||||
grid->setColumnStretch(1,3);
|
||||
@@ -525,6 +569,7 @@ CredentialsPage::CredentialsPage(QWidget *parent, MainWindow *mainWindow) : QScr
|
||||
|
||||
connect(twitterAuthorise, SIGNAL(clicked()), this, SLOT(authoriseTwitter()));
|
||||
connect(stravaAuthorise, SIGNAL(clicked()), this, SLOT(authoriseStrava()));
|
||||
connect(cyclingAnalyticsAuthorise, SIGNAL(clicked()), this, SLOT(authoriseCyclingAnalytics()));
|
||||
}
|
||||
|
||||
void CredentialsPage::authoriseTwitter()
|
||||
@@ -600,6 +645,16 @@ void CredentialsPage::authoriseStrava()
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void CredentialsPage::authoriseCyclingAnalytics()
|
||||
{
|
||||
#ifdef GC_HAVE_LIBOAUTH
|
||||
OAuthDialog *oauthDialog = new OAuthDialog(mainWindow, OAuthDialog::CYCLING_ANALYTICS);
|
||||
oauthDialog->setWindowModality(Qt::ApplicationModal);
|
||||
oauthDialog->exec();
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
CredentialsPage::saveClicked()
|
||||
{
|
||||
@@ -615,6 +670,8 @@ CredentialsPage::saveClicked()
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_RWGPSPASS, rideWithGPSPass->text());
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_TTBUSER, ttbUser->text());
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_TTBPASS, ttbPass->text());
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_SELUSER, selUser->text());
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_SELPASS, selPass->text());
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_TPTYPE, tpType->currentIndex());
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_TWURL, twitterURL->text());
|
||||
appsettings->setCValue(mainWindow->cyclist, GC_WIURL, wiURL->text());
|
||||
|
||||
@@ -131,6 +131,7 @@ class CredentialsPage : public QScrollArea
|
||||
public slots:
|
||||
void authoriseTwitter();
|
||||
void authoriseStrava();
|
||||
void authoriseCyclingAnalytics();
|
||||
|
||||
private:
|
||||
MainWindow *mainWindow;
|
||||
@@ -158,12 +159,17 @@ class CredentialsPage : public QScrollArea
|
||||
QLineEdit *stravaPIN;
|
||||
char *s_id, *s_secret;
|
||||
|
||||
QPushButton *cyclingAnalyticsAuthorise, *cyclingAnalyticsAuthorised;
|
||||
|
||||
QLineEdit *rideWithGPSUser;
|
||||
QLineEdit *rideWithGPSPass;
|
||||
|
||||
QLineEdit *ttbUser;
|
||||
QLineEdit *ttbPass;
|
||||
|
||||
QLineEdit *selUser;
|
||||
QLineEdit *selPass;
|
||||
|
||||
QLineEdit *wiURL; // url for withings
|
||||
QLineEdit *wiUser;
|
||||
QLineEdit *wiPass;
|
||||
|
||||
@@ -104,6 +104,8 @@
|
||||
#define GC_RWGPSPASS "rwgps/pass"
|
||||
#define GC_TTBUSER "ttb/user"
|
||||
#define GC_TTBPASS "ttb/pass"
|
||||
#define GC_SELUSER "sel/user"
|
||||
#define GC_SELPASS "sel/pass"
|
||||
#define GC_WIURL "wi/url"
|
||||
#define GC_WIUSER "wi/user"
|
||||
#define GC_WIKEY "wi/key"
|
||||
@@ -154,6 +156,10 @@
|
||||
#define GC_STRAVA_CLIENT_ID "83" //< client id
|
||||
#define GC_STRAVA_TOKEN "strava_token"
|
||||
|
||||
//Cycling Analytics
|
||||
#define GC_CYCLINGANALYTICS_CLIENT_ID "1504958" // app id
|
||||
#define GC_CYCLINGANALYTICS_TOKEN "cyclinganalytics_token"
|
||||
|
||||
// Tcx Smart recording
|
||||
#define GC_GARMIN_SMARTRECORD "garminSmartRecord"
|
||||
#define GC_GARMIN_HWMARK "garminHWMark"
|
||||
|
||||
@@ -31,6 +31,43 @@
|
||||
#include "DBAccess.h"
|
||||
#include "TcxRideFile.h"
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
// Utility function to create a QByteArray of data in GZIP format
|
||||
// This is essentially the same as qCompress but creates it in
|
||||
// GZIP format (with recquisite headers) instead of ZLIB's format
|
||||
// which has less filename info in the header
|
||||
//
|
||||
static QByteArray zCompress(const QByteArray &source)
|
||||
{
|
||||
// int size is source.size()
|
||||
// const char *data is source.data()
|
||||
z_stream strm;
|
||||
|
||||
strm.zalloc = Z_NULL;
|
||||
strm.zfree = Z_NULL;
|
||||
strm.opaque = Z_NULL;
|
||||
|
||||
// note that (15+16) below means windowbits+_16_ adds the gzip header/footer
|
||||
deflateInit2(&strm, Z_BEST_COMPRESSION, Z_DEFLATED, (15+16), 8, Z_DEFAULT_STRATEGY);
|
||||
|
||||
// input data
|
||||
strm.avail_in = source.size();
|
||||
strm.next_in = (Bytef *)source.data();
|
||||
|
||||
// output data - on stack not heap, will be released
|
||||
QByteArray dest(source.size()/2, '\0'); // should compress by 50%, if not don't bother
|
||||
|
||||
strm.avail_out = source.size()/2;
|
||||
strm.next_out = (Bytef *)dest.data();
|
||||
|
||||
// now compress!
|
||||
deflate(&strm, Z_FINISH);
|
||||
|
||||
// return byte array on the stack
|
||||
return QByteArray(dest.data(), (source.size()/2) - strm.avail_out);
|
||||
}
|
||||
|
||||
ShareDialog::ShareDialog(MainWindow *mainWindow, RideItem *item) :
|
||||
mainWindow(mainWindow)
|
||||
{
|
||||
@@ -45,6 +82,8 @@ ShareDialog::ShareDialog(MainWindow *mainWindow, RideItem *item) :
|
||||
// uploaders
|
||||
stravaUploader = new StravaUploader(mainWindow, ride, this);
|
||||
rideWithGpsUploader = new RideWithGpsUploader(mainWindow, ride, this);
|
||||
cyclingAnalyticsUploader = new CyclingAnalyticsUploader(mainWindow, ride, this);
|
||||
selfLoopsUploader = new SelfLoopsUploader(mainWindow, ride, this);
|
||||
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
QGroupBox *groupBox1 = new QGroupBox(tr("Choose which sites you wish to share on: "));
|
||||
@@ -54,10 +93,17 @@ ShareDialog::ShareDialog(MainWindow *mainWindow, RideItem *item) :
|
||||
stravaChk->setEnabled(false);
|
||||
#endif
|
||||
rideWithGPSChk = new QCheckBox(tr("Ride With GPS"));
|
||||
cyclingAnalyticsChk = new QCheckBox(tr("Cycling Analytics"));
|
||||
#ifndef GC_CYCLINGANALYTICS_CLIENT_SECRET
|
||||
cyclingAnalyticsChk->setEnabled(false);
|
||||
#endif
|
||||
selfLoopsChk = new QCheckBox(tr("Selfloops"));
|
||||
|
||||
QGridLayout *vbox1 = new QGridLayout();
|
||||
vbox1->addWidget(stravaChk,0,0);
|
||||
vbox1->addWidget(rideWithGPSChk,0,2);
|
||||
vbox1->addWidget(rideWithGPSChk,0,1);
|
||||
vbox1->addWidget(cyclingAnalyticsChk,0,2);
|
||||
vbox1->addWidget(selfLoopsChk,0,3);
|
||||
|
||||
groupBox1->setLayout(vbox1);
|
||||
mainLayout->addWidget(groupBox1);
|
||||
@@ -131,7 +177,8 @@ ShareDialog::upload()
|
||||
{
|
||||
show();
|
||||
|
||||
if (!stravaChk->isChecked() && !rideWithGPSChk->isChecked()) {
|
||||
if (!stravaChk->isChecked() && !rideWithGPSChk->isChecked() &&
|
||||
!cyclingAnalyticsChk->isChecked() && !selfLoopsChk->isChecked()) {
|
||||
QMessageBox aMsgBox;
|
||||
aMsgBox.setText(tr("No share site selected !"));
|
||||
aMsgBox.exec();
|
||||
@@ -146,18 +193,28 @@ ShareDialog::upload()
|
||||
if (stravaChk->isChecked()) {
|
||||
shareSiteCount ++;
|
||||
}
|
||||
|
||||
if (rideWithGPSChk->isChecked()) {
|
||||
shareSiteCount ++;
|
||||
}
|
||||
if (cyclingAnalyticsChk->isChecked()) {
|
||||
shareSiteCount ++;
|
||||
}
|
||||
if (selfLoopsChk->isChecked()) {
|
||||
shareSiteCount ++;
|
||||
}
|
||||
|
||||
if (stravaChk->isChecked()) {
|
||||
stravaUploader->uploadStrava();
|
||||
}
|
||||
|
||||
if (rideWithGPSChk->isChecked()) {
|
||||
rideWithGpsUploader->uploadRideWithGPS();
|
||||
}
|
||||
if (cyclingAnalyticsChk->isChecked()) {
|
||||
cyclingAnalyticsUploader->upload();
|
||||
}
|
||||
if (selfLoopsChk->isChecked()) {
|
||||
selfLoopsUploader->upload();
|
||||
}
|
||||
}
|
||||
|
||||
StravaUploader::StravaUploader(MainWindow *mainWindow, RideItem *ride, ShareDialog *parent) :
|
||||
@@ -633,4 +690,347 @@ RideWithGpsUploader::closeClicked()
|
||||
}
|
||||
|
||||
|
||||
CyclingAnalyticsUploader::CyclingAnalyticsUploader(MainWindow *mainWindow, RideItem *ride, ShareDialog *parent) :
|
||||
mainWindow(mainWindow), ride(ride), parent(parent)
|
||||
{
|
||||
cyclingAnalyticsUploadId = ride->ride()->getTag("CyclingAnalytics uploadId", "");
|
||||
}
|
||||
|
||||
void
|
||||
CyclingAnalyticsUploader::upload()
|
||||
{
|
||||
// OAuth no more login
|
||||
token = appsettings->cvalue(mainWindow->cyclist, GC_CYCLINGANALYTICS_TOKEN, "").toString();
|
||||
if (token=="")
|
||||
{
|
||||
QMessageBox aMsgBox;
|
||||
aMsgBox.setText(tr("Cannot login to CyclingAnalytics. Check permission"));
|
||||
aMsgBox.exec();
|
||||
return;
|
||||
}
|
||||
|
||||
// already shared ?
|
||||
if(cyclingAnalyticsUploadId.length()>0)
|
||||
{
|
||||
overwrite = false;
|
||||
|
||||
dialog = new QDialog();
|
||||
QVBoxLayout *layout = new QVBoxLayout;
|
||||
|
||||
QVBoxLayout *layoutLabel = new QVBoxLayout();
|
||||
QLabel *label = new QLabel();
|
||||
label->setText(tr("This Ride is marked as already on CyclingAnalytics. Are you sure you want to upload it?"));
|
||||
layoutLabel->addWidget(label);
|
||||
|
||||
QPushButton *ok = new QPushButton(tr("OK"), dialog);
|
||||
QPushButton *cancel = new QPushButton(tr("Cancel"), dialog);
|
||||
QHBoxLayout *buttons = new QHBoxLayout();
|
||||
buttons->addStretch();
|
||||
buttons->addWidget(cancel);
|
||||
buttons->addWidget(ok);
|
||||
|
||||
connect(ok, SIGNAL(clicked()), this, SLOT(okClicked()));
|
||||
connect(cancel, SIGNAL(clicked()), this, SLOT(closeClicked()));
|
||||
|
||||
layout->addLayout(layoutLabel);
|
||||
layout->addLayout(buttons);
|
||||
|
||||
dialog->setLayout(layout);
|
||||
|
||||
if (!dialog->exec()) return;
|
||||
}
|
||||
|
||||
requestUploadCyclingAnalytics();
|
||||
|
||||
if(!uploadSuccessful)
|
||||
{
|
||||
parent->progressLabel->setText("Error uploading to CyclingAnalytics");
|
||||
}
|
||||
else
|
||||
{
|
||||
parent->progressLabel->setText(tr("Successfully uploaded to CyclingAnalytics"));
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CyclingAnalyticsUploader::requestUploadCyclingAnalytics()
|
||||
{
|
||||
parent->progressLabel->setText(tr("Upload ride to CyclingAnalytics..."));
|
||||
parent->progressBar->setValue(parent->progressBar->value()+10/parent->shareSiteCount);
|
||||
|
||||
QEventLoop eventLoop;
|
||||
QNetworkAccessManager networkMgr;
|
||||
|
||||
connect(&networkMgr, SIGNAL(finished(QNetworkReply*)), this, SLOT(requestUploadCyclingAnalyticsFinished(QNetworkReply*)));
|
||||
connect(&networkMgr, SIGNAL(finished(QNetworkReply *)), &eventLoop, SLOT(quit()));
|
||||
|
||||
QUrl url = QUrl( "https://www.cyclinganalytics.com/api/me/upload" );
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
|
||||
QString boundary = QVariant(qrand()).toString()+QVariant(qrand()).toString()+QVariant(qrand()).toString();
|
||||
|
||||
TcxFileReader reader;
|
||||
QByteArray file = reader.toByteArray(mainWindow, ride->ride(), parent->altitudeChk->isChecked(), parent->powerChk->isChecked(), parent->heartrateChk->isChecked(), parent->cadenceChk->isChecked());
|
||||
|
||||
// MULTIPART *****************
|
||||
|
||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||
multiPart->setBoundary(boundary.toAscii());
|
||||
|
||||
request.setRawHeader("Authorization", (QString("Bearer %1").arg(token)).toAscii());
|
||||
|
||||
QHttpPart activityNamePart;
|
||||
activityNamePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"title\""));
|
||||
activityNamePart.setBody(QString(parent->titleEdit->text()).toAscii());
|
||||
|
||||
QHttpPart dataTypePart;
|
||||
dataTypePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"format\""));
|
||||
dataTypePart.setBody("tcx");
|
||||
|
||||
QHttpPart filenamePart;
|
||||
filenamePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"filename\""));
|
||||
filenamePart.setBody("file.tcx");
|
||||
|
||||
QHttpPart filePart;
|
||||
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"data\"; filename=\"file.tcx\"; type=\"text/xml\""));
|
||||
filePart.setBody(file);
|
||||
|
||||
multiPart->append(activityNamePart);
|
||||
multiPart->append(filenamePart);
|
||||
multiPart->append(dataTypePart);
|
||||
multiPart->append(filePart);
|
||||
|
||||
QScopedPointer<QNetworkReply> reply( networkMgr.post(request, multiPart) );
|
||||
multiPart->setParent(reply.data());
|
||||
|
||||
parent->progressBar->setValue(parent->progressBar->value()+30/parent->shareSiteCount);
|
||||
parent->progressLabel->setText(tr("Upload ride... Sending to CyclingAnalytics"));
|
||||
|
||||
eventLoop.exec();
|
||||
}
|
||||
|
||||
void
|
||||
CyclingAnalyticsUploader::requestUploadCyclingAnalyticsFinished(QNetworkReply *reply)
|
||||
{
|
||||
parent->progressBar->setValue(parent->progressBar->value()+50/parent->shareSiteCount);
|
||||
parent->progressLabel->setText(tr("Upload to CyclingAnalytics finished."));
|
||||
|
||||
uploadSuccessful = false;
|
||||
|
||||
QString response = reply->readAll();
|
||||
//qDebug() << "response" << response;
|
||||
|
||||
QScriptValue sc;
|
||||
QScriptEngine se;
|
||||
|
||||
sc = se.evaluate("("+response+")");
|
||||
QString uploadError = sc.property("error").toString();
|
||||
if (uploadError.toLower() == "none" || uploadError.toLower() == "null")
|
||||
uploadError = "";
|
||||
|
||||
if (uploadError.length()>0 || reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
//qDebug() << "Error " << reply->error() ;
|
||||
//qDebug() << "Error " << uploadError;
|
||||
parent->errorLabel->setText(parent->errorLabel->text()+ tr(" Error from CyclingAnalytics: ") + uploadError + "\n" );
|
||||
}
|
||||
else
|
||||
{
|
||||
cyclingAnalyticsUploadId = sc.property("upload_id").toString();
|
||||
|
||||
ride->ride()->setTag("CyclingAnalytics uploadId", cyclingAnalyticsUploadId);
|
||||
ride->setDirty(true);
|
||||
|
||||
//qDebug() << "uploadId: " << cyclingAnalyticsUploadId;
|
||||
|
||||
parent->progressBar->setValue(parent->progressBar->value()+10/parent->shareSiteCount);
|
||||
uploadSuccessful = true;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
CyclingAnalyticsUploader::okClicked()
|
||||
{
|
||||
dialog->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
void
|
||||
CyclingAnalyticsUploader::closeClicked()
|
||||
{
|
||||
dialog->reject();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
SelfLoopsUploader::SelfLoopsUploader(MainWindow *mainWindow, RideItem *ride, ShareDialog *parent) :
|
||||
mainWindow(mainWindow), ride(ride), parent(parent)
|
||||
{
|
||||
selfloopsUploadId = ride->ride()->getTag("Selfloops uploadId", "");
|
||||
selfloopsActivityId = ride->ride()->getTag("Selfloops activityId", "");
|
||||
}
|
||||
|
||||
void
|
||||
SelfLoopsUploader::upload()
|
||||
{
|
||||
// allready shared ?
|
||||
if(selfloopsActivityId.length()>0)
|
||||
{
|
||||
overwrite = false;
|
||||
|
||||
dialog = new QDialog();
|
||||
QVBoxLayout *layout = new QVBoxLayout;
|
||||
|
||||
QVBoxLayout *layoutLabel = new QVBoxLayout();
|
||||
QLabel *label = new QLabel();
|
||||
label->setText(tr("This Ride is marked as already on Selfloops. Are you sure you want to upload it?"));
|
||||
layoutLabel->addWidget(label);
|
||||
|
||||
QPushButton *ok = new QPushButton(tr("OK"), dialog);
|
||||
QPushButton *cancel = new QPushButton(tr("Cancel"), dialog);
|
||||
QHBoxLayout *buttons = new QHBoxLayout();
|
||||
buttons->addStretch();
|
||||
buttons->addWidget(cancel);
|
||||
buttons->addWidget(ok);
|
||||
|
||||
connect(ok, SIGNAL(clicked()), this, SLOT(okClicked()));
|
||||
connect(cancel, SIGNAL(clicked()), this, SLOT(closeClicked()));
|
||||
|
||||
layout->addLayout(layoutLabel);
|
||||
layout->addLayout(buttons);
|
||||
|
||||
dialog->setLayout(layout);
|
||||
|
||||
if (!dialog->exec()) return;
|
||||
}
|
||||
|
||||
requestUploadSelfLoops();
|
||||
|
||||
if(!uploadSuccessful)
|
||||
{
|
||||
parent->progressLabel->setText("Error uploading to Selfloops");
|
||||
}
|
||||
else
|
||||
{
|
||||
parent->progressLabel->setText(tr("Successfully uploaded to Selfloops"));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
make a multipart HTTP POST at the following path "/restapi/public/activities/upload.json"
|
||||
on selflloops web site using SSL.
|
||||
The requested parameters are:
|
||||
- "email" the email of a valid SelfLoops account
|
||||
- "pw" the password
|
||||
- "tcxfile" the zipped TCX file (example: test.tcx.gz).
|
||||
|
||||
On success, response message contains a JSON encoded data
|
||||
with the new "activity_id" created.
|
||||
On error, SelfLoops response contains a JSON encoded data with "error_code" and "message" key.
|
||||
*/
|
||||
void
|
||||
SelfLoopsUploader::requestUploadSelfLoops()
|
||||
{
|
||||
parent->progressLabel->setText(tr("Upload ride to Selfloops..."));
|
||||
parent->progressBar->setValue(parent->progressBar->value()+10/parent->shareSiteCount);
|
||||
|
||||
QEventLoop eventLoop;
|
||||
QNetworkAccessManager networkMgr;
|
||||
|
||||
connect(&networkMgr, SIGNAL(finished(QNetworkReply*)), this, SLOT(requestUploadSelfLoopsFinished(QNetworkReply*)));
|
||||
connect(&networkMgr, SIGNAL(finished(QNetworkReply *)), &eventLoop, SLOT(quit()));
|
||||
|
||||
QUrl url = QUrl( "https://www.selfloops.com/restapi/public/activities/upload.json" );
|
||||
QNetworkRequest request = QNetworkRequest(url);
|
||||
|
||||
QString boundary = QVariant(qrand()).toString()+QVariant(qrand()).toString()+QVariant(qrand()).toString();
|
||||
|
||||
// The TCX file have to be gzipped
|
||||
TcxFileReader reader;
|
||||
QByteArray file = zCompress(reader.toByteArray(mainWindow, ride->ride(), parent->altitudeChk->isChecked(), parent->powerChk->isChecked(), parent->heartrateChk->isChecked(), parent->cadenceChk->isChecked()));
|
||||
|
||||
QString username = appsettings->cvalue(mainWindow->cyclist, GC_SELUSER).toString();
|
||||
QString password = appsettings->cvalue(mainWindow->cyclist, GC_SELPASS).toString();
|
||||
|
||||
// MULTIPART *****************
|
||||
|
||||
QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::MixedType);
|
||||
multiPart->setBoundary(boundary.toAscii());
|
||||
|
||||
QHttpPart emailPart;
|
||||
emailPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"email\""));
|
||||
emailPart.setBody(username.toAscii());
|
||||
|
||||
QHttpPart passwordPart;
|
||||
passwordPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"pw\""));
|
||||
passwordPart.setBody(password.toAscii());
|
||||
|
||||
QHttpPart filePart;
|
||||
filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"tcxfile\"; filename=\"myfile.tcx.gz\"; type=\"application/x-gzip\""));
|
||||
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-gzip");
|
||||
filePart.setBody(file);
|
||||
|
||||
multiPart->append(emailPart);
|
||||
multiPart->append(passwordPart);
|
||||
multiPart->append(filePart);
|
||||
|
||||
QScopedPointer<QNetworkReply> reply( networkMgr.post(request, multiPart) );
|
||||
multiPart->setParent(reply.data());
|
||||
|
||||
parent->progressBar->setValue(parent->progressBar->value()+30/parent->shareSiteCount);
|
||||
parent->progressLabel->setText(tr("Upload ride... Sending to Selfloops"));
|
||||
|
||||
eventLoop.exec();
|
||||
}
|
||||
|
||||
void
|
||||
SelfLoopsUploader::requestUploadSelfLoopsFinished(QNetworkReply *reply)
|
||||
{
|
||||
parent->progressBar->setValue(parent->progressBar->value()+50/parent->shareSiteCount);
|
||||
parent->progressLabel->setText(tr("Upload to Selfloops finished."));
|
||||
|
||||
uploadSuccessful = false;
|
||||
|
||||
QString response = reply->readAll();
|
||||
qDebug() << "response" << response;
|
||||
|
||||
QScriptValue sc;
|
||||
QScriptEngine se;
|
||||
|
||||
sc = se.evaluate("("+response+")");
|
||||
QString error = sc.property("error_code").toString();
|
||||
QString uploadError = sc.property("message").toString();
|
||||
|
||||
if (error.length()>0 || reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
qDebug() << "Error " << reply->error() ;
|
||||
qDebug() << "Error " << uploadError;
|
||||
parent->errorLabel->setText(parent->errorLabel->text()+ tr(" Error from Selfloops: ") + uploadError + "\n" );
|
||||
}
|
||||
else
|
||||
{
|
||||
selfloopsActivityId = sc.property("activity_id").toString();
|
||||
|
||||
ride->ride()->setTag("Selfloops activityId", selfloopsActivityId);
|
||||
ride->setDirty(true);
|
||||
|
||||
qDebug() << "uploadId: " << selfloopsActivityId;
|
||||
|
||||
parent->progressBar->setValue(parent->progressBar->value()+10/parent->shareSiteCount);
|
||||
uploadSuccessful = true;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SelfLoopsUploader::okClicked()
|
||||
{
|
||||
dialog->accept();
|
||||
return;
|
||||
}
|
||||
|
||||
void
|
||||
SelfLoopsUploader::closeClicked()
|
||||
{
|
||||
dialog->reject();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,77 @@ private:
|
||||
bool overwrite;
|
||||
};
|
||||
|
||||
|
||||
// uploader to cyclinganalytics.com
|
||||
class CyclingAnalyticsUploader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
G_OBJECT
|
||||
|
||||
public:
|
||||
CyclingAnalyticsUploader(MainWindow *mainWindow, RideItem *item, ShareDialog *parent = 0);
|
||||
|
||||
void upload();
|
||||
|
||||
private slots:
|
||||
void requestUploadCyclingAnalytics();
|
||||
void requestUploadCyclingAnalyticsFinished(QNetworkReply *reply);
|
||||
|
||||
void okClicked();
|
||||
void closeClicked();
|
||||
|
||||
private:
|
||||
MainWindow *mainWindow;
|
||||
ShareDialog *parent;
|
||||
RideItem *ride;
|
||||
QDialog *dialog;
|
||||
|
||||
QString token;
|
||||
|
||||
bool loggedIn, uploadSuccessful;
|
||||
bool overwrite;
|
||||
|
||||
QString cyclingAnalyticsUploadId;
|
||||
|
||||
QString uploadStatus;
|
||||
QString uploadProgress;
|
||||
};
|
||||
|
||||
// uploader to selfloops.com
|
||||
class SelfLoopsUploader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
G_OBJECT
|
||||
|
||||
public:
|
||||
SelfLoopsUploader(MainWindow *mainWindow, RideItem *item, ShareDialog *parent = 0);
|
||||
|
||||
void upload();
|
||||
|
||||
private slots:
|
||||
void requestUploadSelfLoops();
|
||||
void requestUploadSelfLoopsFinished(QNetworkReply *reply);
|
||||
|
||||
void okClicked();
|
||||
void closeClicked();
|
||||
|
||||
private:
|
||||
MainWindow *mainWindow;
|
||||
ShareDialog *parent;
|
||||
RideItem *ride;
|
||||
QDialog *dialog;
|
||||
|
||||
QString token;
|
||||
|
||||
bool loggedIn, uploadSuccessful;
|
||||
bool overwrite;
|
||||
|
||||
QString selfloopsUploadId, selfloopsActivityId;
|
||||
|
||||
QString uploadStatus;
|
||||
QString uploadProgress;
|
||||
};
|
||||
|
||||
class ShareDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -141,11 +212,15 @@ private:
|
||||
|
||||
QCheckBox *stravaChk;
|
||||
QCheckBox *rideWithGPSChk;
|
||||
QCheckBox *cyclingAnalyticsChk;
|
||||
QCheckBox *selfLoopsChk;
|
||||
|
||||
RideItem *ride;
|
||||
|
||||
StravaUploader *stravaUploader;
|
||||
RideWithGpsUploader *rideWithGpsUploader;
|
||||
CyclingAnalyticsUploader *cyclingAnalyticsUploader;
|
||||
SelfLoopsUploader *selfLoopsUploader;
|
||||
|
||||
QString athleteId;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user