mirror of
https://github.com/tomahawk-player/tomahawk.git
synced 2025-08-12 09:04:33 +02:00
Add hatchet plugin to master
This commit is contained in:
@@ -37,6 +37,7 @@ add_definitions( "-DQT_STRICT_ITERATORS" )
|
||||
option(BUILD_GUI "Build Tomahawk with GUI" ON)
|
||||
option(BUILD_RELEASE "Generate TOMAHAWK_VERSION without GIT info" OFF)
|
||||
option(BUILD_TESTS "Build Tomahawk with unit tests" ON)
|
||||
option(BUILD_HATCHET "Build the Hatchet plugin" OFF)
|
||||
|
||||
option(WITH_BREAKPAD "Build with breakpad integration" ON)
|
||||
option(WITH_CRASHREPORTER "Build with CrashReporter" ON)
|
||||
|
@@ -3,16 +3,17 @@ include( ${CMAKE_CURRENT_LIST_DIR}/../../TomahawkAddPlugin.cmake )
|
||||
file(GLOB SUBDIRECTORIES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "*")
|
||||
foreach(SUBDIRECTORY ${SUBDIRECTORIES})
|
||||
if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIRECTORY}" AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${SUBDIRECTORY}/CMakeLists.txt")
|
||||
if(SUBDIRECTORY STREQUAL "xmpp")
|
||||
if(SUBDIRECTORY STREQUAL "twitter")
|
||||
elseif(SUBDIRECTORY STREQUAL "xmpp")
|
||||
if( JREEN_FOUND )
|
||||
add_subdirectory( xmpp )
|
||||
endif()
|
||||
elseif(SUBDIRECTORY STREQUAL "twitter")
|
||||
if(QTWEETLIB_FOUND AND BUILD_GUI)
|
||||
add_subdirectory( twitter )
|
||||
elseif(SUBDIRECTORY STREQUAL "hatchet")
|
||||
if(BUILD_HATCHET AND BUILD_GUI)
|
||||
add_subdirectory(hatchet)
|
||||
endif()
|
||||
else()
|
||||
add_subdirectory( ${SUBDIRECTORY} )
|
||||
add_subdirectory(${SUBDIRECTORY})
|
||||
endif()
|
||||
endif()
|
||||
endforeach()
|
||||
|
52
src/accounts/hatchet/CMakeLists.txt
Normal file
52
src/accounts/hatchet/CMakeLists.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
cmake_minimum_required(VERSION 2.8)
|
||||
CMAKE_POLICY(SET CMP0017 NEW)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake/Modules")
|
||||
|
||||
if(NOT TOMAHAWK_LIBRARIES)
|
||||
message(STATUS "BUILDING OUTSIDE TOMAHAWK")
|
||||
find_package(Tomahawk REQUIRED)
|
||||
else()
|
||||
message(STATUS "BUILDING INSIDE TOMAHAWK")
|
||||
set(TOMAHAWK_USE_FILE "${CMAKE_SOURCE_DIR}/TomahawkUse.cmake")
|
||||
endif()
|
||||
include( ${TOMAHAWK_USE_FILE} )
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(QCA2 REQUIRED)
|
||||
find_package(websocketpp 0.2.99 REQUIRED)
|
||||
|
||||
include_directories(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${WEBSOCKETPP_INCLUDE_DIR}
|
||||
${TOMAHAWK_INCLUDE_DIRS}
|
||||
${QCA2_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
add_definitions(-D_WEBSOCKETPP_CPP11_STL_)
|
||||
|
||||
if(APPLE)
|
||||
# http://stackoverflow.com/questions/7226753/osx-lion-xcode-4-1-how-do-i-setup-a-c0x-project/7236451#7236451
|
||||
add_definitions(-std=c++11 -stdlib=libc++ -U__STRICT_ANSI__)
|
||||
set(PLATFORM_SPECIFIC_LINK_LIBRARIES "/usr/lib/libc++.dylib")
|
||||
else()
|
||||
add_definitions(-std=c++0x)
|
||||
endif()
|
||||
|
||||
tomahawk_add_plugin(hatchet
|
||||
TYPE account
|
||||
EXPORT_MACRO ACCOUNTDLLEXPORT_PRO
|
||||
SOURCES
|
||||
account/HatchetAccount.cpp
|
||||
account/HatchetAccountConfig.cpp
|
||||
sip/WebSocket.cpp
|
||||
sip/WebSocketThreadController.cpp
|
||||
sip/HatchetSip.cpp
|
||||
UI
|
||||
account/HatchetAccountConfig.ui
|
||||
LINK_LIBRARIES
|
||||
${TOMAHAWK_LIBRARIES}
|
||||
${QCA2_LIBRARIES}
|
||||
${PLATFORM_SPECIFIC_LINK_LIBRARIES}
|
||||
)
|
||||
|
417
src/accounts/hatchet/account/HatchetAccount.cpp
Normal file
417
src/accounts/hatchet/account/HatchetAccount.cpp
Normal file
@@ -0,0 +1,417 @@
|
||||
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
|
||||
*
|
||||
* Copyright 2012 Leo Franchi <lfranchi@kde.org>
|
||||
*
|
||||
* Tomahawk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tomahawk is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "HatchetAccount.h"
|
||||
|
||||
#include "HatchetAccountConfig.h"
|
||||
#include "utils/Closure.h"
|
||||
#include "utils/Logger.h"
|
||||
#include "sip/HatchetSip.h"
|
||||
#include "utils/TomahawkUtils.h"
|
||||
|
||||
#include <QtPlugin>
|
||||
#include <QFile>
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QUrl>
|
||||
#include <QUuid>
|
||||
|
||||
#include <qjson/parser.h>
|
||||
#include <qjson/serializer.h>
|
||||
|
||||
using namespace Tomahawk;
|
||||
using namespace Accounts;
|
||||
|
||||
static QPixmap* s_icon = 0;
|
||||
HatchetAccount* HatchetAccount::s_instance = 0;
|
||||
|
||||
const QString c_loginServer("https://mandella.hatchet.is/v1");
|
||||
const QString c_accessTokenServer("https://mandella.hatchet.is/v1");
|
||||
|
||||
HatchetAccountFactory::HatchetAccountFactory()
|
||||
{
|
||||
#ifndef ENABLE_HEADLESS
|
||||
if ( s_icon == 0 )
|
||||
s_icon = new QPixmap( ":/hatchet-account/hatchet-icon-512x512.png" );
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
HatchetAccountFactory::~HatchetAccountFactory()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
QPixmap
|
||||
HatchetAccountFactory::icon() const
|
||||
{
|
||||
return *s_icon;
|
||||
}
|
||||
|
||||
|
||||
Account*
|
||||
HatchetAccountFactory::createAccount( const QString& pluginId )
|
||||
{
|
||||
return new HatchetAccount( pluginId.isEmpty() ? generateId( factoryId() ) : pluginId );
|
||||
}
|
||||
|
||||
|
||||
// Hatchet account
|
||||
|
||||
HatchetAccount::HatchetAccount( const QString& accountId )
|
||||
: Account( accountId )
|
||||
, m_publicKey( nullptr )
|
||||
{
|
||||
s_instance = this;
|
||||
|
||||
QFile pemFile( ":/hatchet-account/mandella.pem" );
|
||||
pemFile.open( QIODevice::ReadOnly );
|
||||
tDebug() << Q_FUNC_INFO << "certs/mandella.pem: " << pemFile.readAll();
|
||||
pemFile.close();
|
||||
pemFile.open( QIODevice::ReadOnly );
|
||||
QCA::ConvertResult conversionResult;
|
||||
QCA::PublicKey publicKey = QCA::PublicKey::fromPEM(pemFile.readAll(), &conversionResult);
|
||||
if ( QCA::ConvertGood != conversionResult )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "INVALID PUBKEY READ";
|
||||
return;
|
||||
}
|
||||
m_publicKey = new QCA::PublicKey( publicKey );
|
||||
}
|
||||
|
||||
|
||||
HatchetAccount::~HatchetAccount()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
HatchetAccount*
|
||||
HatchetAccount::instance()
|
||||
{
|
||||
return s_instance;
|
||||
}
|
||||
|
||||
|
||||
AccountConfigWidget*
|
||||
HatchetAccount::configurationWidget()
|
||||
{
|
||||
if ( m_configWidget.isNull() )
|
||||
m_configWidget = QWeakPointer<HatchetAccountConfig>( new HatchetAccountConfig( this ) );
|
||||
|
||||
return m_configWidget.data();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccount::authenticate()
|
||||
{
|
||||
if ( connectionState() == Connected )
|
||||
return;
|
||||
|
||||
if ( !authToken().isEmpty() )
|
||||
{
|
||||
qDebug() << "Have saved credentials with auth token:" << authToken();
|
||||
if ( sipPlugin() )
|
||||
sipPlugin()->connectPlugin();
|
||||
}
|
||||
else if ( !username().isEmpty() )
|
||||
{
|
||||
// Need to re-prompt for password, since we don't save it!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccount::deauthenticate()
|
||||
{
|
||||
if ( !m_tomahawkSipPlugin.isNull() )
|
||||
sipPlugin()->disconnectPlugin();
|
||||
emit deauthenticated();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccount::setConnectionState( Account::ConnectionState connectionState )
|
||||
{
|
||||
m_state = connectionState;
|
||||
|
||||
emit connectionStateChanged( connectionState );
|
||||
}
|
||||
|
||||
|
||||
Account::ConnectionState
|
||||
HatchetAccount::connectionState() const
|
||||
{
|
||||
return m_state;
|
||||
}
|
||||
|
||||
|
||||
SipPlugin*
|
||||
HatchetAccount::sipPlugin()
|
||||
{
|
||||
if ( m_tomahawkSipPlugin.isNull() )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO;
|
||||
m_tomahawkSipPlugin = QWeakPointer< HatchetSipPlugin >( new HatchetSipPlugin( this ) );
|
||||
connect( m_tomahawkSipPlugin.data(), SIGNAL( authUrlDiscovered( Tomahawk::Accounts::HatchetAccount::Service, QString ) ),
|
||||
this, SLOT( authUrlDiscovered( Tomahawk::Accounts::HatchetAccount::Service, QString ) ) );
|
||||
|
||||
return m_tomahawkSipPlugin.data();
|
||||
}
|
||||
return m_tomahawkSipPlugin.data();
|
||||
}
|
||||
|
||||
|
||||
QPixmap
|
||||
HatchetAccount::icon() const
|
||||
{
|
||||
return *s_icon;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
HatchetAccount::isAuthenticated() const
|
||||
{
|
||||
return credentials().contains( "authtoken" );
|
||||
}
|
||||
|
||||
|
||||
QString
|
||||
HatchetAccount::username() const
|
||||
{
|
||||
return credentials().value( "username" ).toString();
|
||||
}
|
||||
|
||||
|
||||
QByteArray
|
||||
HatchetAccount::authToken() const
|
||||
{
|
||||
return credentials().value( "authtoken" ).toByteArray();
|
||||
}
|
||||
|
||||
|
||||
uint
|
||||
HatchetAccount::authTokenExpiration() const
|
||||
{
|
||||
bool ok;
|
||||
return credentials().value( "expiration" ).toUInt( &ok );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccount::loginWithPassword( const QString& username, const QString& password, const QString &otp )
|
||||
{
|
||||
if ( username.isEmpty() || password.isEmpty() || !m_publicKey )
|
||||
{
|
||||
tLog() << "No tomahawk account username or pw or public key, not logging in";
|
||||
return;
|
||||
}
|
||||
|
||||
m_uuid = QUuid::createUuid().toString();
|
||||
QCA::SecureArray sa( m_uuid.toLatin1() );
|
||||
QCA::SecureArray result = m_publicKey->encrypt( sa, QCA::EME_PKCS1_OAEP );
|
||||
|
||||
QVariantMap params;
|
||||
params[ "password" ] = password;
|
||||
params[ "username" ] = username;
|
||||
if ( !otp.isEmpty() )
|
||||
params[ "otp" ] = otp;
|
||||
params[ "client" ] = "Tomahawk (" + QHostInfo::localHostName() + ")";
|
||||
params[ "nonce" ] = QString( result.toByteArray().toBase64() );
|
||||
|
||||
QJson::Serializer s;
|
||||
const QByteArray msgJson = s.serialize( params );
|
||||
|
||||
QNetworkRequest req( QUrl( c_loginServer + "/auth/credentials") );
|
||||
req.setHeader( QNetworkRequest::ContentTypeHeader, "application/json; charset=utf-8" );
|
||||
QNetworkReply* reply = TomahawkUtils::nam()->post( req, msgJson );
|
||||
|
||||
NewClosure( reply, SIGNAL( finished() ), this, SLOT( onPasswordLoginFinished( QNetworkReply*, const QString& ) ), reply, username );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccount::fetchAccessTokens( const QString& type )
|
||||
{
|
||||
if ( username().isEmpty() || authToken().isEmpty() )
|
||||
{
|
||||
tLog() << "No authToken, not logging in";
|
||||
return;
|
||||
}
|
||||
|
||||
if ( authTokenExpiration() < ( QDateTime::currentMSecsSinceEpoch() / 1000 ) )
|
||||
tLog() << "Auth token has expired, but may still be valid on the server";
|
||||
|
||||
tLog() << "Fetching access tokens";
|
||||
QNetworkRequest req( QUrl( c_accessTokenServer + "/tokens/" + type + "?authtoken=" + authToken() ) );
|
||||
|
||||
QNetworkReply* reply = TomahawkUtils::nam()->get( req );
|
||||
|
||||
connect( reply, SIGNAL( finished() ), this, SLOT( onFetchAccessTokensFinished() ) );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccount::onPasswordLoginFinished( QNetworkReply* reply, const QString& username )
|
||||
{
|
||||
Q_ASSERT( reply );
|
||||
bool ok;
|
||||
const QVariantMap resp = parseReply( reply, ok );
|
||||
if ( !ok )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Error getting parsed reply from auth server";
|
||||
emit authError( "An error occurred reading the reply from the server");
|
||||
deauthenticate();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !resp.value( "error" ).toString().isEmpty() )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Auth server returned an error";
|
||||
emit authError( resp.value( "error" ).toString() );
|
||||
deauthenticate();
|
||||
return;
|
||||
}
|
||||
|
||||
const QString nonce = resp.value( "data" ).toMap().value( "nonce" ).toString();
|
||||
if ( nonce != m_uuid )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Auth server nonce value does not match!";
|
||||
emit authError( "The nonce value was incorrect. YOUR ACCOUNT MAY BE COMPROMISED." );
|
||||
deauthenticate();
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray authenticationToken = resp.value( "data" ).toMap().value( "token" ).toByteArray();
|
||||
uint expiration = resp.value( "data" ).toMap().value( "expiration" ).toUInt( &ok );
|
||||
|
||||
QVariantHash creds = credentials();
|
||||
creds[ "username" ] = username;
|
||||
creds[ "authtoken" ] = authenticationToken;
|
||||
creds[ "expiration" ] = expiration;
|
||||
setCredentials( creds );
|
||||
syncConfig();
|
||||
|
||||
if ( !authenticationToken.isEmpty() )
|
||||
{
|
||||
if ( sipPlugin() )
|
||||
sipPlugin()->connectPlugin();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccount::onFetchAccessTokensFinished()
|
||||
{
|
||||
tLog() << Q_FUNC_INFO;
|
||||
QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() );
|
||||
Q_ASSERT( reply );
|
||||
bool ok;
|
||||
const QVariantMap resp = parseReply( reply, ok );
|
||||
if ( !ok || !resp.value( "error" ).toString().isEmpty() )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Auth server returned an error";
|
||||
if ( ok )
|
||||
emit authError( resp.value( "error" ).toString() );
|
||||
deauthenticate();
|
||||
return;
|
||||
}
|
||||
|
||||
QVariantHash creds = credentials();
|
||||
QStringList tokenTypesFound;
|
||||
|
||||
tDebug() << Q_FUNC_INFO << "resp: " << resp;
|
||||
|
||||
foreach( QVariant tokenVariant, resp[ "data" ].toMap()[ "tokens" ].toList() )
|
||||
{
|
||||
QVariantMap tokenMap = tokenVariant.toMap();
|
||||
QString tokenTypeName = tokenMap[ "type" ].toString() + "tokens";
|
||||
if ( !tokenTypesFound.contains( tokenTypeName ) )
|
||||
{
|
||||
creds[ tokenTypeName ] = QVariantList();
|
||||
tokenTypesFound.append( tokenTypeName );
|
||||
}
|
||||
creds[ tokenTypeName ] = creds[ tokenTypeName ].toList() << tokenMap;
|
||||
}
|
||||
|
||||
tDebug() << Q_FUNC_INFO << "Creds: " << creds;
|
||||
|
||||
setCredentials( creds );
|
||||
syncConfig();
|
||||
|
||||
tLog() << Q_FUNC_INFO << "Access tokens fetched successfully";
|
||||
|
||||
emit accessTokensFetched();
|
||||
}
|
||||
|
||||
|
||||
QString
|
||||
HatchetAccount::authUrlForService( const Service &service ) const
|
||||
{
|
||||
return m_extraAuthUrls.value( service, QString() );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccount::authUrlDiscovered( Service service, const QString &authUrl )
|
||||
{
|
||||
m_extraAuthUrls[ service ] = authUrl;
|
||||
}
|
||||
|
||||
|
||||
QVariantMap
|
||||
HatchetAccount::parseReply( QNetworkReply* reply, bool& okRet ) const
|
||||
{
|
||||
QVariantMap resp;
|
||||
|
||||
reply->deleteLater();
|
||||
|
||||
bool ok;
|
||||
int statusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt( &ok );
|
||||
if ( reply->error() != QNetworkReply::NoError && statusCode != 400 && statusCode != 500 )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Network error in command:" << reply->error() << reply->errorString();
|
||||
okRet = false;
|
||||
return resp;
|
||||
}
|
||||
|
||||
QJson::Parser p;
|
||||
resp = p.parse( reply, &ok ).toMap();
|
||||
|
||||
if ( !ok )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Error parsing JSON from server";
|
||||
okRet = false;
|
||||
return resp;
|
||||
}
|
||||
|
||||
if ( !resp.value( "error", "" ).toString().isEmpty() )
|
||||
{
|
||||
tLog() << "Error from tomahawk server response, or in parsing from json:" << resp.value( "error" ).toString() << resp;
|
||||
}
|
||||
|
||||
tDebug() << Q_FUNC_INFO << "Got keys" << resp.keys();
|
||||
tDebug() << Q_FUNC_INFO << "Got values" << resp.values();
|
||||
okRet = true;
|
||||
return resp;
|
||||
}
|
||||
|
||||
Q_EXPORT_PLUGIN2( Tomahawk::Accounts::AccountFactory, Tomahawk::Accounts::HatchetAccountFactory )
|
132
src/accounts/hatchet/account/HatchetAccount.h
Normal file
132
src/accounts/hatchet/account/HatchetAccount.h
Normal file
@@ -0,0 +1,132 @@
|
||||
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
|
||||
*
|
||||
* Copyright 2012 Leo Franchi <lfranchi@kde.org>
|
||||
*
|
||||
* Tomahawk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tomahawk is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef HATCHET_ACCOUNT_H
|
||||
#define HATCHET_ACCOUNT_H
|
||||
|
||||
|
||||
#include <accounts/Account.h>
|
||||
#include <accounts/AccountDllMacro.h>
|
||||
|
||||
#include <QtCrypto>
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
class HatchetSipPlugin;
|
||||
|
||||
namespace Tomahawk
|
||||
{
|
||||
namespace Accounts
|
||||
{
|
||||
|
||||
class HatchetAccountConfig;
|
||||
|
||||
class ACCOUNTDLLEXPORT HatchetAccountFactory : public AccountFactory
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_INTERFACES( Tomahawk::Accounts::AccountFactory )
|
||||
public:
|
||||
HatchetAccountFactory();
|
||||
virtual ~HatchetAccountFactory();
|
||||
|
||||
virtual QString factoryId() const { return "hatchetaccount"; }
|
||||
virtual QString prettyName() const { return "Hatchet"; }
|
||||
virtual QString description() const { return tr( "Connect to your Hatchet account" ); }
|
||||
virtual bool isUnique() const { return true; }
|
||||
AccountTypes types() const { return AccountTypes( SipType ); };
|
||||
// virtual bool allowUserCreation() const { return false; }
|
||||
#ifndef ENABLE_HEADLESS
|
||||
virtual QPixmap icon() const;
|
||||
#endif
|
||||
|
||||
|
||||
virtual Account* createAccount ( const QString& pluginId = QString() );
|
||||
};
|
||||
|
||||
class ACCOUNTDLLEXPORT HatchetAccount : public Account
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Service {
|
||||
Facebook = 0
|
||||
};
|
||||
|
||||
HatchetAccount( const QString &accountId );
|
||||
virtual ~HatchetAccount();
|
||||
|
||||
static HatchetAccount* instance();
|
||||
|
||||
QPixmap icon() const;
|
||||
|
||||
void authenticate();
|
||||
void deauthenticate();
|
||||
bool isAuthenticated() const;
|
||||
|
||||
void setConnectionState( Account::ConnectionState connectionState );
|
||||
ConnectionState connectionState() const;
|
||||
|
||||
|
||||
virtual Tomahawk::InfoSystem::InfoPluginPtr infoPlugin() { return Tomahawk::InfoSystem::InfoPluginPtr(); }
|
||||
SipPlugin* sipPlugin();
|
||||
|
||||
AccountConfigWidget* configurationWidget();
|
||||
QWidget* aclWidget() { return 0; }
|
||||
|
||||
QString username() const;
|
||||
|
||||
void fetchAccessTokens( const QString& type = "dreamcatcher" );
|
||||
|
||||
QString authUrlForService( const Service& service ) const;
|
||||
|
||||
signals:
|
||||
void authError( QString error );
|
||||
void deauthenticated();
|
||||
void accessTokensFetched();
|
||||
|
||||
private slots:
|
||||
void onPasswordLoginFinished( QNetworkReply*, const QString& username );
|
||||
void onFetchAccessTokensFinished();
|
||||
void authUrlDiscovered( Tomahawk::Accounts::HatchetAccount::Service service, const QString& authUrl );
|
||||
|
||||
private:
|
||||
QByteArray authToken() const;
|
||||
uint authTokenExpiration() const;
|
||||
|
||||
void loginWithPassword( const QString& username, const QString& password, const QString &otp );
|
||||
|
||||
QVariantMap parseReply( QNetworkReply* reply, bool& ok ) const;
|
||||
|
||||
QWeakPointer<HatchetAccountConfig> m_configWidget;
|
||||
|
||||
Account::ConnectionState m_state;
|
||||
|
||||
QWeakPointer< HatchetSipPlugin > m_tomahawkSipPlugin;
|
||||
QHash< Service, QString > m_extraAuthUrls;
|
||||
|
||||
static HatchetAccount* s_instance;
|
||||
friend class HatchetAccountConfig;
|
||||
|
||||
QCA::PublicKey* m_publicKey;
|
||||
QString m_uuid;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endif
|
182
src/accounts/hatchet/account/HatchetAccountConfig.cpp
Normal file
182
src/accounts/hatchet/account/HatchetAccountConfig.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
|
||||
*
|
||||
* Copyright 2012 Leo Franchi <lfranchi@kde.org>
|
||||
*
|
||||
* Tomahawk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tomahawk is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "HatchetAccountConfig.h"
|
||||
#include "HatchetAccount.h"
|
||||
#include "utils/TomahawkUtils.h"
|
||||
#include "utils/Logger.h"
|
||||
|
||||
#include "ui_HatchetAccountConfig.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
||||
using namespace Tomahawk;
|
||||
using namespace Accounts;
|
||||
|
||||
namespace {
|
||||
enum ButtonAction {
|
||||
Login,
|
||||
Register,
|
||||
Logout
|
||||
};
|
||||
}
|
||||
|
||||
HatchetAccountConfig::HatchetAccountConfig( HatchetAccount* account )
|
||||
: AccountConfigWidget( 0 )
|
||||
, m_ui( new Ui::HatchetAccountConfig )
|
||||
, m_account( account )
|
||||
{
|
||||
Q_ASSERT( m_account );
|
||||
|
||||
m_ui->setupUi( this );
|
||||
|
||||
m_ui->label->setPixmap( m_ui->label->pixmap()->scaled( QSize( 128, 127 ), Qt::KeepAspectRatio, Qt::SmoothTransformation ) );
|
||||
|
||||
m_ui->loginButton->setDefault( true );
|
||||
connect( m_ui->loginButton, SIGNAL( clicked( bool ) ), this, SLOT( login() ) );
|
||||
|
||||
connect( m_ui->usernameEdit, SIGNAL( textChanged( QString ) ), this, SLOT( fieldsChanged() ) );
|
||||
connect( m_ui->passwordEdit, SIGNAL( textChanged( QString ) ), this, SLOT( fieldsChanged() ) );
|
||||
connect( m_ui->otpEdit, SIGNAL( textChanged( QString ) ), this, SLOT( fieldsChanged() ) );
|
||||
|
||||
connect( m_account, SIGNAL( authError( QString ) ), this, SLOT( authError( QString ) ) );
|
||||
connect( m_account, SIGNAL( deauthenticated() ), this, SLOT( showLoggedOut() ) );
|
||||
connect( m_account, SIGNAL( accessTokensFetched() ), this, SLOT( accountInfoUpdated() ) );
|
||||
|
||||
if ( !m_account->authToken().isEmpty() )
|
||||
accountInfoUpdated();
|
||||
else
|
||||
{
|
||||
m_ui->usernameEdit->setText( m_account->username() );
|
||||
showLoggedOut();
|
||||
}
|
||||
}
|
||||
|
||||
HatchetAccountConfig::~HatchetAccountConfig()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccountConfig::login()
|
||||
{
|
||||
const ButtonAction action = static_cast< ButtonAction>( m_ui->loginButton->property( "action" ).toInt() );
|
||||
|
||||
if ( action == Login )
|
||||
{
|
||||
// Log in mode
|
||||
m_account->loginWithPassword( m_ui->usernameEdit->text(), m_ui->passwordEdit->text(), m_ui->otpEdit->text() );
|
||||
}
|
||||
else if ( action == Logout )
|
||||
{
|
||||
// TODO
|
||||
m_ui->usernameEdit->clear();
|
||||
m_ui->passwordEdit->clear();
|
||||
m_ui->otpEdit->clear();
|
||||
|
||||
QVariantHash creds = m_account->credentials();
|
||||
creds.clear();
|
||||
m_account->setCredentials( creds );
|
||||
m_account->sync();
|
||||
m_account->deauthenticate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccountConfig::fieldsChanged()
|
||||
{
|
||||
const QString username = m_ui->usernameEdit->text();
|
||||
const QString password = m_ui->passwordEdit->text();
|
||||
|
||||
const ButtonAction action = static_cast< ButtonAction>( m_ui->loginButton->property( "action" ).toInt() );
|
||||
|
||||
m_ui->loginButton->setEnabled( !username.isEmpty() && !password.isEmpty() && action == Login );
|
||||
|
||||
m_ui->errorLabel->clear();
|
||||
|
||||
if ( action == Login )
|
||||
m_ui->loginButton->setText( tr( "Login" ) );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccountConfig::showLoggedIn()
|
||||
{
|
||||
m_ui->usernameLabel->hide();
|
||||
m_ui->usernameEdit->hide();
|
||||
m_ui->otpLabel->hide();
|
||||
m_ui->otpEdit->hide();
|
||||
m_ui->passwordLabel->hide();
|
||||
m_ui->passwordEdit->hide();
|
||||
|
||||
m_ui->loggedInLabel->setText( tr( "Logged in as: %1" ).arg( m_account->username() ) );
|
||||
m_ui->loggedInLabel->show();
|
||||
|
||||
m_ui->errorLabel->clear();
|
||||
m_ui->errorLabel->hide();
|
||||
|
||||
m_ui->loginButton->setText( "Log out" );
|
||||
m_ui->loginButton->setProperty( "action", Logout );
|
||||
m_ui->loginButton->setDefault( true );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccountConfig::showLoggedOut()
|
||||
{
|
||||
m_ui->usernameLabel->show();
|
||||
m_ui->usernameEdit->show();
|
||||
m_ui->passwordLabel->show();
|
||||
m_ui->passwordEdit->show();
|
||||
m_ui->otpEdit->show();
|
||||
m_ui->otpLabel->show();
|
||||
|
||||
m_ui->loggedInLabel->clear();
|
||||
m_ui->loggedInLabel->hide();
|
||||
|
||||
m_ui->errorLabel->clear();
|
||||
|
||||
m_ui->loginButton->setText( "Login" );
|
||||
m_ui->loginButton->setProperty( "action", Login );
|
||||
m_ui->loginButton->setDefault( true );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccountConfig::accountInfoUpdated()
|
||||
{
|
||||
showLoggedIn();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccountConfig::authError( const QString &error )
|
||||
{
|
||||
QMessageBox::critical( this, "An error was encountered logging in:", error );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetAccountConfig::showEvent( QShowEvent *event )
|
||||
{
|
||||
AccountConfigWidget::showEvent( event );
|
||||
m_ui->loginButton->setDefault( true );
|
||||
}
|
69
src/accounts/hatchet/account/HatchetAccountConfig.h
Normal file
69
src/accounts/hatchet/account/HatchetAccountConfig.h
Normal file
@@ -0,0 +1,69 @@
|
||||
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
|
||||
*
|
||||
* Copyright 2012 Leo Franchi <lfranchi@kde.org>
|
||||
*
|
||||
* Tomahawk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tomahawk is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef HATCHET_ACCOUNT_CONFIG_H
|
||||
#define HATCHET_ACCOUNT_CONFIG_H
|
||||
|
||||
#include <accounts/AccountConfigWidget.h>
|
||||
|
||||
#include <QWidget>
|
||||
#include <QVariantMap>
|
||||
|
||||
class QNetworkReply;
|
||||
|
||||
namespace Ui {
|
||||
class HatchetAccountConfig;
|
||||
}
|
||||
|
||||
namespace Tomahawk {
|
||||
namespace Accounts {
|
||||
|
||||
class HatchetAccount;
|
||||
|
||||
class HatchetAccountConfig : public AccountConfigWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HatchetAccountConfig( HatchetAccount* account );
|
||||
virtual ~HatchetAccountConfig();
|
||||
|
||||
private slots:
|
||||
void login();
|
||||
|
||||
void fieldsChanged();
|
||||
|
||||
void showLoggedIn();
|
||||
void showLoggedOut();
|
||||
|
||||
void accountInfoUpdated();
|
||||
|
||||
void authError( const QString& error );
|
||||
|
||||
protected:
|
||||
//virtual void changeEvent( QEvent* event );
|
||||
virtual void showEvent( QShowEvent* event );
|
||||
|
||||
private:
|
||||
Ui::HatchetAccountConfig* m_ui;
|
||||
HatchetAccount* m_account;
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
150
src/accounts/hatchet/account/HatchetAccountConfig.ui
Normal file
150
src/accounts/hatchet/account/HatchetAccountConfig.ui
Normal file
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>HatchetAccountConfig</class>
|
||||
<widget class="QWidget" name="HatchetAccountConfig">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>301</width>
|
||||
<height>404</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="../resources.qrc">:/hatchet-account/hatchet-icon-512x512.png</pixmap>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="instructionLabel">
|
||||
<property name="text">
|
||||
<string>Connect to your Hatchet account</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="loggedInLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="credentialsLayout">
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="otpLabel">
|
||||
<property name="text">
|
||||
<string>One-time
|
||||
Password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="usernameLabel">
|
||||
<property name="text">
|
||||
<string>Username</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="usernameEdit">
|
||||
<property name="placeholderText">
|
||||
<string>Hatchet username</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="passwordLabel">
|
||||
<property name="text">
|
||||
<string>Password:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="passwordEdit">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="echoMode">
|
||||
<enum>QLineEdit::Password</enum>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>Hatchet password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="otpEdit">
|
||||
<property name="placeholderText">
|
||||
<string>(Only if configured)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="errorLabel">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="loginButton">
|
||||
<property name="text">
|
||||
<string>Login</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="../resources.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
14
src/accounts/hatchet/admin/certs/dreamcatcher.pem
Normal file
14
src/accounts/hatchet/admin/certs/dreamcatcher.pem
Normal file
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyT6j9B1hiRHwV96BSZJp
|
||||
vLnGS0p6h1fxJiVGOjpcct/pA1rfVW66LTC7seYT6mF15flccIn5H8srAvH2a57S
|
||||
uCMHDrHkQIoNDoOf+Ersw4tYzVYv+5h8IN9apYr6iqfT7rn3Fzb5oEB+GqcDurX3
|
||||
4aaDjm+Wq0QjJAZf0fOaKor0ASejkj6EywpaHLwj51DKtf6SYbDEf52vDOzsDDnx
|
||||
2AzVeqzPNFnnau6/v7rCi081b33qOrbJBtJjSNAJcM3iVf465k8KP2VD8nX3wzwT
|
||||
9H0TEp67UMm0lz2i6gSAyEcIR2FNdTjVa1ZHYF2tnOihFDu0H4U4/4mPswpNNdOp
|
||||
mtc5WxxK5ouKqxD8ArSoclmZMybIBbL0lLRjo4VC/olfbd1RQ57L9ETtNIf0xxKU
|
||||
aweUkfWyJfU5ueaSlBaGoKSGugqp50ql3Td0m2JnJmaopdSi12L3EdlaO0+/OL3f
|
||||
0aXbtnsZXlIHoq7FBEYcLhCNbwEeceV5Cveb5Os8w2331jkgpcPoXQFOtIVyva2B
|
||||
3RadI0aALIbX5Tt0/AcbLeq10BJH8Ny2RSrhF2cxkjXEwQ68OUbCv1U4Si+EL44E
|
||||
tctg8zj9rB8SG+miE18cXaQEUxA/olOU/Axkm8dLLLTxqWsD3VQYDxBgBemaY1OT
|
||||
O32yipB9ko1sLCx6ZaQQ56sCAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
14
src/accounts/hatchet/admin/certs/mandella.pem
Normal file
14
src/accounts/hatchet/admin/certs/mandella.pem
Normal file
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwqJWocwqceJBfO1Bbfdr
|
||||
ozqYW+3T5VlmpBILw1OREDuUjdCO8mYvgfFTq5/JTZo/grLmd+LqntU3Y3E/u5Ij
|
||||
Y92hRbPVmsQGIBcbTXuaasFLwjF9HAHaDcTbD0hS1mg1ZeO8r+uVvGmtdGDjulo/
|
||||
lCqo8PP2SEu7Bx1w2XQLQSiCnKVZ5Zk25JQDiiDzsBmdTPhOaihU3kyo32qTOovs
|
||||
xXv9d6J/Uziv/DcuoIDKS1Uvn1h91QTn2wDdW6NcPgjHq/BD0o7cHeuI6d7XXZm3
|
||||
XM2TGQLl489h3kT3j6Tn30K3vXobYQvRXH76xjvYuWHlFaT/eSyEx0W/dIoN1Ot/
|
||||
88SEaR0hlgBhL1e9sLbQUXKhHooeWApWUDVtioQGRklcZZV3eP3YUUkvUmhfls8R
|
||||
dgcI68cfZp0JFtbosHtJdzOcDwncKwJCUOP0XIx/5xn4wrAUJqjq08IcRG5s6GUn
|
||||
lw/n8Tb/paG2Ip5XhzNYX8fSklj2C6/5jZP2jTnAnFVy314U3vFEqLe/kQxIPWen
|
||||
pPlTqLSBq1J0HomxSUuizjSF4u1oW7TchCO+zV1EjOD3FEsBpWgkG9NIyqOtdUVo
|
||||
QH4c5HsflspmSAnM3zuap9NHPjFo2Q1tqoqqA2arTzO5zb1d0IQxe193dZ4zm4vj
|
||||
fb5OBuKW3xwYpJbezb2rHH8CAwEAAQ==
|
||||
-----END PUBLIC KEY-----
|
44
src/accounts/hatchet/admin/certs/startcomroot.pem
Normal file
44
src/accounts/hatchet/admin/certs/startcomroot.pem
Normal file
@@ -0,0 +1,44 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIHyTCCBbGgAwIBAgIBATANBgkqhkiG9w0BAQUFADB9MQswCQYDVQQGEwJJTDEW
|
||||
MBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0YWwg
|
||||
Q2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3RhcnRDb20gQ2VydGlmaWNh
|
||||
dGlvbiBBdXRob3JpdHkwHhcNMDYwOTE3MTk0NjM2WhcNMzYwOTE3MTk0NjM2WjB9
|
||||
MQswCQYDVQQGEwJJTDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMi
|
||||
U2VjdXJlIERpZ2l0YWwgQ2VydGlmaWNhdGUgU2lnbmluZzEpMCcGA1UEAxMgU3Rh
|
||||
cnRDb20gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUA
|
||||
A4ICDwAwggIKAoICAQDBiNsJvGxGfHiflXu1M5DycmLWwTYgIiRezul38kMKogZk
|
||||
pMyONvg45iPwbm2xPN1yo4UcodM9tDMr0y+v/uqwQVlntsQGfQqedIXWeUyAN3rf
|
||||
OQVSWff0G0ZDpNKFhdLDcfN1YjS6LIp/Ho/u7TTQEceWzVI9ujPW3U3eCztKS5/C
|
||||
Ji/6tRYccjV3yjxd5srhJosaNnZcAdt0FCX+7bWgiA/deMotHweXMAEtcnn6RtYT
|
||||
Kqi5pquDSR3l8u/d5AGOGAqPY1MWhWKpDhk6zLVmpsJrdAfkK+F2PrRt2PZE4XNi
|
||||
HzvEvqBTViVsUQn3qqvKv3b9bZvzndu/PWa8DFaqr5hIlTpL36dYUNk4dalb6kMM
|
||||
Av+Z6+hsTXBbKWWc3apdzK8BMewM69KN6Oqce+Zu9ydmDBpI125C4z/eIT574Q1w
|
||||
+2OqqGwaVLRcJXrJosmLFqa7LH4XXgVNWG4SHQHuEhANxjJ/GP/89PrNbpHoNkm+
|
||||
Gkhpi8KWTRoSsmkXwQqQ1vp5Iki/untp+HDH+no32NgN0nZPV/+Qt+OR0t3vwmC3
|
||||
Zzrd/qqc8NSLf3Iizsafl7b4r4qgEKjZ+xjGtrVcUjyJthkqcwEKDwOzEmDyei+B
|
||||
26Nu/yYwl/WL3YlXtq09s68rxbd2AvCl1iuahhQqcvbjM4xdCUsT37uMdBNSSwID
|
||||
AQABo4ICUjCCAk4wDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAa4wHQYDVR0OBBYE
|
||||
FE4L7xqkQFulF2mHMMo0aEPQQa7yMGQGA1UdHwRdMFswLKAqoCiGJmh0dHA6Ly9j
|
||||
ZXJ0LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMCugKaAnhiVodHRwOi8vY3Js
|
||||
LnN0YXJ0Y29tLm9yZy9zZnNjYS1jcmwuY3JsMIIBXQYDVR0gBIIBVDCCAVAwggFM
|
||||
BgsrBgEEAYG1NwEBATCCATswLwYIKwYBBQUHAgEWI2h0dHA6Ly9jZXJ0LnN0YXJ0
|
||||
Y29tLm9yZy9wb2xpY3kucGRmMDUGCCsGAQUFBwIBFilodHRwOi8vY2VydC5zdGFy
|
||||
dGNvbS5vcmcvaW50ZXJtZWRpYXRlLnBkZjCB0AYIKwYBBQUHAgIwgcMwJxYgU3Rh
|
||||
cnQgQ29tbWVyY2lhbCAoU3RhcnRDb20pIEx0ZC4wAwIBARqBl0xpbWl0ZWQgTGlh
|
||||
YmlsaXR5LCByZWFkIHRoZSBzZWN0aW9uICpMZWdhbCBMaW1pdGF0aW9ucyogb2Yg
|
||||
dGhlIFN0YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFBvbGljeSBhdmFp
|
||||
bGFibGUgYXQgaHR0cDovL2NlcnQuc3RhcnRjb20ub3JnL3BvbGljeS5wZGYwEQYJ
|
||||
YIZIAYb4QgEBBAQDAgAHMDgGCWCGSAGG+EIBDQQrFilTdGFydENvbSBGcmVlIFNT
|
||||
TCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTANBgkqhkiG9w0BAQUFAAOCAgEAFmyZ
|
||||
9GYMNPXQhV59CuzaEE44HF7fpiUFS5Eyweg78T3dRAlbB0mKKctmArexmvclmAk8
|
||||
jhvh3TaHK0u7aNM5Zj2gJsfyOZEdUauCe37Vzlrk4gNXcGmXCPleWKYK34wGmkUW
|
||||
FjgKXlf2Ysd6AgXmvB618p70qSmD+LIU424oh0TDkBreOKk8rENNZEXO3SipXPJz
|
||||
ewT4F+irsfMuXGRuczE6Eri8sxHkfY+BUZo7jYn0TZNmezwD7dOaHZrzZVD1oNB1
|
||||
ny+v8OqCQ5j4aZyJecRDjkZy42Q2Eq/3JR44iZB3fsNrarnDy0RLrHiQi+fHLB5L
|
||||
EUTINFInzQpdn4XBidUaePKVEFMy3YCEZnXZtWgo+2EuvoSoOMCZEoalHmdkrQYu
|
||||
L6lwhceWD3yJZfWOQ1QOq92lgDmUYMA0yZZwLKMS9R9Ie70cfmu3nZD0Ijuu+Pwq
|
||||
yvqCUqDvr0tVk+vBtfAii6w0TiYiBKGHLHVKt+V9E9e4DGTANtLJL4YSjCMJwRuC
|
||||
O3NJo2pXh5Tl1njFmUNj403gdy3hZZlyaQQaRwnmDwFWJPsfvw55qVguucQJAX6V
|
||||
um0ABj6y6koQOdjQK/W/7HW/lwLFCRsI3FU34oH7N4RDYiDK51ZLZer+bMEkkySh
|
||||
NOsF/5oirpt9P/FlUQqmMGqz9IgcgA38corog14=
|
||||
-----END CERTIFICATE-----
|
BIN
src/accounts/hatchet/admin/icons/hatchet-icon-512x512.png
Normal file
BIN
src/accounts/hatchet/admin/icons/hatchet-icon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
37
src/accounts/hatchet/cmake/Modules/FindQCA2.cmake
Normal file
37
src/accounts/hatchet/cmake/Modules/FindQCA2.cmake
Normal file
@@ -0,0 +1,37 @@
|
||||
# - Try to find QCA2 (Qt Cryptography Architecture 2)
|
||||
# Once done this will define
|
||||
#
|
||||
# QCA2_FOUND - system has QCA2
|
||||
# QCA2_INCLUDE_DIR - the QCA2 include directory
|
||||
# QCA2_LIBRARIES - the libraries needed to use QCA2
|
||||
# QCA2_DEFINITIONS - Compiler switches required for using QCA2
|
||||
#
|
||||
# use pkg-config to get the directories and then use these values
|
||||
# in the FIND_PATH() and FIND_LIBRARY() calls
|
||||
|
||||
# Copyright (c) 2006, Michael Larouche, <michael.larouche@kdemail.net>
|
||||
#
|
||||
# Redistribution and use is allowed according to the terms of the BSD license.
|
||||
# For details see the accompanying COPYING-CMAKE-SCRIPTS file.
|
||||
|
||||
if (NOT WIN32)
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(PC_QCA2 qca2)
|
||||
set(QCA2_DEFINITIONS ${PC_QCA2_CFLAGS_OTHER})
|
||||
endif (NOT WIN32)
|
||||
|
||||
find_library(QCA2_LIBRARIES
|
||||
NAMES qca
|
||||
HINTS ${PC_QCA2_LIBDIR} ${PC_QCA2_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
find_path(QCA2_INCLUDE_DIR qca.h
|
||||
HINTS ${PC_QCA2_INCLUDEDIR} ${PC_QCA2_INCLUDE_DIRS}
|
||||
PATH_SUFFIXES QtCrypto
|
||||
PATHS /usr/local/lib/qca.framework/Headers/
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(QCA2 DEFAULT_MSG QCA2_LIBRARIES QCA2_INCLUDE_DIR)
|
||||
|
||||
mark_as_advanced(QCA2_INCLUDE_DIR QCA2_LIBRARIES)
|
8
src/accounts/hatchet/resources.qrc
Normal file
8
src/accounts/hatchet/resources.qrc
Normal file
@@ -0,0 +1,8 @@
|
||||
<RCC>
|
||||
<qresource prefix="/hatchet-account">
|
||||
<file alias="hatchet-icon-512x512.png">admin/icons/hatchet-icon-512x512.png</file>
|
||||
<file alias="mandella.pem">admin/certs/mandella.pem</file>
|
||||
<file alias="dreamcatcher.pem">admin/certs/dreamcatcher.pem</file>
|
||||
<file alias="startcomroot.pem">admin/certs/startcomroot.pem</file>
|
||||
</qresource>
|
||||
</RCC>
|
605
src/accounts/hatchet/sip/HatchetSip.cpp
Normal file
605
src/accounts/hatchet/sip/HatchetSip.cpp
Normal file
@@ -0,0 +1,605 @@
|
||||
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
|
||||
*
|
||||
* Copyright 2012, Jeff Mitchell <jeff@tomahawk-player.org>
|
||||
*
|
||||
* Tomahawk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tomahawk is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "HatchetSip.h"
|
||||
|
||||
#include "account/HatchetAccount.h"
|
||||
#include "WebSocketThreadController.h"
|
||||
//#include "WebSocket.h"
|
||||
|
||||
#include <database/Database.h>
|
||||
#include <database/DatabaseImpl.h>
|
||||
#include <database/DatabaseCommand_LoadOps.h>
|
||||
#include <network/ControlConnection.h>
|
||||
#include <network/Servent.h>
|
||||
#include <sip/PeerInfo.h>
|
||||
#include <utils/Logger.h>
|
||||
#include <SourceList.h>
|
||||
|
||||
#include <QFile>
|
||||
#include <QUuid>
|
||||
|
||||
HatchetSipPlugin::HatchetSipPlugin( Tomahawk::Accounts::Account *account )
|
||||
: SipPlugin( account )
|
||||
, m_sipState( Closed )
|
||||
, m_version( 0 )
|
||||
, m_publicKey( nullptr )
|
||||
, m_reconnectTimer( this )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO;
|
||||
|
||||
connect( m_account, SIGNAL( accessTokensFetched() ), this, SLOT( connectWebSocket() ) );
|
||||
connect( Servent::instance(), SIGNAL( dbSyncTriggered() ), this, SLOT( dbSyncTriggered() ));
|
||||
|
||||
QFile pemFile( ":/hatchet-account/dreamcatcher.pem" );
|
||||
pemFile.open( QIODevice::ReadOnly );
|
||||
tDebug() << Q_FUNC_INFO << "certs/dreamcatcher.pem: " << pemFile.readAll();
|
||||
pemFile.close();
|
||||
pemFile.open( QIODevice::ReadOnly );
|
||||
QCA::ConvertResult conversionResult;
|
||||
QCA::PublicKey publicKey = QCA::PublicKey::fromPEM(pemFile.readAll(), &conversionResult);
|
||||
if ( QCA::ConvertGood != conversionResult )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "INVALID PUBKEY READ";
|
||||
return;
|
||||
}
|
||||
m_publicKey = new QCA::PublicKey( publicKey );
|
||||
|
||||
m_reconnectTimer.setInterval( 0 );
|
||||
m_reconnectTimer.setSingleShot( true );
|
||||
connect( &m_reconnectTimer, SIGNAL( timeout() ), SLOT( connectPlugin() ) );
|
||||
}
|
||||
|
||||
|
||||
HatchetSipPlugin::~HatchetSipPlugin()
|
||||
{
|
||||
if ( m_webSocketThreadController )
|
||||
{
|
||||
m_webSocketThreadController->quit();
|
||||
m_webSocketThreadController->wait( 60000 );
|
||||
|
||||
delete m_webSocketThreadController.data();
|
||||
}
|
||||
|
||||
m_sipState = Closed;
|
||||
|
||||
hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Disconnected );
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
HatchetSipPlugin::isValid() const
|
||||
{
|
||||
return m_account->enabled() && m_account->isAuthenticated() && m_publicKey;
|
||||
}
|
||||
|
||||
|
||||
Tomahawk::Accounts::HatchetAccount*
|
||||
HatchetSipPlugin::hatchetAccount() const
|
||||
{
|
||||
return qobject_cast< Tomahawk::Accounts::HatchetAccount* >( m_account );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::connectPlugin()
|
||||
{
|
||||
tLog() << Q_FUNC_INFO;
|
||||
if ( !m_account->isAuthenticated() )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Account not authenticated, not continuing";
|
||||
//FIXME: Prompt user for password?
|
||||
return;
|
||||
}
|
||||
|
||||
hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Connecting );
|
||||
hatchetAccount()->fetchAccessTokens();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::disconnectPlugin()
|
||||
{
|
||||
tLog() << Q_FUNC_INFO;
|
||||
if ( m_webSocketThreadController && m_webSocketThreadController->isRunning() )
|
||||
emit disconnectWebSocket();
|
||||
else
|
||||
webSocketDisconnected();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////// Connection methods ////////////////////////////////////
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::connectWebSocket()
|
||||
{
|
||||
tLog() << Q_FUNC_INFO;
|
||||
if ( m_webSocketThreadController )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Already have a thread running, bailing";
|
||||
return;
|
||||
}
|
||||
|
||||
m_webSocketThreadController = QPointer< WebSocketThreadController >( new WebSocketThreadController( this ) );
|
||||
|
||||
if ( !m_webSocketThreadController )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Could not create a new thread, bailing";
|
||||
disconnectPlugin();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !isValid() )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Invalid state, not continuing with connection";
|
||||
return;
|
||||
}
|
||||
|
||||
m_token.clear();
|
||||
|
||||
QVariantList tokensCreds = m_account->credentials()[ "dreamcatchertokens" ].toList();
|
||||
|
||||
//FIXME: Don't blindly pick the first one that matches? Most likely, cycle through if the first one fails
|
||||
QVariantMap connectVals;
|
||||
foreach ( QVariant credObj, tokensCreds )
|
||||
{
|
||||
QVariantMap creds = credObj.toMap();
|
||||
if ( creds.contains( "type" ) && creds[ "type" ].toString() == "dreamcatcher" )
|
||||
{
|
||||
connectVals = creds;
|
||||
m_token = creds[ "token" ].toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QString url;
|
||||
if ( !connectVals.isEmpty() )
|
||||
{
|
||||
QString port = connectVals[ "port" ].toString();
|
||||
if ( port == "443" )
|
||||
url = "wss://";
|
||||
else
|
||||
url = "ws://";
|
||||
url += connectVals[ "host" ].toString() + ':' + connectVals[ "port" ].toString();
|
||||
}
|
||||
|
||||
if ( url.isEmpty() || m_token.isEmpty() )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Unable to find a proper connection endpoint; bailing";
|
||||
disconnectPlugin();
|
||||
return;
|
||||
}
|
||||
else
|
||||
tLog() << Q_FUNC_INFO << "Connecting to Dreamcatcher endpoint at: " << url;
|
||||
|
||||
m_webSocketThreadController->setUrl( url );
|
||||
m_webSocketThreadController->start();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::webSocketConnected()
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "WebSocket connected";
|
||||
|
||||
if ( m_token.isEmpty() || !m_account->credentials().contains( "username" ) )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "access token or username is empty, aborting";
|
||||
disconnectPlugin();
|
||||
return;
|
||||
}
|
||||
|
||||
hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Connected );
|
||||
m_sipState = AcquiringVersion;
|
||||
|
||||
m_uuid = QUuid::createUuid().toString();
|
||||
QCA::SecureArray sa( m_uuid.toLatin1() );
|
||||
QCA::SecureArray result = m_publicKey->encrypt( sa, QCA::EME_PKCS1_OAEP );
|
||||
|
||||
tDebug() << Q_FUNC_INFO << "uuid:" << m_uuid << ", size of uuid:" << m_uuid.size() << ", size of sa:" << sa.size() << ", size of result:" << result.size();
|
||||
|
||||
QVariantMap nonceVerMap;
|
||||
nonceVerMap[ "version" ] = VERSION;
|
||||
nonceVerMap[ "nonce" ] = QString( result.toByteArray().toBase64() );
|
||||
sendBytes( nonceVerMap );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::webSocketDisconnected()
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "WebSocket disconnected";
|
||||
|
||||
m_reconnectTimer.stop();
|
||||
|
||||
if ( m_webSocketThreadController )
|
||||
{
|
||||
m_webSocketThreadController->quit();
|
||||
m_webSocketThreadController->wait( 60000 );
|
||||
|
||||
delete m_webSocketThreadController.data();
|
||||
}
|
||||
|
||||
m_sipState = Closed;
|
||||
m_version = 0;
|
||||
|
||||
hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Disconnected );
|
||||
|
||||
if ( hatchetAccount()->enabled() )
|
||||
{
|
||||
// Work on the assumption that we were disconnected because Dreamcatcher shut down
|
||||
// Reconnect after a time; use reasonable backoff + random
|
||||
int interval = m_reconnectTimer.interval() <= 25000 ? m_reconnectTimer.interval() + 5000 : 30000;
|
||||
interval += QCA::Random::randomInt() % 30;
|
||||
m_reconnectTimer.setInterval( interval );
|
||||
m_reconnectTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
HatchetSipPlugin::sendBytes( const QVariantMap& jsonMap ) const
|
||||
{
|
||||
tLog() << Q_FUNC_INFO;
|
||||
if ( m_sipState == Closed )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "was told to send bytes on a closed connection, not gonna do it";
|
||||
return false;
|
||||
}
|
||||
|
||||
QJson::Serializer serializer;
|
||||
QByteArray bytes = serializer.serialize( jsonMap );
|
||||
if ( bytes.isEmpty() )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "could not serialize register structure to JSON";
|
||||
return false;
|
||||
}
|
||||
|
||||
tDebug() << Q_FUNC_INFO << "Sending bytes of size" << bytes.size();
|
||||
emit rawBytes( bytes );
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::messageReceived( const QByteArray &msg )
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO << "WebSocket message: " << msg;
|
||||
|
||||
QJson::Parser parser;
|
||||
bool ok;
|
||||
QVariant jsonVariant = parser.parse( msg, &ok );
|
||||
if ( !jsonVariant.isValid() )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Failed to parse message back from server";
|
||||
return;
|
||||
}
|
||||
|
||||
QVariantMap retMap = jsonVariant.toMap();
|
||||
|
||||
if ( m_sipState == AcquiringVersion )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "In acquiring version state, expecting version/nonce information";
|
||||
if ( !retMap.contains( "version" ) || !retMap.contains( "nonce" ) )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Failed to acquire version or nonce information";
|
||||
disconnectPlugin();
|
||||
return;
|
||||
}
|
||||
bool ok = false;
|
||||
int ver = retMap[ "version" ].toInt( &ok );
|
||||
if ( ver == 0 || !ok )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Failed to acquire version information";
|
||||
disconnectPlugin();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( retMap[ "nonce" ].toString() != m_uuid )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Failed to validate nonce";
|
||||
disconnectPlugin();
|
||||
return;
|
||||
}
|
||||
|
||||
m_version = ver;
|
||||
|
||||
QVariantMap registerMap;
|
||||
registerMap[ "command" ] = "register";
|
||||
registerMap[ "token" ] = m_token;
|
||||
registerMap[ "dbid" ] = Database::instance()->impl()->dbid();
|
||||
registerMap[ "alias" ] = QHostInfo::localHostName();
|
||||
|
||||
QList< SipInfo > sipinfos = Servent::instance()->getLocalSipInfos( "default", "default" );
|
||||
QVariantList hostinfos;
|
||||
foreach ( SipInfo sipinfo, sipinfos )
|
||||
{
|
||||
QVariantMap hostinfo;
|
||||
hostinfo[ "host" ] = sipinfo.host();
|
||||
hostinfo[ "port" ] = sipinfo.port();
|
||||
hostinfos << hostinfo;
|
||||
}
|
||||
|
||||
registerMap[ "hostinfo" ] = hostinfos;
|
||||
|
||||
if ( !sendBytes( registerMap ) )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Failed sending message";
|
||||
disconnectPlugin();
|
||||
return;
|
||||
}
|
||||
|
||||
m_sipState = Registering;
|
||||
}
|
||||
else if ( m_sipState == Registering )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "In registering state, checking status of registration";
|
||||
if ( retMap.contains( "status" ) &&
|
||||
retMap[ "status" ].toString() == "success" )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Registered successfully";
|
||||
m_sipState = Connected;
|
||||
hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Connected );
|
||||
m_reconnectTimer.setInterval( 0 );
|
||||
QTimer::singleShot(0, this, SLOT( dbSyncTriggered() ) );
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Failed to register successfully";
|
||||
//m_ws.data()->stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if ( m_sipState != Connected )
|
||||
{
|
||||
// ...erm?
|
||||
tLog() << Q_FUNC_INFO << "Got a message from a non connected socket?";
|
||||
return;
|
||||
}
|
||||
else if ( !retMap.contains( "command" ) ||
|
||||
!retMap[ "command" ].canConvert< QString >() )
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO << "Unable to convert and/or interepret command from server";
|
||||
return;
|
||||
}
|
||||
|
||||
QString command = retMap[ "command" ].toString();
|
||||
|
||||
if ( command == "new-peer" )
|
||||
newPeer( retMap );
|
||||
else if ( command == "peer-authorization" )
|
||||
peerAuthorization( retMap );
|
||||
else if ( command == "synclastseen" )
|
||||
sendOplog( retMap );
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
HatchetSipPlugin::checkKeys( QStringList keys, const QVariantMap& map ) const
|
||||
{
|
||||
foreach ( QString key, keys )
|
||||
{
|
||||
if ( !map.contains( key ) )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Did not find the value" << key << "in the new-peer structure";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////// Peer handling methods ////////////////////////////////////
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::newPeer( const QVariantMap& valMap )
|
||||
{
|
||||
const QString username = valMap[ "username" ].toString();
|
||||
const QVariantList hostinfo = valMap[ "hostinfo" ].toList();
|
||||
const QString dbid = valMap[ "dbid" ].toString();
|
||||
|
||||
tDebug() << Q_FUNC_INFO << "username:" << username << "dbid" << dbid;
|
||||
|
||||
QStringList keys( QStringList() << "command" << "username" << "hostinfo" << "dbid" );
|
||||
if ( !checkKeys( keys, valMap ) )
|
||||
return;
|
||||
|
||||
Tomahawk::peerinfo_ptr peerInfo = Tomahawk::PeerInfo::get( this, dbid, Tomahawk::PeerInfo::AutoCreate );
|
||||
peerInfo->setContactId( username );
|
||||
peerInfo->setFriendlyName( username );
|
||||
QVariantMap data;
|
||||
data.insert( "dbid", dbid );
|
||||
peerInfo->setData( data );
|
||||
|
||||
QList< SipInfo > sipInfos;
|
||||
|
||||
foreach ( const QVariant listItem, hostinfo )
|
||||
{
|
||||
if ( !listItem.canConvert< QVariantMap >() )
|
||||
continue;
|
||||
|
||||
QVariantMap hostpair = listItem.toMap();
|
||||
|
||||
if ( !hostpair.contains( "host" ) || !hostpair.contains( "port" ) )
|
||||
continue;
|
||||
|
||||
const QString host = hostpair[ "host" ].toString();
|
||||
unsigned int port = hostpair[ "port" ].toUInt();
|
||||
|
||||
if ( host.isEmpty() || port == 0 )
|
||||
continue;
|
||||
|
||||
SipInfo sipInfo;
|
||||
sipInfo.setNodeId( dbid );
|
||||
sipInfo.setHost( host );
|
||||
sipInfo.setPort( port );
|
||||
sipInfo.setVisible( true );
|
||||
sipInfos << sipInfo;
|
||||
}
|
||||
|
||||
m_sipInfoHash[ dbid ] = sipInfos;
|
||||
|
||||
peerInfo->setStatus( Tomahawk::PeerInfo::Online );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::sendSipInfos(const Tomahawk::peerinfo_ptr& receiver, const QList< SipInfo >& infos)
|
||||
{
|
||||
if ( infos.size() == 0 )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Got no sipinfo data (list size 0)";
|
||||
return;
|
||||
}
|
||||
|
||||
const QString dbid = receiver->data().toMap().value( "dbid" ).toString();
|
||||
tDebug() << Q_FUNC_INFO << "Send local info to " << receiver->friendlyName() << "(" << dbid << ") we are" << infos[ 0 ].nodeId() << "with offerkey " << infos[ 0 ].key();
|
||||
|
||||
QVariantMap sendMap;
|
||||
sendMap[ "command" ] = "authorize-peer";
|
||||
sendMap[ "dbid" ] = dbid;
|
||||
sendMap[ "offerkey" ] = infos[ 0 ].key();
|
||||
|
||||
if ( !sendBytes( sendMap ) )
|
||||
tLog() << Q_FUNC_INFO << "Failed sending message";
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::peerAuthorization( const QVariantMap& valMap )
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO << "dbid:" << valMap[ "dbid" ].toString() << "offerkey" << valMap[ "offerkey" ].toString();
|
||||
|
||||
QStringList keys( QStringList() << "command" << "dbid" << "offerkey" );
|
||||
if ( !checkKeys( keys, valMap ) )
|
||||
return;
|
||||
|
||||
QString dbid = valMap[ "dbid" ].toString();
|
||||
|
||||
Tomahawk::peerinfo_ptr peerInfo = Tomahawk::PeerInfo::get( this, dbid );
|
||||
if( peerInfo.isNull() )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Received a peer-authorization for a peer we don't know about";
|
||||
return;
|
||||
}
|
||||
|
||||
QList< SipInfo > sipInfos = m_sipInfoHash[ dbid ];
|
||||
for (int i = 0; i < sipInfos.size(); i++)
|
||||
sipInfos[i].setKey( valMap[ "offerkey" ].toString() );
|
||||
peerInfo->setSipInfos( sipInfos );
|
||||
m_sipInfoHash.remove( dbid );
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////// Syncing methods ////////////////////////////////////
|
||||
|
||||
void
|
||||
HatchetSipPlugin::dbSyncTriggered()
|
||||
{
|
||||
if ( m_sipState != Connected )
|
||||
return;
|
||||
|
||||
if ( !SourceList::instance() || SourceList::instance()->getLocal().isNull() )
|
||||
return;
|
||||
|
||||
QVariantMap sourceMap;
|
||||
sourceMap[ "command" ] = "synctrigger";
|
||||
const Tomahawk::source_ptr src = SourceList::instance()->getLocal();
|
||||
sourceMap[ "name" ] = src->friendlyName();
|
||||
sourceMap[ "alias" ] = QHostInfo::localHostName();
|
||||
sourceMap[ "friendlyname" ] = src->dbFriendlyName();
|
||||
|
||||
if ( !sendBytes( sourceMap ) )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Failed sending message";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::sendOplog( const QVariantMap& valMap ) const
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO;
|
||||
DatabaseCommand_loadOps* cmd = new DatabaseCommand_loadOps( SourceList::instance()->getLocal(), valMap[ "lastrevision" ].toString() );
|
||||
connect( cmd, SIGNAL( done( QString, QString, QList< dbop_ptr > ) ), SLOT( oplogFetched( QString, QString, QList< dbop_ptr > ) ) );
|
||||
Database::instance()->enqueue( QSharedPointer< DatabaseCommand >( cmd ) );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
HatchetSipPlugin::oplogFetched( const QString& sinceguid, const QString& /* lastguid */, const QList< dbop_ptr > ops )
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO;
|
||||
const uint_fast32_t byteMax = 1 << 25;
|
||||
int currBytes = 0;
|
||||
QVariantMap commandMap;
|
||||
commandMap[ "command" ] = "oplog";
|
||||
commandMap[ "startingrevision" ] = sinceguid;
|
||||
currBytes += 60; //baseline for quotes, keys, commas, colons, etc.
|
||||
QVariantList revisions;
|
||||
tDebug() << Q_FUNC_INFO << "Found" << ops.size() << "ops";
|
||||
foreach( const dbop_ptr op, ops )
|
||||
{
|
||||
currBytes += 80; //baseline for quotes, keys, commas, colons, etc.
|
||||
QVariantMap revMap;
|
||||
revMap[ "revision" ] = op->guid;
|
||||
currBytes += op->guid.length();
|
||||
revMap[ "singleton" ] = op->singleton;
|
||||
currBytes += 5; //true or false
|
||||
revMap[ "command" ] = op->command;
|
||||
currBytes += op->command.length();
|
||||
currBytes += 5; //true or false
|
||||
if ( op->compressed )
|
||||
{
|
||||
revMap[ "compressed" ] = true;
|
||||
QByteArray b64 = op->payload.toBase64();
|
||||
revMap[ "payload" ] = op->payload.toBase64();
|
||||
currBytes += b64.length();
|
||||
}
|
||||
else
|
||||
{
|
||||
revMap[ "compressed" ] = false;
|
||||
revMap[ "payload" ] = op->payload;
|
||||
currBytes += op->payload.length();
|
||||
}
|
||||
if ( currBytes >= (int)(byteMax - 1000000) ) // tack on an extra 1M for safety as it seems qjson puts in spaces
|
||||
break;
|
||||
else
|
||||
revisions << revMap;
|
||||
}
|
||||
tDebug() << Q_FUNC_INFO << "Sending" << revisions.size() << "revisions";
|
||||
commandMap[ "revisions" ] = revisions;
|
||||
|
||||
if ( !sendBytes( commandMap ) )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Failed sending message, attempting to send a blank message to clear sync state";
|
||||
QVariantMap rescueMap;
|
||||
rescueMap[ "command" ] = "oplog";
|
||||
if ( !sendBytes( rescueMap ) )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Failed to send rescue map; state may be out-of-sync with server; reconnecting";
|
||||
disconnectPlugin();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
95
src/accounts/hatchet/sip/HatchetSip.h
Normal file
95
src/accounts/hatchet/sip/HatchetSip.h
Normal file
@@ -0,0 +1,95 @@
|
||||
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
|
||||
*
|
||||
* Copyright 2012, Jeff Mitchell <jeff@tomahawk-player.org>
|
||||
*
|
||||
* Tomahawk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tomahawk is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef HATCHET_SIP_H
|
||||
#define HATCHET_SIP_H
|
||||
|
||||
#include "accounts/AccountDllMacro.h"
|
||||
#include "database/Op.h"
|
||||
#include "sip/SipPlugin.h"
|
||||
#include "account/HatchetAccount.h"
|
||||
|
||||
#include <QPointer>
|
||||
#include <QTimer>
|
||||
#include <QtCrypto>
|
||||
|
||||
class WebSocketThreadController;
|
||||
|
||||
const int VERSION = 1;
|
||||
|
||||
class ACCOUNTDLLEXPORT HatchetSipPlugin : public SipPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
enum SipState {
|
||||
AcquiringVersion,
|
||||
Registering,
|
||||
Connected,
|
||||
Closed
|
||||
};
|
||||
|
||||
public:
|
||||
HatchetSipPlugin( Tomahawk::Accounts::Account *account );
|
||||
|
||||
virtual ~HatchetSipPlugin();
|
||||
|
||||
virtual bool isValid() const;
|
||||
|
||||
virtual void sendSipInfos( const Tomahawk::peerinfo_ptr& receiver, const QList< SipInfo >& infos );
|
||||
|
||||
public slots:
|
||||
virtual void connectPlugin();
|
||||
void disconnectPlugin();
|
||||
void checkSettings() {}
|
||||
void configurationChanged() {}
|
||||
void addContact( const QString &, const QString& ) {}
|
||||
void sendMsg( const QString&, const SipInfo& ) {}
|
||||
void webSocketConnected();
|
||||
void webSocketDisconnected();
|
||||
|
||||
signals:
|
||||
void connectWebSocket() const;
|
||||
void disconnectWebSocket() const;
|
||||
void authUrlDiscovered( Tomahawk::Accounts::HatchetAccount::Service service, const QString& authUrl ) const;
|
||||
void rawBytes( QByteArray bytes ) const;
|
||||
|
||||
private slots:
|
||||
void dbSyncTriggered();
|
||||
void messageReceived( const QByteArray& msg );
|
||||
void connectWebSocket();
|
||||
void oplogFetched( const QString& sinceguid, const QString& lastguid, const QList< dbop_ptr > ops );
|
||||
|
||||
private:
|
||||
bool sendBytes( const QVariantMap& jsonMap ) const;
|
||||
bool checkKeys( QStringList keys, const QVariantMap& map ) const;
|
||||
void newPeer( const QVariantMap& valMap );
|
||||
void peerAuthorization( const QVariantMap& valMap );
|
||||
void sendOplog( const QVariantMap& valMap ) const;
|
||||
Tomahawk::Accounts::HatchetAccount* hatchetAccount() const;
|
||||
|
||||
QPointer< WebSocketThreadController > m_webSocketThreadController;
|
||||
QString m_token;
|
||||
QString m_uuid;
|
||||
SipState m_sipState;
|
||||
int m_version;
|
||||
QCA::PublicKey* m_publicKey;
|
||||
QTimer m_reconnectTimer;
|
||||
QHash< QString, QList< SipInfo > > m_sipInfoHash;
|
||||
};
|
||||
|
||||
#endif
|
324
src/accounts/hatchet/sip/WebSocket.cpp
Normal file
324
src/accounts/hatchet/sip/WebSocket.cpp
Normal file
@@ -0,0 +1,324 @@
|
||||
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
|
||||
*
|
||||
* Copyright 2012, Leo Franchi <lfranchi@kde.org>
|
||||
*
|
||||
* Tomahawk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tomahawk is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "WebSocket.h"
|
||||
|
||||
#include "utils/Logger.h"
|
||||
|
||||
#include <functional>
|
||||
|
||||
typedef typename websocketpp::lib::error_code error_code;
|
||||
|
||||
WebSocket::WebSocket( const QString& url )
|
||||
: QObject( nullptr )
|
||||
, m_disconnecting( false )
|
||||
, m_url( url )
|
||||
, m_outputStream()
|
||||
, m_lastSocketState( QAbstractSocket::UnconnectedState )
|
||||
, m_connectionTimer( this )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "WebSocket constructing";
|
||||
m_client = std::unique_ptr< hatchet_client >( new hatchet_client() );
|
||||
m_client->set_message_handler( std::bind(&onMessage, this, std::placeholders::_1, std::placeholders::_2 ) );
|
||||
m_client->set_close_handler( std::bind(&onClose, this, std::placeholders::_1 ) );
|
||||
m_client->register_ostream( &m_outputStream );
|
||||
|
||||
m_connectionTimer.setSingleShot( true );
|
||||
m_connectionTimer.setInterval( 30000 );
|
||||
connect( &m_connectionTimer, SIGNAL( timeout() ), SLOT( disconnectWs() ) );
|
||||
}
|
||||
|
||||
|
||||
WebSocket::~WebSocket()
|
||||
{
|
||||
if ( m_connection )
|
||||
m_connection.reset();
|
||||
|
||||
m_client.reset();
|
||||
|
||||
if ( m_socket )
|
||||
delete m_socket.data();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocket::setUrl( const QString &url )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Setting url to" << url;
|
||||
if ( m_url == url )
|
||||
return;
|
||||
|
||||
// We'll let automatic reconnection handle things
|
||||
if ( m_socket && m_socket->isEncrypted() )
|
||||
disconnectWs();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocket::connectWs()
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Connecting";
|
||||
m_disconnecting = false;
|
||||
if ( m_socket )
|
||||
{
|
||||
if ( m_socket->isEncrypted() )
|
||||
return;
|
||||
|
||||
if ( m_socket->state() == QAbstractSocket::ClosingState )
|
||||
QMetaObject::invokeMethod( this, "connectWs", Qt::QueuedConnection );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
tLog() << Q_FUNC_INFO << "Establishing new connection";
|
||||
m_socket = QPointer< QSslSocket >( new QSslSocket( nullptr ) );
|
||||
m_socket->addCaCertificate( QSslCertificate::fromPath( ":/hatchet-account/startcomroot.pem").first() );
|
||||
QObject::connect( m_socket, SIGNAL( stateChanged( QAbstractSocket::SocketState ) ), SLOT( socketStateChanged( QAbstractSocket::SocketState ) ) );
|
||||
QObject::connect( m_socket, SIGNAL( sslErrors( const QList< QSslError >& ) ), SLOT( sslErrors( const QList< QSslError >& ) ) );
|
||||
QObject::connect( m_socket, SIGNAL( encrypted() ), SLOT( encrypted() ) );
|
||||
QObject::connect( m_socket, SIGNAL( readyRead() ), SLOT( socketReadyRead() ) );
|
||||
m_socket->connectToHostEncrypted( m_url.host(), m_url.port() );
|
||||
m_connectionTimer.start();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocket::disconnectWs( websocketpp::close::status::value status, const QString &reason )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Disconnecting";
|
||||
m_disconnecting = true;
|
||||
|
||||
error_code ec;
|
||||
if ( m_connection )
|
||||
{
|
||||
m_connection->close( status, reason.toAscii().constData(), ec );
|
||||
QMetaObject::invokeMethod( this, "readOutput", Qt::QueuedConnection );
|
||||
QTimer::singleShot( 5000, this, SLOT( disconnectSocket() ) ); //safety
|
||||
return;
|
||||
}
|
||||
|
||||
disconnectSocket();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocket::disconnectSocket()
|
||||
{
|
||||
if ( m_socket && m_socket->state() == QAbstractSocket::ConnectedState )
|
||||
m_socket->disconnectFromHost();
|
||||
else
|
||||
QMetaObject::invokeMethod( this, "cleanup", Qt::QueuedConnection );
|
||||
|
||||
QTimer::singleShot( 5000, this, SLOT( cleanup() ) ); //safety
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocket::cleanup()
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Cleaning up";
|
||||
m_outputStream.seekg( std::ios_base::end );
|
||||
m_outputStream.seekp( std::ios_base::end );
|
||||
m_queuedMessagesToSend.empty();
|
||||
if ( m_connection )
|
||||
m_connection.reset();
|
||||
|
||||
emit disconnected();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocket::socketStateChanged( QAbstractSocket::SocketState state )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Socket state changed to" << state;
|
||||
switch ( state )
|
||||
{
|
||||
case QAbstractSocket::ClosingState:
|
||||
if ( m_lastSocketState == QAbstractSocket::ClosingState )
|
||||
{
|
||||
// It seems like it does not actually properly close, so force it
|
||||
tLog() << Q_FUNC_INFO << "Got a double closing state, cleaning up and emitting disconnected";
|
||||
m_socket->deleteLater();
|
||||
m_lastSocketState = QAbstractSocket::UnconnectedState;
|
||||
QMetaObject::invokeMethod( this, "cleanup", Qt::QueuedConnection );
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case QAbstractSocket::UnconnectedState:
|
||||
if ( m_lastSocketState == QAbstractSocket::UnconnectedState )
|
||||
return;
|
||||
tLog() << Q_FUNC_INFO << "Socket now unconnected, cleaning up and emitting disconnected";
|
||||
m_socket->deleteLater();
|
||||
m_lastSocketState = QAbstractSocket::UnconnectedState;
|
||||
QMetaObject::invokeMethod( this, "cleanup", Qt::QueuedConnection );
|
||||
return;
|
||||
default:
|
||||
;
|
||||
}
|
||||
m_lastSocketState = state;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocket::sslErrors( const QList< QSslError >& errors )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Encountered errors when trying to connect via SSL";
|
||||
foreach( QSslError error, errors )
|
||||
tLog() << Q_FUNC_INFO << "Error: " << error.errorString();
|
||||
QMetaObject::invokeMethod( this, "disconnectWs", Qt::QueuedConnection );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocket::encrypted()
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Encrypted connection to Dreamcatcher established";
|
||||
error_code ec;
|
||||
|
||||
QUrl url(m_url);
|
||||
websocketpp::uri_ptr uri(new websocketpp::uri(false, url.host().toStdString(), url.port(), "/"));
|
||||
|
||||
m_connection = m_client->get_connection( uri, ec );
|
||||
if ( !m_connection || ec.value() != 0 )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Got error creating WS connection, error is:" << QString::fromStdString( ec.message() );
|
||||
disconnectWs();
|
||||
return;
|
||||
}
|
||||
m_client->connect( m_connection );
|
||||
QMetaObject::invokeMethod( this, "readOutput", Qt::QueuedConnection );
|
||||
emit connected();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocket::readOutput()
|
||||
{
|
||||
if ( !m_connection )
|
||||
return;
|
||||
|
||||
tDebug() << Q_FUNC_INFO;
|
||||
|
||||
std::string outputString = m_outputStream.str();
|
||||
if ( outputString.size() > 0 )
|
||||
{
|
||||
m_outputStream.str("");
|
||||
|
||||
tDebug() << Q_FUNC_INFO << "Got string of size" << outputString.size() << "from ostream";
|
||||
qint64 sizeWritten = m_socket->write( outputString.data(), outputString.size() );
|
||||
tDebug() << Q_FUNC_INFO << "Wrote" << sizeWritten << "bytes to the socket";
|
||||
if ( sizeWritten == -1 )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Error during writing, closing connection";
|
||||
QMetaObject::invokeMethod( this, "disconnectWs", Qt::QueuedConnection );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ( m_queuedMessagesToSend.size() )
|
||||
{
|
||||
if ( m_connection->get_state() == websocketpp::session::state::open )
|
||||
{
|
||||
foreach( QByteArray message, m_queuedMessagesToSend )
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO << "Sending queued message of size" << message.size();
|
||||
m_connection->send( std::string( message.constData(), message.size() ), websocketpp::frame::opcode::TEXT );
|
||||
}
|
||||
|
||||
m_queuedMessagesToSend.clear();
|
||||
QMetaObject::invokeMethod( this, "readOutput", Qt::QueuedConnection );
|
||||
m_connectionTimer.stop();
|
||||
}
|
||||
else if ( !m_disconnecting )
|
||||
QTimer::singleShot( 200, this, SLOT( readOutput() ) );
|
||||
}
|
||||
else
|
||||
m_connectionTimer.stop();
|
||||
}
|
||||
|
||||
void
|
||||
WebSocket::socketReadyRead()
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO;
|
||||
|
||||
if ( !m_socket || !m_socket->isEncrypted() )
|
||||
return;
|
||||
|
||||
if ( !m_socket->isValid() )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Socket appears to no longer be valid. Something is wrong; disconnecting";
|
||||
QMetaObject::invokeMethod( this, "disconnectWs", Qt::QueuedConnection );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( qint64 bytes = m_socket->bytesAvailable() )
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO << "Bytes available:" << bytes;
|
||||
QByteArray buf;
|
||||
buf.resize( bytes );
|
||||
qint64 bytesRead = m_socket->read( buf.data(), bytes );
|
||||
tDebug() << Q_FUNC_INFO << "Bytes read:" << bytesRead; // << ", content is" << websocketpp::utility::to_hex( buf.constData(), bytesRead ).data();
|
||||
if ( bytesRead != bytes )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Error occurred during socket read. Something is wrong; disconnecting";
|
||||
QMetaObject::invokeMethod( this, "disconnectWs", Qt::QueuedConnection );
|
||||
return;
|
||||
}
|
||||
std::stringstream ss( std::string( buf.constData(), bytesRead ) );
|
||||
ss >> *m_connection;
|
||||
}
|
||||
|
||||
QMetaObject::invokeMethod( this, "readOutput", Qt::QueuedConnection );
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocket::encodeMessage( const QByteArray &bytes )
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO << "Encoding message"; //, message is" << bytes.constData();
|
||||
if ( !m_connection )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Asked to send message but do not have a valid connection!";
|
||||
return;
|
||||
}
|
||||
|
||||
if ( m_connection->get_state() != websocketpp::session::state::open )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Connection not yet open/upgraded, queueing work to send";
|
||||
m_queuedMessagesToSend.append( bytes );
|
||||
m_connectionTimer.start();
|
||||
}
|
||||
else
|
||||
m_connection->send( std::string( bytes.constData() ), websocketpp::frame::opcode::TEXT );
|
||||
|
||||
QMetaObject::invokeMethod( this, "readOutput", Qt::QueuedConnection );
|
||||
}
|
||||
|
||||
void
|
||||
onMessage( WebSocket* ws, websocketpp::connection_hdl, hatchet_client::message_ptr msg )
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO << "Handling message";
|
||||
std::string payload = msg->get_payload();
|
||||
ws->decodedMessage( QByteArray( payload.data(), payload.length() ) );
|
||||
}
|
||||
|
||||
void
|
||||
onClose( WebSocket *ws, websocketpp::connection_hdl )
|
||||
{
|
||||
tDebug() << Q_FUNC_INFO << "Handling message";
|
||||
QMetaObject::invokeMethod( ws, "disconnectSocket", Qt::QueuedConnection );
|
||||
}
|
84
src/accounts/hatchet/sip/WebSocket.h
Normal file
84
src/accounts/hatchet/sip/WebSocket.h
Normal file
@@ -0,0 +1,84 @@
|
||||
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
|
||||
*
|
||||
* Copyright 2012, Leo Franchi <lfranchi@kde.org>
|
||||
*
|
||||
* Tomahawk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tomahawk is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef WEBSOCKET__H
|
||||
#define WEBSOCKET__H
|
||||
|
||||
#include "DllMacro.h"
|
||||
|
||||
#include "hatchet_config.hpp"
|
||||
#include <websocketpp/client.hpp>
|
||||
|
||||
#include <QPointer>
|
||||
#include <QSslSocket>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
|
||||
#include <memory>
|
||||
|
||||
typedef typename websocketpp::client< websocketpp::config::hatchet_client > hatchet_client;
|
||||
|
||||
class WebSocket;
|
||||
|
||||
void onMessage( WebSocket* ws, websocketpp::connection_hdl, hatchet_client::message_ptr msg );
|
||||
void onClose( WebSocket* ws, websocketpp::connection_hdl );
|
||||
|
||||
class DLLEXPORT WebSocket : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit WebSocket( const QString& url );
|
||||
virtual ~WebSocket();
|
||||
|
||||
signals:
|
||||
void connected();
|
||||
void disconnected();
|
||||
void decodedMessage( QByteArray bytes );
|
||||
|
||||
public slots:
|
||||
void setUrl( const QString& url );
|
||||
void connectWs();
|
||||
void disconnectWs( websocketpp::close::status::value status = websocketpp::close::status::normal, const QString& reason = QString( "Disconnecting" ) );
|
||||
void encodeMessage( const QByteArray& bytes );
|
||||
|
||||
private slots:
|
||||
void socketStateChanged( QAbstractSocket::SocketState state );
|
||||
void sslErrors( const QList< QSslError >& errors );
|
||||
void disconnectSocket();
|
||||
void cleanup();
|
||||
void encrypted();
|
||||
void readOutput();
|
||||
void socketReadyRead();
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY( WebSocket )
|
||||
|
||||
friend void onMessage( WebSocket *ws, websocketpp::connection_hdl, hatchet_client::message_ptr msg );
|
||||
friend void onClose( WebSocket *ws, websocketpp::connection_hdl );
|
||||
|
||||
bool m_disconnecting;
|
||||
QUrl m_url;
|
||||
std::stringstream m_outputStream;
|
||||
std::unique_ptr< hatchet_client > m_client;
|
||||
hatchet_client::connection_ptr m_connection;
|
||||
QPointer< QSslSocket > m_socket;
|
||||
QAbstractSocket::SocketState m_lastSocketState;
|
||||
QList< QByteArray > m_queuedMessagesToSend;
|
||||
QTimer m_connectionTimer;
|
||||
};
|
||||
|
||||
#endif
|
71
src/accounts/hatchet/sip/WebSocketThreadController.cpp
Normal file
71
src/accounts/hatchet/sip/WebSocketThreadController.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
|
||||
*
|
||||
* Copyright 2012, Leo Franchi <lfranchi@kde.org>
|
||||
*
|
||||
* Tomahawk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tomahawk is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "WebSocketThreadController.h"
|
||||
#include "WebSocket.h"
|
||||
|
||||
#include "utils/Logger.h"
|
||||
|
||||
WebSocketThreadController::WebSocketThreadController( QObject* sip )
|
||||
: QThread( nullptr )
|
||||
, m_webSocket( nullptr )
|
||||
, m_sip( sip )
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
WebSocketThreadController::~WebSocketThreadController()
|
||||
{
|
||||
if ( m_webSocket )
|
||||
{
|
||||
delete m_webSocket;
|
||||
m_webSocket = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocketThreadController::setUrl( const QString &url )
|
||||
{
|
||||
m_url = url;
|
||||
if ( m_webSocket )
|
||||
{
|
||||
QMetaObject::invokeMethod( m_webSocket, "setUrl", Qt::QueuedConnection, Q_ARG( QString, url ));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
WebSocketThreadController::run()
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Starting";
|
||||
m_webSocket = QPointer< WebSocket >( new WebSocket( m_url ) );
|
||||
if ( m_webSocket && m_sip )
|
||||
{
|
||||
tLog() << Q_FUNC_INFO << "Have a valid websocket and parent";
|
||||
connect( m_sip, SIGNAL( connectWebSocket() ), m_webSocket, SLOT( connectWs() ), Qt::QueuedConnection );
|
||||
connect( m_sip, SIGNAL( disconnectWebSocket() ), m_webSocket, SLOT( disconnectWs() ), Qt::QueuedConnection );
|
||||
connect( m_sip, SIGNAL( rawBytes( QByteArray ) ), m_webSocket, SLOT( encodeMessage( QByteArray ) ), Qt::QueuedConnection );
|
||||
connect( m_webSocket, SIGNAL( connected() ), m_sip, SLOT( webSocketConnected() ), Qt::QueuedConnection );
|
||||
connect( m_webSocket, SIGNAL( disconnected() ), m_sip, SLOT( webSocketDisconnected() ), Qt::QueuedConnection );
|
||||
connect( m_webSocket, SIGNAL( decodedMessage( QByteArray ) ), m_sip, SLOT( messageReceived( QByteArray ) ), Qt::QueuedConnection );
|
||||
QMetaObject::invokeMethod( m_webSocket, "connectWs", Qt::QueuedConnection );
|
||||
exec();
|
||||
delete m_webSocket;
|
||||
m_webSocket = 0;
|
||||
}
|
||||
}
|
49
src/accounts/hatchet/sip/WebSocketThreadController.h
Normal file
49
src/accounts/hatchet/sip/WebSocketThreadController.h
Normal file
@@ -0,0 +1,49 @@
|
||||
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
|
||||
*
|
||||
* Copyright 2012, Leo Franchi <lfranchi@kde.org>
|
||||
*
|
||||
* Tomahawk is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Tomahawk is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Tomahawk. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef WEBSOCKET_THREAD_CONTROLLER_H
|
||||
#define WEBSOCKET_THREAD_CONTROLLER_H
|
||||
|
||||
#include "DllMacro.h"
|
||||
|
||||
#include <QPointer>
|
||||
#include <QThread>
|
||||
|
||||
class WebSocket;
|
||||
|
||||
class DLLEXPORT WebSocketThreadController : public QThread
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WebSocketThreadController( QObject* sip );
|
||||
virtual ~WebSocketThreadController();
|
||||
|
||||
void setUrl( const QString &url );
|
||||
|
||||
protected:
|
||||
void run();
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY( WebSocketThreadController )
|
||||
|
||||
QPointer< WebSocket > m_webSocket;
|
||||
QPointer< QObject > m_sip;
|
||||
QString m_url;
|
||||
};
|
||||
|
||||
#endif
|
72
src/accounts/hatchet/sip/hatchet_config.hpp
Normal file
72
src/accounts/hatchet/sip/hatchet_config.hpp
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2013, Peter Thorson. All rights reserved.
|
||||
* Copyright (c) 2013, Jeff Mitchell <jeff@tomahawk-player.org>. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* * Neither the name of the WebSocket++ Project nor the
|
||||
* names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL PETER THORSON BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
|
||||
#ifndef WEBSOCKETPP_CONFIG_HATCHET_CLIENT_HPP
|
||||
#define WEBSOCKETPP_CONFIG_HATCHET_CLIENT_HPP
|
||||
|
||||
#include <websocketpp/config/core_client.hpp>
|
||||
#include <websocketpp/transport/iostream/endpoint.hpp>
|
||||
#include <websocketpp/concurrency/none.hpp>
|
||||
|
||||
namespace websocketpp {
|
||||
namespace config {
|
||||
|
||||
struct hatchet_client : public core_client {
|
||||
typedef hatchet_client type;
|
||||
|
||||
typedef websocketpp::concurrency::none concurrency_type;
|
||||
|
||||
typedef core_client::request_type request_type;
|
||||
typedef core_client::response_type response_type;
|
||||
|
||||
typedef core_client::message_type message_type;
|
||||
typedef core_client::con_msg_manager_type con_msg_manager_type;
|
||||
typedef core_client::endpoint_msg_manager_type endpoint_msg_manager_type;
|
||||
|
||||
typedef core_client::alog_type alog_type;
|
||||
typedef core_client::elog_type elog_type;
|
||||
|
||||
typedef core_client::rng_type rng_type;
|
||||
|
||||
struct transport_config {
|
||||
typedef type::concurrency_type concurrency_type;
|
||||
typedef type::alog_type alog_type;
|
||||
typedef type::elog_type elog_type;
|
||||
typedef type::request_type request_type;
|
||||
typedef type::response_type response_type;
|
||||
};
|
||||
|
||||
typedef websocketpp::transport::iostream::endpoint<transport_config> transport_type;
|
||||
|
||||
//static const websocketpp::log::level alog_level = websocketpp::log::alevel::all;
|
||||
};
|
||||
|
||||
} // namespace config
|
||||
} // namespace websocketpp
|
||||
|
||||
#endif // WEBSOCKETPP_CONFIG_HATCHET_CLIENT_HPP
|
@@ -70,8 +70,12 @@ Source::Source( int id, const QString& nodeId )
|
||||
|
||||
if ( m_isLocal )
|
||||
{
|
||||
connect( Accounts::AccountManager::instance(), SIGNAL( connected( Tomahawk::Accounts::Account* ) ), SLOT( setOnline() ) );
|
||||
connect( Accounts::AccountManager::instance(), SIGNAL( disconnected( Tomahawk::Accounts::Account* ) ), SLOT( setOffline() ) );
|
||||
connect( Accounts::AccountManager::instance(),
|
||||
SIGNAL( connected( Tomahawk::Accounts::Account* ) ),
|
||||
SLOT( setOnline() ) );
|
||||
connect( Accounts::AccountManager::instance(),
|
||||
SIGNAL( disconnected( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason ) ),
|
||||
SLOT( handleDisconnect( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +334,13 @@ Source::removeCollection( const collection_ptr& c )
|
||||
emit collectionRemoved( c );
|
||||
}
|
||||
|
||||
void
|
||||
Source::handleDisconnect( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason reason )
|
||||
{
|
||||
if ( reason == Tomahawk::Accounts::AccountManager::Disabled )
|
||||
setOffline();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
Source::setOffline()
|
||||
|
@@ -26,6 +26,7 @@
|
||||
#include <QtCore/QVariantMap>
|
||||
|
||||
#include "Typedefs.h"
|
||||
#include "accounts/AccountManager.h"
|
||||
#include "network/ControlConnection.h"
|
||||
#include "network/DbSyncConnection.h"
|
||||
#include "collection/Collection.h"
|
||||
@@ -136,6 +137,7 @@ private slots:
|
||||
void dbLoaded( unsigned int id, const QString& fname );
|
||||
void updateIndexWhenSynced();
|
||||
|
||||
void handleDisconnect( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason reason );
|
||||
void setOffline();
|
||||
void setOnline();
|
||||
|
||||
|
@@ -512,7 +512,8 @@ AccountManager::onStateChanged( Account::ConnectionState state )
|
||||
if ( account->connectionState() == Account::Disconnected )
|
||||
{
|
||||
m_connectedAccounts.removeAll( account );
|
||||
emit disconnected( account );
|
||||
DisconnectReason reason = account->enabled() ? Disconnected : Disabled;
|
||||
emit disconnected( account, reason );
|
||||
}
|
||||
else if ( account->connectionState() == Account::Connected )
|
||||
{
|
||||
|
@@ -21,7 +21,8 @@
|
||||
#ifndef ACCOUNTMANAGER_H
|
||||
#define ACCOUNTMANAGER_H
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QFlags>
|
||||
#include <QObject>
|
||||
|
||||
#include "Typedefs.h"
|
||||
#include "DllMacro.h"
|
||||
@@ -42,6 +43,11 @@ class DLLEXPORT AccountManager : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum DisconnectReason {
|
||||
Disconnected,
|
||||
Disabled
|
||||
};
|
||||
|
||||
static AccountManager* instance();
|
||||
|
||||
explicit AccountManager( QObject *parent );
|
||||
@@ -61,7 +67,7 @@ public:
|
||||
void hookupAndEnable( Account* account, bool startup = false ); /// Hook up signals and start the plugin
|
||||
void removeAccount( Account* account );
|
||||
|
||||
QList< Account* > accounts() const { return m_accounts; };
|
||||
QList< Account* > accounts() const { return m_accounts; }
|
||||
QList< Account* > accounts( Tomahawk::Accounts::AccountType type ) const { return m_accountsByAccountType[ type ]; }
|
||||
|
||||
QList< Account* > accountsFromFactory( Tomahawk::Accounts::AccountFactory* factory ) const;
|
||||
@@ -105,7 +111,7 @@ signals:
|
||||
void removed( Tomahawk::Accounts::Account* );
|
||||
|
||||
void connected( Tomahawk::Accounts::Account* );
|
||||
void disconnected( Tomahawk::Accounts::Account* );
|
||||
void disconnected( Tomahawk::Accounts::Account*, Tomahawk::Accounts::AccountManager::DisconnectReason );
|
||||
void authError( Tomahawk::Accounts::Account* );
|
||||
|
||||
void stateChanged( Account* p, Accounts::Account::ConnectionState state );
|
||||
|
@@ -54,7 +54,7 @@ Closure::Closure(QObject* sender,
|
||||
|
||||
Closure::Closure(QObject* sender,
|
||||
const char* signal,
|
||||
std::tr1::function<void()> callback)
|
||||
function<void()> callback)
|
||||
: callback_(callback) {
|
||||
Connect(sender, signal);
|
||||
}
|
||||
|
@@ -21,7 +21,13 @@
|
||||
|
||||
#include "DllMacro.h"
|
||||
|
||||
#ifdef _WEBSOCKETPP_CPP11_STL_
|
||||
#include <functional>
|
||||
using std::function;
|
||||
#else
|
||||
#include <tr1/functional>
|
||||
using std::tr1::function;
|
||||
#endif
|
||||
|
||||
#include <QMetaMethod>
|
||||
#include <QObject>
|
||||
@@ -64,7 +70,7 @@ class DLLEXPORT Closure : public QObject, boost::noncopyable {
|
||||
const ClosureArgumentWrapper* val3 = 0);
|
||||
|
||||
Closure(QObject* sender, const char* signal,
|
||||
std::tr1::function<void()> callback);
|
||||
function<void()> callback);
|
||||
|
||||
void setAutoDelete( bool autoDelete ) { autoDelete_ = autoDelete; }
|
||||
|
||||
@@ -87,7 +93,7 @@ class DLLEXPORT Closure : public QObject, boost::noncopyable {
|
||||
void Connect(QObject* sender, const char* signal);
|
||||
|
||||
QMetaMethod slot_;
|
||||
std::tr1::function<void()> callback_;
|
||||
function<void()> callback_;
|
||||
bool autoDelete_;
|
||||
QObject* outOfThreadReceiver_;
|
||||
|
||||
|
Reference in New Issue
Block a user