File indexing completed on 2024-04-21 16:12:46

0001 /*
0002     SPDX-FileCopyrightText: 2007 Jeff Cooper <weirdsox11@gmail.com>
0003     SPDX-FileCopyrightText: 2007 Thomas Georgiou <TAGeorgiou@gmail.com>
0004     SPDX-FileCopyrightText: 2022 Alexander Lohnau <alexander.lohnau@gmx.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-only
0007 */
0008 
0009 #include "dictengine.h"
0010 
0011 #include <chrono>
0012 
0013 #include <KLocalizedString>
0014 #include <QDebug>
0015 #include <QRegularExpression>
0016 #include <QUrl>
0017 
0018 using namespace std::chrono_literals;
0019 
0020 DictEngine::DictEngine(QObject *parent)
0021     : QObject(parent)
0022     , m_dictNames{QByteArrayLiteral("wn")} // In case we need to switch it later
0023     , m_serverName(QStringLiteral("dict.org")) // Default, good dictionary
0024     , m_definitionResponses{
0025           QByteArrayLiteral("250"), /**< ok (optional timing information here) */
0026           QByteArrayLiteral("550"), /**< Invalid database */
0027           QByteArrayLiteral("501"), /**< Syntax error, illegal parameters */
0028           QByteArrayLiteral("503"), /**< Command parameter not implemented */
0029       }
0030 {
0031     m_definitionTimer.setInterval(30s);
0032     m_definitionTimer.setSingleShot(true);
0033     connect(&m_definitionTimer, &QTimer::timeout, this, &DictEngine::slotDefinitionReadFinished);
0034 }
0035 
0036 DictEngine::~DictEngine()
0037 {
0038 }
0039 
0040 void DictEngine::setDict(const QByteArray &dict)
0041 {
0042     m_dictNames = dict.split(',');
0043 }
0044 
0045 void DictEngine::setServer(const QString &server)
0046 {
0047     m_serverName = server;
0048 }
0049 
0050 static QString wnToHtml(const QString &word, QByteArray &text)
0051 {
0052     QList<QByteArray> splitText = text.split('\n');
0053     QString def;
0054     def += QLatin1String("<dl>\n");
0055     static QRegularExpression linkRx(QStringLiteral("{(.*?)}"));
0056 
0057     bool isFirst = true;
0058     while (!splitText.empty()) {
0059         // 150 n definitions retrieved - definitions follow
0060         // 151 word database name - text follows
0061         // 250 ok (optional timing information here)
0062         // 552 No match
0063         QString currentLine = splitText.takeFirst();
0064         if (currentLine.startsWith(QLatin1String("151"))) {
0065             isFirst = true;
0066             continue;
0067         }
0068 
0069         if (currentLine.startsWith('.')) {
0070             def += QLatin1String("</dd>");
0071             continue;
0072         }
0073 
0074         // Don't early return if there are multiple dicts
0075         if (currentLine.startsWith("552") || currentLine.startsWith("501")) {
0076             def += QStringLiteral("<dt><b>%1</b></dt>\n<dd>%2</dd>").arg(word, i18n("No match found for %1", word));
0077             continue;
0078         }
0079 
0080         if (!(currentLine.startsWith(QLatin1String("150")) || currentLine.startsWith(QLatin1String("151")) || currentLine.startsWith(QLatin1String("250")))) {
0081             // Handle links
0082             int offset = 0;
0083             QRegularExpressionMatchIterator it = linkRx.globalMatch(currentLine);
0084             while (it.hasNext()) {
0085                 QRegularExpressionMatch match = it.next();
0086                 QUrl url;
0087                 url.setScheme("dict");
0088                 url.setPath(match.captured(1));
0089                 const QString linkText = QStringLiteral("<a href=\"%1\">%2</a>").arg(url.toString(), match.captured(1));
0090                 currentLine.replace(match.capturedStart(0) + offset, match.capturedLength(0), linkText);
0091                 offset += linkText.length() - match.capturedLength(0);
0092             }
0093 
0094             if (isFirst) {
0095                 def += "<dt><b>" + currentLine + "</b></dt>\n<dd>";
0096                 isFirst = false;
0097                 continue;
0098             } else {
0099                 static QRegularExpression newLineRx(QStringLiteral("([1-9]{1,2}:)"));
0100                 if (currentLine.contains(newLineRx)) {
0101                     def += QLatin1String("\n<br>\n");
0102                 }
0103                 static QRegularExpression makeMeBoldRx(QStringLiteral("^([\\s\\S]*[1-9]{1,2}:)"));
0104                 currentLine.replace(makeMeBoldRx, QLatin1String("<b>\\1</b>"));
0105                 def += currentLine;
0106                 continue;
0107             }
0108         }
0109     }
0110 
0111     def += QLatin1String("</dl>");
0112     return def;
0113 }
0114 
0115 void DictEngine::getDefinition()
0116 {
0117     // One-time connection
0118     disconnect(m_tcpSocket, &QTcpSocket::readyRead, this, &DictEngine::getDefinition);
0119 
0120     // Clear the old data to prepare for a new lookup
0121     m_definitionData.clear();
0122 
0123     connect(m_tcpSocket, &QTcpSocket::readyRead, this, &DictEngine::slotDefinitionReadyRead);
0124 
0125     m_tcpSocket->readAll();
0126 
0127     // Command Pipelining: https://datatracker.ietf.org/doc/html/rfc2229#section-4
0128     QByteArray command;
0129     for (const QByteArray &dictName : std::as_const(m_dictNames)) {
0130         command += QByteArrayLiteral("DEFINE ") + dictName + QByteArrayLiteral(" \"") + m_currentWord.toUtf8() + QByteArrayLiteral("\"\n");
0131     }
0132 
0133     m_tcpSocket->write(command);
0134     m_tcpSocket->flush();
0135 
0136     m_definitionTimer.start();
0137 }
0138 
0139 void DictEngine::getDicts()
0140 {
0141     m_tcpSocket->readAll();
0142     QByteArray ret;
0143 
0144     m_tcpSocket->write(QByteArray("SHOW DB\n"));
0145     m_tcpSocket->flush();
0146 
0147     if (m_tcpSocket->waitForReadyRead()) {
0148         while (!ret.contains("250") && !ret.contains("420") && !ret.contains("421") && m_tcpSocket->waitForReadyRead()) {
0149             ret += m_tcpSocket->readAll();
0150         }
0151     }
0152 
0153     QMap<QString, QString> availableDicts;
0154     const QList<QByteArray> retLines = ret.split('\n');
0155     for (const QByteArray &curr : retLines) {
0156         if (curr.endsWith("420") || curr.startsWith("421")) {
0157             // TODO: what happens if the server is down
0158         }
0159         if (curr.startsWith("554")) {
0160             // TODO: What happens if no DB available?
0161             // TODO: Eventually there will be functionality to change the server...
0162             break;
0163         }
0164 
0165         // ignore status code and empty lines
0166         if (curr.startsWith("250") || curr.startsWith("110") || curr.isEmpty()) {
0167             continue;
0168         }
0169 
0170         if (!curr.startsWith('-') && !curr.startsWith('.')) {
0171             const QString line = QString::fromUtf8(curr).trimmed();
0172             const QString id = line.section(' ', 0, 0);
0173             QString description = line.section(' ', 1);
0174             if (description.startsWith('"') && description.endsWith('"')) {
0175                 description.remove(0, 1);
0176                 description.chop(1);
0177             }
0178             availableDicts.insert(id, description);
0179         }
0180     }
0181 
0182     m_tcpSocket->disconnectFromHost();
0183     m_availableDictsCache.insert(m_serverName, availableDicts);
0184     Q_EMIT dictsRecieved(availableDicts);
0185     Q_EMIT dictLoadingChanged(false);
0186 }
0187 
0188 void DictEngine::requestDicts()
0189 {
0190     if (m_availableDictsCache.contains(m_serverName)) {
0191         Q_EMIT dictsRecieved(m_availableDictsCache.value(m_serverName));
0192         return;
0193     }
0194     if (m_tcpSocket) {
0195         m_tcpSocket->abort(); // stop if lookup is in progress and new query is requested
0196         m_tcpSocket->deleteLater();
0197         m_tcpSocket = nullptr;
0198     }
0199 
0200     Q_EMIT dictLoadingChanged(true);
0201     m_tcpSocket = new QTcpSocket(this);
0202     connect(m_tcpSocket, &QTcpSocket::disconnected, this, &DictEngine::socketClosed);
0203     connect(m_tcpSocket, &QTcpSocket::errorOccurred, this, [this] {
0204         Q_EMIT dictErrorOccurred(m_tcpSocket->error(), m_tcpSocket->errorString());
0205         socketClosed();
0206     });
0207     connect(m_tcpSocket, &QTcpSocket::readyRead, this, &DictEngine::getDicts);
0208     m_tcpSocket->connectToHost(m_serverName, 2628);
0209 }
0210 
0211 void DictEngine::requestDefinition(const QString &query)
0212 {
0213     if (m_tcpSocket) {
0214         m_definitionTimer.stop();
0215         m_tcpSocket->abort(); // stop if lookup is in progress and new query is requested
0216         // Delete now to fix "Unexpected null receiver"
0217         delete m_tcpSocket;
0218         m_tcpSocket = nullptr;
0219     }
0220 
0221     QStringList queryParts = query.split(':', Qt::SkipEmptyParts);
0222     if (queryParts.isEmpty()) {
0223         return;
0224     }
0225 
0226     m_currentWord = queryParts.last();
0227     m_currentQuery = query;
0228 
0229     // asked for a dictionary?
0230     if (queryParts.count() > 1) {
0231         setDict(queryParts[queryParts.count() - 2].toLatin1());
0232         // default to wordnet
0233     } else {
0234         setDict(QByteArrayLiteral("wn"));
0235     }
0236 
0237     // asked for a server?
0238     if (queryParts.count() > 2) {
0239         setServer(queryParts[queryParts.count() - 3]);
0240         // default to wordnet
0241     } else {
0242         setServer(QStringLiteral("dict.org"));
0243     }
0244     m_tcpSocket = new QTcpSocket(this);
0245     connect(m_tcpSocket, &QTcpSocket::disconnected, this, &DictEngine::socketClosed);
0246     connect(m_tcpSocket, &QTcpSocket::errorOccurred, this, [this] {
0247         Q_EMIT dictErrorOccurred(m_tcpSocket->error(), m_tcpSocket->errorString());
0248         socketClosed();
0249     });
0250     connect(m_tcpSocket, &QTcpSocket::readyRead, this, &DictEngine::getDefinition);
0251     m_tcpSocket->connectToHost(m_serverName, 2628);
0252 }
0253 
0254 void DictEngine::slotDefinitionReadyRead()
0255 {
0256     m_definitionData += m_tcpSocket->readAll();
0257 
0258     const bool finished = std::any_of(m_definitionResponses.cbegin(), m_definitionResponses.cend(), [this](const QByteArray &code) {
0259         return m_definitionData.contains(code);
0260     });
0261 
0262     if (finished) {
0263         slotDefinitionReadFinished();
0264         return;
0265     }
0266 
0267     // Close the socket after 30s inactivity
0268     m_definitionTimer.start();
0269 }
0270 
0271 void DictEngine::slotDefinitionReadFinished()
0272 {
0273     m_definitionTimer.stop();
0274 
0275     const QString html = wnToHtml(m_currentWord, m_definitionData);
0276     Q_EMIT definitionRecieved(html);
0277 
0278     m_tcpSocket->disconnectFromHost();
0279     socketClosed();
0280 }
0281 
0282 void DictEngine::socketClosed()
0283 {
0284     Q_EMIT dictLoadingChanged(false);
0285 
0286     if (m_tcpSocket) {
0287         m_tcpSocket->deleteLater();
0288     }
0289     m_tcpSocket = nullptr;
0290 }