added tempo integration

master
Boernsman 2021-02-18 09:49:05 +01:00
parent 8150258814
commit 7a30f648a5
6 changed files with 465 additions and 32 deletions

View File

@ -166,6 +166,7 @@ private:
bool m_connected = false;
bool checkStatusCode(QNetworkReply *reply, const QByteArray &rawData);
private slots:
void onRefreshTimeout();

View File

@ -62,15 +62,26 @@ void IntegrationPluginTempo::startPairing(ThingPairingInfo *info)
return;
}
QString jiraCloudInstanceName = info->params().paramValue(tempoConnectionAtlassianAccountParamTypeId).toString();
QUrl url = Tempo::getLoginUrl(QUrl("https://127.0.0.1:8888"), jiraCloudInstanceName, clientId);
qCDebug(dcTempo()) << "Checking if the Tempo server is reachable";
QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(url));
QString jiraCloudInstanceName = info->params().paramValue(tempoConnectionThingAtlassianAccountNameParamTypeId).toString();
Tempo *tempo = new Tempo(hardwareManager()->networkManager(), clientId, clientSecret, this);
QUrl url = tempo->getLoginUrl(QUrl("https://127.0.0.1:8888"), jiraCloudInstanceName);
qCDebug(dcTempo()) << "Checking if the Tempo server is reachable: https://api.tempo.io/core/3";
QNetworkReply *reply = hardwareManager()->networkManager()->get(QNetworkRequest(QUrl("https://api.tempo.io/core/3")));
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, info, [reply, info, url, this] {
connect(reply, &QNetworkReply::finished, info, [reply, info, tempo, url, this] {
if (reply->error() != QNetworkReply::NetworkError::HostNotFoundError) {
qCDebug(dcTempo()) << "Tempo server is reachable";
ThingId thingId = info->thingId();
m_setupTempoConnections.insert(info->thingId(), tempo);
connect(info, &ThingPairingInfo::aborted, this, [thingId, this] {
qCWarning(dcTempo()) << "ThingPairingInfo aborted, cleaning up";
Tempo *tempo = m_setupTempoConnections.take(thingId);
if (tempo)
tempo->deleteLater();
});
qCDebug(dcTempo()) << "OAuthUrl" << url.toString();
info->setOAuthUrl(url);
info->finish(Thing::ThingErrorNoError);
} else {
@ -87,9 +98,9 @@ void IntegrationPluginTempo::startPairing(ThingPairingInfo *info)
void IntegrationPluginTempo::confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret)
{
Q_UNUSED(username);
qCDebug(dcTempo()) << "Confirm pairing";
if (info->thingClassId() == tempoConnectionThingClassId) {
if (info->thingClassId() == tempoConnectionThingClassId) {
qCDebug(dcTempo()) << "Confirm pairing" << info->thingName();
QUrl url(secret);
QUrlQuery query(url);
QByteArray authorizationCode = query.queryItemValue("code").toLocal8Bit();
@ -100,7 +111,7 @@ void IntegrationPluginTempo::confirmPairing(ThingPairingInfo *info, const QStrin
Tempo *tempo = m_setupTempoConnections.value(info->thingId());
if (!tempo) {
qWarning(dcTempo()) << "No Tempo connection found for device:" << info->thingName();
qWarning(dcTempo()) << "No tempo connection found for device:" << info->thingName();
m_setupTempoConnections.remove(info->thingId());
return info->finish(Thing::ThingErrorHardwareFailure);
}
@ -127,6 +138,99 @@ void IntegrationPluginTempo::setupThing(ThingSetupInfo *info)
if (thing->thingClassId() == tempoConnectionThingClassId) {
Tempo *tempo;
if (m_tempoConnections.contains(thing)) {
qCDebug(dcTempo()) << "Setup after reconfiguration, cleaning up";
m_tempoConnections.take(thing)->deleteLater();
}
if (m_setupTempoConnections.keys().contains(thing->id())) {
// This thing setup is after a pairing process
qCDebug(dcTempo()) << "Tempo OAuth setup complete";
tempo = m_setupTempoConnections.take(thing->id());
if (!tempo) {
qCWarning(dcTempo()) << "Tempo connection object not found for thing" << thing->name();
}
m_tempoConnections.insert(thing, tempo);
info->finish(Thing::ThingErrorNoError);
} else {
//device loaded from the device database, needs a new access token;
pluginStorage()->beginGroup(thing->id().toString());
QByteArray refreshToken = pluginStorage()->value("refresh_token").toByteArray();
pluginStorage()->endGroup();
if (refreshToken.isEmpty()) {
info->finish(Thing::ThingErrorAuthenticationFailure, tr("Refresh token is not available."));
return;
}
QByteArray clientId = configValue(tempoPluginCustomClientIdParamTypeId).toByteArray();
QByteArray clientSecret = configValue(tempoPluginCustomClientSecretParamTypeId).toByteArray();
if (clientId.isEmpty() || clientSecret.isEmpty()) {
clientId = apiKeyStorage()->requestKey("tempo").data("clientId");
clientSecret = apiKeyStorage()->requestKey("tempo").data("clientSecret");
} else {
qCDebug(dcTempo()) << "Using custom API id and secret.";
}
if (clientId.isEmpty() || clientSecret.isEmpty()) {
info->finish(Thing::ThingErrorAuthenticationFailure, tr("Client id and/or secret is not available."));
return;
}
Tempo *tempo = new Tempo(hardwareManager()->networkManager(), clientId, clientSecret, this);
tempo->getAccessTokenFromRefreshToken(refreshToken);
connect(tempo, &Tempo::receivedAccessToken, info, [info] {
info->finish(Thing::ThingErrorNoError);
});
connect(info, &ThingSetupInfo::aborted, tempo, &Tempo::deleteLater);
}
connect(tempo, &Tempo::connectionChanged, this, &IntegrationPluginTempo::onConnectionChanged);
connect(tempo, &Tempo::authenticationStatusChanged, this, &IntegrationPluginTempo::onAuthenticationStatusChanged);
connect(tempo, &Tempo::accountsReceived, this, &IntegrationPluginTempo::onReceivedAccounts);
} else if (thing->thingClassId() == accountThingClassId) {
Thing *parentThing = myThings().findById(thing->parentId());
if (parentThing->setupComplete()) {
info->finish(Thing::ThingErrorNoError);
} else {
connect(parentThing, &Thing::setupStatusChanged, info, [parentThing, info]{
if (parentThing->setupComplete()) {
info->finish(Thing::ThingErrorNoError);
}
});
}
} else {
Q_ASSERT_X(false, "setupThing", QString("Unhandled thingClassId: %1").arg(thing->thingClassId().toString()).toUtf8());
}
}
void IntegrationPluginTempo::postSetupThing(Thing *thing)
{
qCDebug(dcTempo()) << "Post setup thing" << thing->name();
if (!m_pluginTimer15min) {
m_pluginTimer15min = hardwareManager()->pluginTimerManager()->registerTimer(60*15);
connect(m_pluginTimer15min, &PluginTimer::timeout, this, [this]() {
qCDebug(dcTempo()) << "Refresh timer timout, polling all Tempo accounts.";
Q_FOREACH (Thing *thing, myThings().filterByThingClassId(tempoConnectionThingClassId)) {
Tempo *tempo = m_tempoConnections.value(thing);
if (!tempo) {
qWarning(dcTempo()) << "No Tempo connection found for" << thing->name();
continue;
}
tempo->getAccounts();
Q_FOREACH (Thing *childThing, myThings().filterByParentId(thing->id())) {
QString key = childThing->paramValue(accountThingKeyParamTypeId).toString();
QDate from(1970, 1, 1);
tempo->getWorkloadByAccount(key, from, QDate::currentDate());
}
}
});
}
if (thing->thingClassId() == tempoConnectionThingClassId) {
Tempo *tempo = m_tempoConnections.value(thing);
tempo->getAccounts();
} else if (thing->thingClassId() == accountThingClassId) {
}
}
@ -147,6 +251,41 @@ void IntegrationPluginTempo::thingRemoved(Thing *thing)
qCDebug(dcTempo()) << "Thing removed" << thing->name();
}
void IntegrationPluginTempo::onConnectionChanged(bool connected)
{
Tempo *tempo = static_cast<Tempo *>(sender());
Thing *thing = m_tempoConnections.key(tempo);
if (!thing)
return;
thing->setStateValue(tempoConnectionConnectedStateTypeId, connected);
if (!connected) {
Q_FOREACH(Thing *child, myThings().filterByParentId(thing->id())) {
child->setStateValue(accountConnectedStateTypeId, connected);
}
}
}
void IntegrationPluginTempo::onAuthenticationStatusChanged(bool authenticated)
{
qCDebug(dcTempo()) << "Authentication changed" << authenticated;
Tempo *tempoConnection = static_cast<Tempo *>(sender());
Thing *thing = m_tempoConnections.key(tempoConnection);
if (!thing)
return;
thing->setStateValue(tempoConnectionLoggedInStateTypeId, authenticated);
if (!authenticated) {
//refresh access token needs to be refreshed
pluginStorage()->beginGroup(thing->id().toString());
QByteArray refreshToken = pluginStorage()->value("refresh_token").toByteArray();
pluginStorage()->endGroup();
tempoConnection->getAccessTokenFromRefreshToken(refreshToken);
}
}
void IntegrationPluginTempo::onReceivedAccounts(const QList<Tempo::Account> &accounts)
{
qCDebug(dcTempo()) << "Received" << accounts.count() << "accounts";
@ -160,13 +299,13 @@ void IntegrationPluginTempo::onReceivedAccounts(const QList<Tempo::Account> &acc
Q_FOREACH(Tempo::Account account, accounts) {
ThingClassId thingClassId;
Thing * existingThing = myThings().findByParams(ParamList() << Param(m_idParamTypeIds.value(thingClassId), appliance.homeApplianceId));
if (existingThing) {
qCDebug(dcTempo()) << "Thing is already added to system" << existingThing->name();
//Thing * existingThing = myThings().findByParams(ParamList() << Param(m_idParamTypeIds.value(thingClassId), appliance.homeApplianceId));
//if (existingThing) {
// qCDebug(dcTempo()) << "Thing is already added to system" << existingThing->name();
//Set connected state;
//existingThing->setStateValue(m_connectedStateTypeIds.value(thingClassId), appliance.connected);
continue;
}
// continue;
// }
qCDebug(dcTempo()) << "Found new account:" << account.name << "key:" << account.key << "id:" << account.id;
ThingDescriptor descriptor(thingClassId, account.name, account.key, parentThing->id());
@ -179,3 +318,16 @@ void IntegrationPluginTempo::onReceivedAccounts(const QList<Tempo::Account> &acc
emit autoThingsAppeared(desciptors);
}
void IntegrationPluginTempo::onAccountWorkloadReceived(const QString &accountKey, QList<Tempo::Worklog> workloads)
{
Thing *thing = myThings().findByParams(ParamList() << Param(accountThingKeyParamTypeId, accountKey));
if (!thing)
return;
uint totalTimeSpentSeconds = 0;
Q_FOREACH(Tempo::Worklog workload, workloads) {
totalTimeSpentSeconds += workload.timeSpentSeconds;
}
thing->setStateValue(accountTotalTimeSpentStateTypeId, totalTimeSpentSeconds/60);
}

View File

@ -50,6 +50,7 @@ public:
void confirmPairing(ThingPairingInfo *info, const QString &username, const QString &secret) override;
void setupThing(ThingSetupInfo *info) override;
void postSetupThing(Thing *thing) override;
void executeAction(ThingActionInfo *info) override;
void thingRemoved(Thing *thing) override;
@ -62,7 +63,8 @@ private:
private slots:
void onConnectionChanged(bool connected);
void onAuthenticationStatusChanged(bool authenticated);
void onRequestExecuted(QUuid requestId, bool success);
void onReceivedAccounts(const QList<Tempo::Account> &accounts);
void onAccountWorkloadReceived(const QString &accountKey, QList<Tempo::Worklog> workloads);
};
#endif // INTEGRATIONPLUGINTEMPO_H

View File

@ -87,9 +87,9 @@
"paramTypes": [
{
"id": "c6aeddae-56af-496d-a419-1635ff9bae50",
"name": "id",
"displayName": "ID",
"defaultValue": "-",
"name": "key",
"displayName": "Key",
"defaultValue": "",
"type": "QString"
}
],
@ -163,6 +163,24 @@
"displayNameEvent": "Customer changed",
"defaultValue": "",
"type": "QString"
},
{
"id": "1ac39002-56a1-4911-aa68-9d14e142edae",
"name": "totalTimeSpent",
"displayName": "Total time spent",
"displayNameEvent": "Total time spent changed",
"defaultValue": 0,
"type": "uint",
"unit": "Minutes"
},
{
"id": "81bec4e8-9fd3-43d1-b339-2a7fdd83e8cb",
"name": "monthTimeSpent",
"displayName": "This month time spent",
"displayNameEvent": "This month time spent changed",
"defaultValue": 0,
"type": "uint",
"unit": "Minutes"
}
]
}

View File

@ -35,7 +35,7 @@
#include "extern-plugininfo.h"
Tempo::Tempo(NetworkAccessManager *networkmanager, const QByteArray &clientId, const QByteArray &clientSecret, QObject *parent) :
QObject(parent),
QObject(parent),
m_clientId(clientId),
m_clientSecret(clientSecret),
m_networkManager(networkmanager)
@ -56,20 +56,140 @@ QByteArray Tempo::refreshToken()
return m_refreshToken;
}
QUrl Tempo::getLoginUrl(const QUrl &redirectUrl, const QString &jiraCloudInstanceName, const QByteArray &clientId)
QUrl Tempo::getLoginUrl(const QUrl &redirectUrl, const QString &jiraCloudInstanceName)
{
if (m_clientId.isEmpty()) {
qWarning(dcTempo) << "Client Id not defined!";
return QUrl("");
}
if (redirectUrl.isEmpty()){
qWarning(dcTempo) << "No redirect uri defined!";
}
m_redirectUri = QUrl::toPercentEncoding(redirectUrl.toString());
QUrl url;
url.setScheme("https");
url.setHost(jiraCloudInstanceName+"atlassian.net");
url.setHost(jiraCloudInstanceName+".atlassian.net");
url.setPath("/plugins/servlet/ac/io.tempo.jira/oauth-authorize/");
QUrlQuery query;
query.addQueryItem("client_id", clientId);
query.addQueryItem("redirect_uri", redirectUrl.toString());
query.addQueryItem("client_id", m_clientId);
query.addQueryItem("redirect_uri", m_redirectUri);
query.addQueryItem("access_type", "tenant_user");
url.setQuery(query);
return url;
}
void Tempo::getAccessTokenFromRefreshToken(const QByteArray &refreshToken)
{
if (refreshToken.isEmpty()) {
qWarning(dcTempo) << "No refresh token given!";
setAuthenticated(false);
return;
}
QUrl url(m_baseTokenUrl);
QUrlQuery query;
query.clear();
query.addQueryItem("grant_type", "refresh_token");
query.addQueryItem("refresh_token", refreshToken);
query.addQueryItem("client_secret", m_clientSecret);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply *reply = m_networkManager->post(request, query.toString().toUtf8());
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [this, reply](){
QByteArray rawData = reply->readAll();
if (!checkStatusCode(reply, rawData)) {
return;
}
QJsonDocument data = QJsonDocument::fromJson(rawData);
if(!data.toVariant().toMap().contains("access_token")) {
setAuthenticated(false);
return;
}
m_accessToken = data.toVariant().toMap().value("access_token").toByteArray();
emit receivedAccessToken(m_accessToken);
if (data.toVariant().toMap().contains("expires_in")) {
int expireTime = data.toVariant().toMap().value("expires_in").toInt();
qCDebug(dcTempo) << "Access token expires int" << expireTime << "s, at" << QDateTime::currentDateTime().addSecs(expireTime).toString();
if (!m_tokenRefreshTimer) {
qWarning(dcTempo()) << "Access token refresh timer not initialized";
return;
}
if (expireTime < 20) {
qCWarning(dcTempo()) << "Expire time too short";
return;
}
m_tokenRefreshTimer->start((expireTime - 20) * 1000);
}
});
}
void Tempo::getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode)
{
// Obtaining access token
if(authorizationCode.isEmpty())
qWarning(dcTempo()) << "No authorization code given!";
if(m_clientId.isEmpty())
qWarning(dcTempo()) << "Client key not set!";
if(m_clientSecret.isEmpty())
qWarning(dcTempo()) << "Client secret not set!";
QUrl url = QUrl(m_baseTokenUrl);
QUrlQuery query; url.setQuery(query);
query.clear();
query.addQueryItem("client_id", m_clientId);
query.addQueryItem("client_secret", m_clientSecret);
query.addQueryItem("redirect_uri", m_redirectUri);
query.addQueryItem("grant_type", "authorization_code");
query.addQueryItem("code", authorizationCode);
// query.addQueryItem("code_verifier", m_codeChallenge);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply *reply = m_networkManager->post(request, query.toString().toUtf8());
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [this, reply](){
QByteArray rawData = reply->readAll();
if (!checkStatusCode(reply, rawData)) {
return;
}
QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData);
if(!jsonDoc.toVariant().toMap().contains("access_token") || !jsonDoc.toVariant().toMap().contains("refresh_token") ) {
setAuthenticated(false);
return;
}
m_accessToken = jsonDoc.toVariant().toMap().value("access_token").toByteArray();
receivedAccessToken(m_accessToken);
m_refreshToken = jsonDoc.toVariant().toMap().value("refresh_token").toByteArray();
receivedRefreshToken(m_refreshToken);
if (jsonDoc.toVariant().toMap().contains("expires_in")) {
int expireTime = jsonDoc.toVariant().toMap().value("expires_in").toInt();
qCDebug(dcTempo()) << "Token expires in" << expireTime << "s, at" << QDateTime::currentDateTime().addSecs(expireTime).toString();
if (!m_tokenRefreshTimer) {
qWarning(dcTempo()) << "Token refresh timer not initialized";
setAuthenticated(false);
return;
}
if (expireTime < 20) {
qCWarning(dcTempo()) << "Expire time too short";
return;
}
m_tokenRefreshTimer->start((expireTime - 20) * 1000);
}
});
}
void Tempo::getAccounts()
{
QUrl url = QUrl(m_baseControlUrl+"/accounts");
@ -130,12 +250,52 @@ void Tempo::getAccounts()
void Tempo::getWorkloadByAccount(const QString &accountKey, QDate from, QDate to)
{
QUrl url = QUrl(m_baseControlUrl+"/worklogs/account/"+accountKey);
QUrlQuery query;
query.addQueryItem("from", from.toString(Qt::DateFormat::ISODate));
query.addQueryItem("to", to.toString(Qt::DateFormat::ISODate));
url.setQuery(query);
QNetworkRequest request(url);
request.setRawHeader("Authorization", "Bearer "+m_accessToken);
QNetworkReply *reply = m_networkManager->get(request);
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
connect(reply, &QNetworkReply::finished, this, [this, accountKey, reply]{
QByteArray rawData = reply->readAll();
if (!checkStatusCode(reply, rawData)) {
return;
}
QVariantMap dataMap = QJsonDocument::fromJson(rawData).toVariant().toMap();
QVariantList worklogList = dataMap.value("results").toList();
QList<Worklog> worklogs;
Q_FOREACH(QVariant var, worklogList) {
QVariantMap map = var.toMap();
Worklog worklog;
worklog.self = map["self"].toString();
worklog.tempoWorklogId = map["tempoWorklogId"].toInt();
worklog.jiraWorklogId = map["jiraWorklogId"].toInt();
worklog.issue = map["issue"].toMap().value("key").toString();
worklog.timeSpentSeconds = map["timeSpentSeconds"].toInt();
//TODO startDate: required (date-only)
//TODO startTime: required (time-only)
worklog.description = map["description"].toString();
//TODO createdAt: required (datetime)
//TODO updatedAt: required (datetime)
worklog.authorAccountId = map["author"].toMap().value("accountId").toString();
worklog.authorDisplayName = map["author"].toMap().value("displayName").toString();
worklogs.append(worklog);
}
if (!worklogs.isEmpty())
emit accountWorklogsReceived(accountKey, worklogs);
});
}
void Tempo::onRefreshTimer()
{
qCDebug(dcTempo()) << "Refresh authentication token";
getAccessTokenFromRefreshToken(m_refreshToken);
}
void Tempo::setAuthenticated(bool state)
@ -153,3 +313,87 @@ void Tempo::setConnected(bool state)
emit connectionChanged(state);
}
}
bool Tempo::checkStatusCode(QNetworkReply *reply, const QByteArray &rawData)
{
// Check for the internet connection
if (reply->error() == QNetworkReply::NetworkError::HostNotFoundError ||
reply->error() == QNetworkReply::NetworkError::UnknownNetworkError ||
reply->error() == QNetworkReply::NetworkError::TemporaryNetworkFailureError) {
qCWarning(dcTempo()) << "Connection error" << reply->errorString();
setConnected(false);
setAuthenticated(false);
return false;
} else {
setConnected(true);
}
int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(rawData, &error);
switch (status){
case 200: //The request was successful. Typically returned for successful GET requests.
case 204: //The request was successful. Typically returned for successful PUT/DELETE requests with no payload.
break;
case 400: //Error occurred (e.g. validation error - value is out of range)
if(!jsonDoc.toVariant().toMap().contains("error")) {
if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_client") {
qWarning(dcTempo()) << "Client token provided doesnt correspond to client that generated auth code.";
}
if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_redirect_uri") {
qWarning(dcTempo()) << "Missing redirect_uri parameter.";
}
if(jsonDoc.toVariant().toMap().value("error").toString() == "invalid_code") {
qWarning(dcTempo()) << "Expired authorization code.";
}
}
setAuthenticated(false);
return false;
case 401:
qWarning(dcTempo()) << "Client does not have permission to use this API.";
setAuthenticated(false);
return false;
case 403:
qCWarning(dcTempo()) << "Forbidden, Scope has not been granted or home appliance is not assigned to HC account";
setAuthenticated(false);
return false;
case 404:
qCWarning(dcTempo()) << "Not Found. This resource is not available (e.g. no images on washing machine)";
return false;
case 405:
qWarning(dcTempo()) << "Wrong HTTP method used.";
setAuthenticated(false);
return false;
case 408:
qCWarning(dcTempo())<< "Request Timeout, API Server failed to produce an answer or has no connection to backend service";
return false;
case 409:
qCWarning(dcTempo()) << "Conflict - Command/Query cannot be executed for the home appliance, the error response contains the error details";
qCWarning(dcTempo()) << "Error" << jsonDoc;
return false;
case 415:
qCWarning(dcTempo())<< "Unsupported Media Type. The request's Content-Type is not supported";
return false;
case 429:
qCWarning(dcTempo())<< "Too Many Requests, the number of requests for a specific endpoint exceeded the quota of the client";
return false;
case 500:
qCWarning(dcTempo())<< "Internal Server Error, in case of a server configuration error or any errors in resource files";
return false;
case 503:
qCWarning(dcTempo())<< "Service Unavailable,if a required backend service is not available";
return false;
default:
break;
}
if (error.error != QJsonParseError::NoError) {
qCWarning(dcTempo()) << "Received invalide JSON object" << rawData;
qCWarning(dcTempo()) << "Status" << status;
setAuthenticated(false);
return false;
}
setAuthenticated(true);
return true;
}

