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 }