1 /***********************************************************************
2  *
3  * Copyright (C) 2009, 2013, 2014, 2016 Graeme Gott <graeme@gottcode.org>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  ***********************************************************************/
19 
20 #include "dictionary.h"
21 
22 #include "wordlist.h"
23 
24 #include <QDateTime>
25 #include <QDir>
26 #include <QFile>
27 #include <QFileInfo>
28 #include <QNetworkAccessManager>
29 #include <QNetworkReply>
30 #include <QNetworkRequest>
31 #include <QStandardPaths>
32 #include <QTextStream>
33 #include <QXmlStreamReader>
34 
35 static const QByteArray USER_AGENT = "Connectagram/" + QByteArray(VERSIONSTR) + " (https://gottcode.org/connectagram/; Qt/" + qVersion() + ")";
36 
37 static const QDateTime BUILD_DATE = QLocale::c().toDateTime(QString("%1 %2").arg(__DATE__, __TIME__).simplified(), "MMM d yyyy hh:ss:mm");
38 
Dictionary(const WordList * wordlist,QObject * parent)39 Dictionary::Dictionary(const WordList* wordlist, QObject* parent)
40 : QObject(parent), m_wordlist(wordlist) {
41 	m_url.setScheme("https");
42 	m_url.setPath("/w/api.php");
43 	m_query.addQueryItem("format", "xml");
44 	m_query.addQueryItem("action", "mobileview");
45 	m_query.addQueryItem("sections", "all");
46 	m_query.addQueryItem("noimages", "");
47 
48 	m_manager = new QNetworkAccessManager(this);
49 	connect(m_manager, &QNetworkAccessManager::finished, this, &Dictionary::lookupFinished);
50 
51 	connect(m_wordlist, &WordList::languageChanged, this, &Dictionary::setLanguage);
52 }
53 
54 //-----------------------------------------------------------------------------
55 
lookup(const QString & word)56 void Dictionary::lookup(const QString& word) {
57 	// Check if word exists in cache and is recent
58 	QFileInfo info(m_cache_path + word);
59 	if (info.exists() && (info.lastModified() >= std::max(BUILD_DATE, QDateTime::currentDateTime().addDays(-14)))) {
60 		QFile file(info.absoluteFilePath());
61 		if (file.open(QFile::ReadOnly | QFile::Text)) {
62 			QTextStream stream(&file);
63 			stream.setCodec("UTF-8");
64 			QString definition = stream.readAll();
65 			file.close();
66 			emit wordDefined(word, definition);
67 			return;
68 		}
69 	}
70 
71 	// Look up word
72 	QStringList spellings = m_wordlist->spellings(word);
73 	for (const QString& spelling : spellings) {
74 		m_spellings[spelling] = word;
75 
76 		QNetworkRequest request;
77 		QUrl url = m_url;
78 		QUrlQuery query = m_query;
79 		query.addQueryItem("page", spelling);
80 		url.setQuery(query);
81 		request.setUrl(url);
82 		request.setRawHeader("User-Agent", USER_AGENT);
83 
84 		QNetworkReply* reply = m_manager->get(request);
85 		m_reply_details[reply] = spelling;
86 	}
87 }
88 
89 //-----------------------------------------------------------------------------
90 
wait()91 void Dictionary::wait() {
92 	QHashIterator<QNetworkReply*, QString> i(m_reply_details);
93 	while (i.hasNext()) {
94 		i.key()->abort();
95 	}
96 }
97 
98 //-----------------------------------------------------------------------------
99 
lookupFinished(QNetworkReply * reply)100 void Dictionary::lookupFinished(QNetworkReply* reply) {
101 	// Find word
102 	QString word = m_reply_details.value(reply);
103 	if (!word.isEmpty()) {
104 		m_reply_details.remove(reply);
105 		word = m_spellings.take(word);
106 	}
107 	if (word.isEmpty()) {
108 		qWarning("Unknown lookup");
109 		reply->deleteLater();
110 		return;
111 	}
112 
113 	// Determine if this is the last spelling of a word
114 	bool last_definition = m_spellings.key(word).isEmpty();
115 
116 	// Fetch word definitions
117 	QString definition = m_definitions.value(word);
118 	if (reply->error() == QNetworkReply::NoError) {
119 		QXmlStreamReader xml(reply);
120 		while (!xml.atEnd()) {
121 			xml.readNext();
122 			if (!xml.isStartElement()) {
123 				continue;
124 			}
125 
126 			if (xml.name() == "section") {
127 				definition += xml.readElementText();
128 			} else if (xml.name() == "error") {
129 				xml.raiseError();
130 			}
131 		}
132 
133 		if (!xml.hasError()) {
134 			if (last_definition) {
135 				definition += "<p align=\"right\">" + tr("Definition from Wiktionary, the free dictionary") + "</p>";
136 				m_definitions.remove(word);
137 			} else {
138 				definition += "<hr>";
139 				m_definitions[word] = definition;
140 			}
141 		} else if (last_definition && definition.isEmpty()) {
142 			definition += tr("No definition found");
143 		}
144 	} else {
145 		definition = tr("Unable to connect to Wiktionary");
146 		last_definition = true;
147 	}
148 
149 	// Show spelling if definition is done
150 	if (last_definition) {
151 		emit wordDefined(word, definition);
152 
153 		// Save word to cache
154 		QFile file(m_cache_path + word);
155 		if (file.open(QFile::WriteOnly | QFile::Text)) {
156 			QTextStream stream(&file);
157 			stream.setCodec("UTF-8");
158 			stream << definition;
159 			file.close();
160 		}
161 	}
162 	reply->deleteLater();
163 }
164 
165 //-----------------------------------------------------------------------------
166 
setLanguage(const QString & langcode)167 void Dictionary::setLanguage(const QString& langcode) {
168 	m_url.setHost(langcode + ".wiktionary.org");
169 
170 	// Find cache path
171 	m_cache_path = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
172 	m_cache_path += "/" + langcode + "/";
173 
174 	// Create cache directory
175 	QDir dir(m_cache_path);
176 	dir.mkpath(dir.absolutePath());
177 }
178