View File

@ -88,11 +88,25 @@ public:
Customer customer;
};
struct Worklog {
QUrl self;
int tempoWorklogId;
int jiraWorklogId;
QString issue;
int timeSpentSeconds;
QDateTime startedAt;
QString description;
QDateTime createdAt;
QDateTime updatedAt;
QString authorAccountId;
QString authorDisplayName;
};
explicit Tempo(NetworkAccessManager *networkmanager, const QByteArray &clientId, const QByteArray &clientSecret, QObject *parent = nullptr);
QByteArray accessToken();
QByteArray refreshToken();
static QUrl getLoginUrl(const QUrl &redirectUrl, const QString &jiraCloudInstanceName, const QByteArray &clientId);
QUrl getLoginUrl(const QUrl &redirectUrl, const QString &jiraCloudInstanceName);
void getAccessTokenFromRefreshToken(const QByteArray &refreshToken);
void getAccessTokenFromAuthorizationCode(const QByteArray &authorizationCode);
@ -130,6 +144,8 @@ signals:
void receivedRefreshToken(const QByteArray &refreshToken);
void receivedAccessToken(const QByteArray &accessToken);
void accountsReceived(const QList<Account> accounts);
void accountWorklogsReceived(const QString &accountKey, QList<Worklog> worklogs);
};
#endif // TEMPO_H