hafasmgatebackend.cpp 13.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
/*
    Copyright (C) 2018 Volker Krause <vkrause@kde.org>

    This program is free software; you can redistribute it and/or modify it
    under the terms of the GNU Library General Public License as published by
    the Free Software Foundation; either version 2 of the License, or (at your
    option) any later version.

    This program 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 Library General Public
    License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include "hafasmgatebackend.h"
19 20
#include "hafasmgateparser.h"
#include "logging.h"
21
#include "cache.h"
22

23
#include <KPublicTransport/Departure>
24 25 26
#include <KPublicTransport/DepartureReply>
#include <KPublicTransport/DepartureRequest>
#include <KPublicTransport/Location>
27 28
#include <KPublicTransport/LocationReply>
#include <KPublicTransport/LocationRequest>
29

30
#include <QCryptographicHash>
31 32 33 34 35
#include <QDateTime>
#include <QDebug>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
36
#include <QMetaEnum>
37 38 39
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
40
#include <QUrlQuery>
41 42 43 44 45

using namespace KPublicTransport;

HafasMgateBackend::HafasMgateBackend() = default;

46 47 48 49 50
bool HafasMgateBackend::isSecure() const
{
    return m_endpoint.startsWith(QLatin1String("https"));
}

51 52
bool HafasMgateBackend::queryJourney(JourneyReply *reply, QNetworkAccessManager *nam) const
{
53
    m_parser.setLocationIdentifierType(locationIdentifierType());
54 55 56 57 58
    return false;
}

bool HafasMgateBackend::queryDeparture(DepartureReply *reply, QNetworkAccessManager *nam) const
{
59
    m_parser.setLocationIdentifierType(locationIdentifierType());
60
    const auto request = reply->request();
61

62
    const auto id = request.stop().identifier(locationIdentifierType());
63 64 65 66 67 68 69 70 71 72
    if (!id.isEmpty()) {
        queryDeparture(reply, id, nam);
        return true;
    }

    // missing the station id
    LocationRequest locReq;
    locReq.setCoordinate(request.stop().latitude(), request.stop().longitude());
    locReq.setName(request.stop().name());
    // TODO set max result = 1
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89

    // check if this location query is cached already
    const auto cacheEntry = Cache::lookupLocation(backendId(), locReq.cacheKey());
    switch (cacheEntry.type) {
        case CacheHitType::Negative:
            addError(reply, Reply::NotFoundError, {});
            return false;
        case CacheHitType::Positive:
            if (cacheEntry.data.size() >= 1) {
                queryDeparture(reply, cacheEntry.data[0].identifier(locationIdentifierType()), nam);
                return true;
            }
            break;
        case CacheHitType::Miss:
            break;
    }

90 91
    const auto locReply = postLocationQuery(locReq, nam);
    if (!locReply) {
92 93
        return false;
    }
94
    QObject::connect(locReply, &QNetworkReply::finished, [this, reply, locReply, locReq, nam]() {
95 96 97 98 99 100
        qDebug() << locReply->request().url();
        switch (locReply->error()) {
            case QNetworkReply::NoError:
            {
                auto res = m_parser.parseLocations(locReply->readAll());
                if (m_parser.error() == Reply::NoError && !res.empty()) {
101
                    Cache::addLocationCacheEntry(backendId(), locReq.cacheKey(), res);
102 103 104 105 106 107 108
                    const auto id = res[0].identifier(locationIdentifierType());
                    if (!id.isEmpty()) {
                        queryDeparture(reply, id, nam);
                    } else {
                        addError(reply, Reply::NotFoundError, QLatin1String("Location query found no results."));
                    }
                } else {
109
                    Cache::addNegativeLocationCacheEntry(backendId(), locReq.cacheKey());
110 111 112 113 114 115 116 117 118 119 120
                    addError(reply, m_parser.error(), m_parser.errorMessage());
                }
                break;
            }
            default:
                addError(reply, Reply::NetworkError, locReply->errorString());
                qCDebug(Log) << locReply->error() << locReply->errorString();
                break;
        }
        locReply->deleteLater();
    });
121

122 123 124 125 126 127 128
    return true;
}

void HafasMgateBackend::queryDeparture(DepartureReply *reply, const QString &locationId, QNetworkAccessManager *nam) const
{
    const auto request = reply->request();

129 130 131 132 133 134 135 136 137 138 139
    QJsonObject stationBoard;
    {
        QJsonObject cfg;
        cfg.insert(QLatin1String("polyEnc"), QLatin1String("GPA"));

        QJsonObject req;
        req.insert(QLatin1String("date"), request.dateTime().toString(QLatin1String("yyyyMMdd")));
        req.insert(QLatin1String("maxJny"), 12);
        req.insert(QLatin1String("stbFltrEquiv"), true);

        QJsonObject stbLoc;
140
        stbLoc.insert(QLatin1String("extId"), locationId);
141 142 143 144 145
        stbLoc.insert(QLatin1String("state"), QLatin1String("F"));
        stbLoc.insert(QLatin1String("type"), QLatin1String("S"));

        req.insert(QLatin1String("stbLoc"), stbLoc);
        req.insert(QLatin1String("time"), request.dateTime().toString(QLatin1String("hhmmss")));
146
        req.insert(QLatin1String("type"), request.mode() == DepartureRequest::QueryDeparture ? QLatin1String("DEP") : QLatin1String("ARR"));
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176

        stationBoard.insert(QLatin1String("cfg"), cfg);
        stationBoard.insert(QLatin1String("meth"), QLatin1String("StationBoard"));
        stationBoard.insert(QLatin1String("req"), req);
    }

    auto netReply = postRequest(stationBoard, nam);
    QObject::connect(netReply, &QNetworkReply::finished, [netReply, reply, this]() {
        qDebug() << netReply->request().url();
        switch (netReply->error()) {
            case QNetworkReply::NoError:
            {
                auto result = m_parser.parseDepartures(netReply->readAll());
                if (m_parser.error() != Reply::NoError) {
                    addError(reply, m_parser.error(), m_parser.errorMessage());
                    qCDebug(Log) << m_parser.error() << m_parser.errorMessage();
                } else {
                    addResult(reply, std::move(result));
                }
                break;
            }
            default:
                addError(reply, Reply::NetworkError, netReply->errorString());
                qCDebug(Log) << netReply->error() << netReply->errorString();
                break;
        }
        netReply->deleteLater();
    });
}

177 178
bool HafasMgateBackend::queryLocation(LocationReply *reply, QNetworkAccessManager *nam) const
{
179
    m_parser.setLocationIdentifierType(locationIdentifierType());
180

181
    const auto req = reply->request();
182 183
    const auto netReply = postLocationQuery(req, nam);
    if (!netReply) {
184 185 186 187 188 189 190 191
        return false;
    }

    QObject::connect(netReply, &QNetworkReply::finished, [netReply, reply, this]() {
        qDebug() << netReply->request().url();
        switch (netReply->error()) {
            case QNetworkReply::NoError:
            {
Volker Krause's avatar
Volker Krause committed
192 193
                auto res = m_parser.parseLocations(netReply->readAll());
                if (m_parser.error() == Reply::NoError) {
194
                    Cache::addLocationCacheEntry(backendId(), reply->request().cacheKey(), res);
Volker Krause's avatar
Volker Krause committed
195 196
                    addResult(reply, std::move(res));
                } else {
197
                    Cache::addNegativeLocationCacheEntry(backendId(), reply->request().cacheKey());
Volker Krause's avatar
Volker Krause committed
198 199
                    addError(reply, m_parser.error(), m_parser.errorMessage());
                }
200 201 202 203 204 205 206 207 208 209 210 211 212
                break;
            }
            default:
                addError(reply, Reply::NetworkError, netReply->errorString());
                qCDebug(Log) << netReply->error() << netReply->errorString();
                break;
        }
        netReply->deleteLater();
    });

    return true;
}

213 214
QNetworkReply* HafasMgateBackend::postRequest(const QJsonObject &svcReq, QNetworkAccessManager *nam) const
{
215 216 217 218 219 220 221 222 223 224 225
    QJsonObject top;
    {
        QJsonObject auth;
        auth.insert(QLatin1String("aid"), m_aid);
        auth.insert(QLatin1String("type"), QLatin1String("AID"));
        top.insert(QLatin1String("auth"), auth);
    }
    {
        QJsonObject client;
        client.insert(QLatin1String("id"), m_clientId);
        client.insert(QLatin1String("type"), m_clientType);
226 227 228 229 230 231
        if (!m_clientVersion.isEmpty()) {
            client.insert(QLatin1String("v"), m_clientVersion);
        }
        if (!m_clientName.isEmpty()) {
            client.insert(QLatin1String("name"), m_clientName);
        }
232 233 234 235 236
        top.insert(QLatin1String("client"), client);
    }
    top.insert(QLatin1String("formatted"), false);
    top.insert(QLatin1String("lang"), QLatin1String("eng"));
    {
237
        QJsonArray svcReqs;
238 239 240 241 242 243 244 245 246
        {
            QJsonObject req;
            req.insert(QLatin1String("getServerDateTime"), true);
            req.insert(QLatin1String("getTimeTablePeriod"), false);

            QJsonObject serverInfo;
            serverInfo.insert(QLatin1String("meth"), QLatin1String("ServerInfo"));
            serverInfo.insert(QLatin1String("req"), req);

247
            svcReqs.push_back(serverInfo);
248
        }
249 250
        svcReqs.push_back(svcReq);
        top.insert(QLatin1String("svcReqL"), svcReqs);
251 252 253
    }
    top.insert(QLatin1String("ver"), m_version);

254
    const auto content = QJsonDocument(top).toJson(QJsonDocument::Compact);
255
    QUrl url(m_endpoint);
256
    QUrlQuery query;
257 258 259 260 261 262 263 264 265 266 267 268
    if (!m_micMacSalt.isEmpty()) {
        QCryptographicHash md5(QCryptographicHash::Md5);
        md5.addData(content);
        const auto mic = md5.result().toHex();
        query.addQueryItem(QLatin1String("mic"), QString::fromLatin1(mic));

        md5.reset();
        // yes, mic is added as hex-encoded string, and the salt is added as raw bytes
        md5.addData(mic);
        md5.addData(m_micMacSalt);
        query.addQueryItem(QLatin1String("mac"), QString::fromLatin1(md5.result().toHex()));
    }
269 270 271 272 273 274 275
    if (!m_checksumSalt.isEmpty()) {
        QCryptographicHash md5(QCryptographicHash::Md5);
        md5.addData(content);
        md5.addData(m_checksumSalt);
        query.addQueryItem(QLatin1String("checksum"), QString::fromLatin1(md5.result().toHex()));
    }
    url.setQuery(query);
276 277

    auto netReq = QNetworkRequest(url);
278
    netReq.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json"));
279
    qCDebug(Log) << netReq.url();
280
    //qCDebug(Log).noquote() << QJsonDocument(top).toJson();
281
    return nam->post(netReq, content);
282
}
283

284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
QNetworkReply* HafasMgateBackend::postLocationQuery(const LocationRequest &req, QNetworkAccessManager *nam) const
{
    QJsonObject methodObj;
    if (req.hasCoordinate()) {
        QJsonObject cfg;
        cfg.insert(QLatin1String("polyEnc"), QLatin1String("GPA"));

        QJsonObject coord;
        coord.insert(QLatin1String("x"), (int)(req.latitude() * 1000000));
        coord.insert(QLatin1String("y"), (int)(req.longitude() * 1000000));
        QJsonObject ring;
        ring.insert(QLatin1String("cCrd"), coord);
        ring.insert(QLatin1String("maxDist"), 20000); // not sure which unit...

        QJsonObject reqObj;
        reqObj.insert(QLatin1String("ring"), ring);
        // ### make this configurable in LocationRequest
        reqObj.insert(QLatin1String("getStops"), true);
        reqObj.insert(QLatin1String("getPOIs"), false);
        reqObj.insert(QLatin1String("maxLoc"), 12);

        methodObj.insert(QLatin1String("cfg"), cfg);
        methodObj.insert(QLatin1String("meth"), QLatin1String("LocGeoPos"));
        methodObj.insert(QLatin1String("req"), reqObj);

    } else if (!req.name().isEmpty()) {
        QJsonObject cfg;
        cfg.insert(QLatin1String("polyEnc"), QLatin1String("GPA"));

        QJsonObject loc;
        loc.insert(QLatin1String("name"), req.name()); // + '?' for auto completion search?
        loc.insert(QLatin1String("type"), QLatin1String("S")); // station: S, address: A, POI: P

        QJsonObject input;
        input.insert(QLatin1String("field"), QLatin1String("S"));
        input.insert(QLatin1String("loc"), loc);
        // ### make this configurable in LocationRequest
        input.insert(QLatin1String("maxLoc"), 12);

        QJsonObject reqObj;
        reqObj.insert(QLatin1String("input"), input);

        methodObj.insert(QLatin1String("cfg"), cfg);
        methodObj.insert(QLatin1String("meth"), QLatin1String("LocMatch"));
        methodObj.insert(QLatin1String("req"), reqObj);

    } else {
        return nullptr;
    }

    return postRequest(methodObj, nam);
}

337 338 339 340
void HafasMgateBackend::setMicMacSalt(const QString &salt)
{
    m_micMacSalt = QByteArray::fromHex(salt.toUtf8());
}
341

342 343 344 345 346
void HafasMgateBackend::setChecksumSalt(const QString &salt)
{
    m_checksumSalt = QByteArray::fromHex(salt.toUtf8());
}

347 348 349 350 351 352 353 354 355 356 357 358
void HafasMgateBackend::setLineModeMap(const QJsonObject& obj)
{
    const auto idx = Line::staticMetaObject.indexOfEnumerator("Mode");
    Q_ASSERT(idx >= 0);
    const auto me = Line::staticMetaObject.enumerator(idx);

    std::unordered_map<int, Line::Mode> modeMap;
    for (auto it = obj.begin(); it != obj.end(); ++it) {
        modeMap[it.key().toInt()] = static_cast<Line::Mode>(me.keyToValue(it.value().toString().toUtf8()));
    }
    m_parser.setLineModeMap(std::move(modeMap));
}
359 360 361 362 363

QString HafasMgateBackend::locationIdentifierType() const
{
    return m_locationIdentifierType.isEmpty() ? backendId() : m_locationIdentifierType;
}