File indexing completed on 2024-04-28 05:27:16
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 m_currentWord = query; 0222 0223 m_tcpSocket = new QTcpSocket(this); 0224 connect(m_tcpSocket, &QTcpSocket::disconnected, this, &DictEngine::socketClosed); 0225 connect(m_tcpSocket, &QTcpSocket::errorOccurred, this, [this] { 0226 Q_EMIT dictErrorOccurred(m_tcpSocket->error(), m_tcpSocket->errorString()); 0227 socketClosed(); 0228 }); 0229 connect(m_tcpSocket, &QTcpSocket::readyRead, this, &DictEngine::getDefinition); 0230 m_tcpSocket->connectToHost(m_serverName, 2628); 0231 } 0232 0233 void DictEngine::requestDefinition(const QString &query, const QString &server) 0234 { 0235 setServer(server); 0236 requestDefinition(query); 0237 } 0238 0239 void DictEngine::requestDefinition(const QString &query, const QByteArray &dictionary) 0240 { 0241 setDict(dictionary); 0242 requestDefinition(query); 0243 } 0244 0245 void DictEngine::requestDefinition(const QString &query, const QString &server, const QByteArray &dictionary) 0246 { 0247 setServer(server); 0248 setDict(dictionary); 0249 requestDefinition(query); 0250 } 0251 0252 void DictEngine::slotDefinitionReadyRead() 0253 { 0254 m_definitionData += m_tcpSocket->readAll(); 0255 0256 const bool finished = std::any_of(m_definitionResponses.cbegin(), m_definitionResponses.cend(), [this](const QByteArray &code) { 0257 return m_definitionData.contains(code); 0258 }); 0259 0260 if (finished) { 0261 slotDefinitionReadFinished(); 0262 return; 0263 } 0264 0265 // Close the socket after 30s inactivity 0266 m_definitionTimer.start(); 0267 } 0268 0269 void DictEngine::slotDefinitionReadFinished() 0270 { 0271 m_definitionTimer.stop(); 0272 0273 const QString html = wnToHtml(m_currentWord, m_definitionData); 0274 Q_EMIT definitionRecieved(html); 0275 0276 m_tcpSocket->disconnectFromHost(); 0277 socketClosed(); 0278 } 0279 0280 void DictEngine::socketClosed() 0281 { 0282 Q_EMIT dictLoadingChanged(false); 0283 0284 if (m_tcpSocket) { 0285 m_tcpSocket->deleteLater(); 0286 } 0287 m_tcpSocket = nullptr; 0288 }