1 /*
2 This file is part of Choqok, the KDE micro-blogging client
3
4 Copyright (C) 2008-2012 Mehrdad Momeny <mehrdad.momeny@gmail.com>
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, see http://www.gnu.org/licenses/
21
22 */
23
24 #include "gnusocialapisearch.h"
25
26 #include <QDomElement>
27
28 #include <KIO/StoredTransferJob>
29 #include <KLocalizedString>
30
31 #include "twitterapiaccount.h"
32
33 #include "gnusocialapidebug.h"
34
35 const QRegExp GNUSocialApiSearch::m_rId(QLatin1String("tag:.+,[\\d-]+:(\\d+)"));
36 const QRegExp GNUSocialApiSearch::mIdRegExp(QLatin1String("(?:user|(?:.*notice))/([0-9]+)"));
37
GNUSocialApiSearch(QObject * parent)38 GNUSocialApiSearch::GNUSocialApiSearch(QObject *parent): TwitterApiSearch(parent)
39 {
40 qCDebug(CHOQOK);
41 mSearchCode[ReferenceGroup] = QLatin1Char('!');
42 mSearchCode[ToUser] = QLatin1Char('@');
43 mSearchCode[FromUser].clear();
44 mSearchCode[ReferenceHashtag] = QLatin1Char('#');
45
46 mSearchTypes[ReferenceHashtag].first = i18nc("Dents are Identica posts", "Dents Including This Hashtag");
47 mSearchTypes[ReferenceHashtag].second = true;
48
49 mSearchTypes[ReferenceGroup].first = i18nc("Dents are Identica posts", "Dents Including This Group");
50 mSearchTypes[ReferenceGroup].second = false;
51
52 mSearchTypes[FromUser].first = i18nc("Dents are Identica posts", "Dents From This User");
53 mSearchTypes[FromUser].second = false;
54
55 mSearchTypes[ToUser].first = i18nc("Dents are Identica posts", "Dents To This User");
56 mSearchTypes[ToUser].second = false;
57
58 }
59
~GNUSocialApiSearch()60 GNUSocialApiSearch::~GNUSocialApiSearch()
61 {
62
63 }
64
buildUrl(const SearchInfo & searchInfo,QString sinceStatusId,uint count,uint page)65 QUrl GNUSocialApiSearch::buildUrl(const SearchInfo &searchInfo,
66 QString sinceStatusId, uint count, uint page)
67 {
68 qCDebug(CHOQOK);
69
70 QString formattedQuery;
71 switch (searchInfo.option) {
72 case ToUser:
73 formattedQuery = searchInfo.query + QLatin1String("/replies/rss");
74 break;
75 case FromUser:
76 formattedQuery = searchInfo.query + QLatin1String("/rss");
77 break;
78 case ReferenceGroup:
79 formattedQuery = QLatin1String("group/") + searchInfo.query + QLatin1String("/rss");
80 break;
81 case ReferenceHashtag:
82 formattedQuery = searchInfo.query;
83 break;
84 default:
85 formattedQuery = searchInfo.query + QLatin1String("/rss");
86 break;
87 };
88
89 QUrl url;
90 TwitterApiAccount *theAccount = qobject_cast<TwitterApiAccount *>(searchInfo.account);
91 Q_ASSERT(theAccount);
92 if (searchInfo.option == ReferenceHashtag) {
93 url = theAccount->apiUrl();
94 url = url.adjusted(QUrl::StripTrailingSlash);
95 url.setPath(url.path() + QLatin1String("/search.atom"));
96 QUrlQuery urlQuery;
97 urlQuery.addQueryItem(QLatin1String("q"), formattedQuery);
98 if (!sinceStatusId.isEmpty()) {
99 urlQuery.addQueryItem(QLatin1String("since_id"), sinceStatusId);
100 }
101 int cntStr;
102 if (count && count <= 100) { // GNU Social allows max 100 notices
103 cntStr = count;
104 } else {
105 cntStr = 100;
106 }
107 urlQuery.addQueryItem(QLatin1String("rpp"), QString::number(cntStr));
108 if (page > 1) {
109 urlQuery.addQueryItem(QLatin1String("page"), QString::number(page));
110 }
111 url.setQuery(urlQuery);
112 } else {
113 url = QUrl(theAccount->apiUrl().url().remove(QLatin1String("/api"), Qt::CaseInsensitive));
114 url = url.adjusted(QUrl::StripTrailingSlash);
115 url.setPath(url.path() + QLatin1Char('/') + (formattedQuery));
116 }
117 return url;
118 }
119
requestSearchResults(const SearchInfo & searchInfo,const QString & sinceStatusId,uint count,uint page)120 void GNUSocialApiSearch::requestSearchResults(const SearchInfo &searchInfo,
121 const QString &sinceStatusId,
122 uint count, uint page)
123 {
124 qCDebug(CHOQOK);
125 QUrl url = buildUrl(searchInfo, sinceStatusId, count, page);
126 qCDebug(CHOQOK) << url;
127 KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo);
128 if (!job) {
129 qCCritical(CHOQOK) << "Cannot create an http GET request!";
130 return;
131 }
132 mSearchJobs[job] = searchInfo;
133 connect(job, &KIO::StoredTransferJob::result, this,
134 (void (GNUSocialApiSearch::*)(KJob*))&GNUSocialApiSearch::searchResultsReturned);
135 job->start();
136 }
137
searchResultsReturned(KJob * job)138 void GNUSocialApiSearch::searchResultsReturned(KJob *job)
139 {
140 qCDebug(CHOQOK);
141 if (job == nullptr) {
142 qCDebug(CHOQOK) << "job is a null pointer";
143 Q_EMIT error(i18n("Unable to fetch search results."));
144 return;
145 }
146
147 SearchInfo info = mSearchJobs.take(job);
148
149 if (job->error()) {
150 qCCritical(CHOQOK) << "Error:" << job->errorString();
151 Q_EMIT error(i18n("Unable to fetch search results: %1", job->errorString()));
152 return;
153 }
154 KIO::StoredTransferJob *jj = qobject_cast<KIO::StoredTransferJob *>(job);
155 QList<Choqok::Post *> postsList;
156 if (info.option == ReferenceHashtag) {
157 postsList = parseAtom(jj->data());
158 } else {
159 postsList = parseRss(jj->data());
160 }
161
162 qCDebug(CHOQOK) << "Emiting searchResultsReceived()";
163 Q_EMIT searchResultsReceived(info, postsList);
164 }
165
optionCode(int option)166 QString GNUSocialApiSearch::optionCode(int option)
167 {
168 return mSearchCode[option];
169 }
170
parseAtom(const QByteArray & buffer)171 QList< Choqok::Post * > GNUSocialApiSearch::parseAtom(const QByteArray &buffer)
172 {
173 QDomDocument document;
174 QList<Choqok::Post *> statusList;
175
176 document.setContent(buffer);
177
178 QDomElement root = document.documentElement();
179
180 if (root.tagName() != QLatin1String("feed")) {
181 qCDebug(CHOQOK) << "There is no feed element in Atom feed " << buffer.data();
182 return statusList;
183 }
184
185 QDomNode node = root.firstChild();
186 QString timeStr;
187 while (!node.isNull()) {
188 if (node.toElement().tagName() != QLatin1String("entry")) {
189 node = node.nextSibling();
190 continue;
191 }
192
193 QDomNode entryNode = node.firstChild();
194 Choqok::Post *status = new Choqok::Post;
195 status->isPrivate = false;
196
197 while (!entryNode.isNull()) {
198 QDomElement elm = entryNode.toElement();
199 if (elm.tagName() == QLatin1String("id")) {
200 // Fomatting example: "tag:search.twitter.com,2005:1235016836"
201 QString id;
202 if (m_rId.exactMatch(elm.text())) {
203 id = m_rId.cap(1);
204 }
205 /* sscanf( qPrintable( elm.text() ),
206 "tag:search.twitter.com,%*d:%d", &id);*/
207 status->postId = id;
208 } else if (elm.tagName() == QLatin1String("published")) {
209 // Formatting example: "2009-02-21T19:42:39Z"
210 // Need to extract date in similar fashion to dateFromString
211 int year, month, day, hour, minute, second;
212 sscanf(qPrintable(elm.text()),
213 "%d-%d-%dT%d:%d:%d%*s", &year, &month, &day, &hour, &minute, &second);
214 QDateTime recognized(QDate(year, month, day), QTime(hour, minute, second));
215 recognized.setTimeSpec(Qt::UTC);
216 status->creationDateTime = recognized;
217 } else if (elm.tagName() == QLatin1String("title")) {
218 status->content = elm.text();
219 } else if (elm.tagName() == QLatin1String("link")) {
220 if (elm.attribute(QLatin1String("rel")) == QLatin1String("related")) {
221 status->author.profileImageUrl = QUrl::fromUserInput(elm.attribute(QLatin1String("href")));
222 } else if (elm.attribute(QLatin1String("rel")) == QLatin1String("alternate")) {
223 status->link = QUrl::fromUserInput(elm.attribute(QLatin1String("href")));
224 }
225 } else if (elm.tagName() == QLatin1String("author")) {
226 QDomNode userNode = entryNode.firstChild();
227 while (!userNode.isNull()) {
228 if (userNode.toElement().tagName() == QLatin1String("name")) {
229 QString fullName = userNode.toElement().text();
230 int bracketPos = fullName.indexOf(QLatin1Char(' '), 0);
231 QString screenName = fullName.left(bracketPos);
232 QString name = fullName.right(fullName.size() - bracketPos - 2);
233 name.chop(1);
234 status->author.realName = name;
235 status->author.userName = screenName;
236 }
237 userNode = userNode.nextSibling();
238 }
239 } else if (elm.tagName() == QLatin1String("twitter:source")) {
240 status->source = QUrl::fromPercentEncoding(elm.text().toLatin1());
241 }
242 entryNode = entryNode.nextSibling();
243 }
244 status->isFavorited = false;
245 statusList.insert(0, status);
246 node = node.nextSibling();
247 }
248 return statusList;
249 }
250
parseRss(const QByteArray & buffer)251 QList< Choqok::Post * > GNUSocialApiSearch::parseRss(const QByteArray &buffer)
252 {
253 qCDebug(CHOQOK);
254 QDomDocument document;
255 QList<Choqok::Post *> statusList;
256
257 document.setContent(buffer);
258
259 QDomElement root = document.documentElement();
260
261 if (root.tagName() != QLatin1String("rdf:RDF")) {
262 qCDebug(CHOQOK) << "There is no rdf:RDF element in RSS feed " << buffer.data();
263 return statusList;
264 }
265
266 QDomNode node = root.firstChild();
267 QString timeStr;
268 while (!node.isNull()) {
269 if (node.toElement().tagName() != QLatin1String("item")) {
270 node = node.nextSibling();
271 continue;
272 }
273
274 Choqok::Post *status = new Choqok::Post;
275
276 QDomAttr statusIdAttr = node.toElement().attributeNode(QLatin1String("rdf:about"));
277 QString statusId;
278 if (mIdRegExp.exactMatch(statusIdAttr.value())) {
279 statusId = mIdRegExp.cap(1);
280 }
281
282 status->postId = statusId;
283
284 QDomNode itemNode = node.firstChild();
285
286 while (!itemNode.isNull()) {
287 if (itemNode.toElement().tagName() == QLatin1String("title")) {
288 QString content = itemNode.toElement().text();
289
290 int nameSep = content.indexOf(QLatin1Char(':'), 0);
291 QString screenName = content.left(nameSep);
292 QString statusText = content.right(content.size() - nameSep - 2);
293
294 status->author.userName = screenName;
295 status->content = statusText;
296 } else if (itemNode.toElement().tagName() == QLatin1String("dc:date")) {
297 int year, month, day, hour, minute, second;
298 sscanf(qPrintable(itemNode.toElement().text()),
299 "%d-%d-%dT%d:%d:%d%*s", &year, &month, &day, &hour, &minute, &second);
300 QDateTime recognized(QDate(year, month, day), QTime(hour, minute, second));
301 recognized.setTimeSpec(Qt::UTC);
302 status->creationDateTime = recognized;
303 } else if (itemNode.toElement().tagName() == QLatin1String("dc:creator")) {
304 status->author.realName = itemNode.toElement().text();
305 } else if (itemNode.toElement().tagName() == QLatin1String("sioc:reply_of")) {
306 QDomAttr userIdAttr = itemNode.toElement().attributeNode(QLatin1String("rdf:resource"));
307 QString id;
308 if (mIdRegExp.exactMatch(userIdAttr.value())) {
309 id = mIdRegExp.cap(1);
310 }
311 status->replyToPostId = id;
312 } else if (itemNode.toElement().tagName() == QLatin1String("statusnet:postIcon")) {
313 QDomAttr imageAttr = itemNode.toElement().attributeNode(QLatin1String("rdf:resource"));
314 status->author.profileImageUrl = QUrl::fromUserInput(imageAttr.value());
315 } else if (itemNode.toElement().tagName() == QLatin1String("link")) {
316 // QDomAttr imageAttr = itemNode.toElement().attributeNode( "rdf:resource" );
317 status->link = QUrl::fromUserInput(itemNode.toElement().text());
318 } else if (itemNode.toElement().tagName() == QLatin1String("sioc:has_discussion")) {
319 status->conversationId = itemNode.toElement().attributeNode(QLatin1String("rdf:resource")).value();
320 }
321
322 itemNode = itemNode.nextSibling();
323 }
324
325 status->isPrivate = false;
326 status->isFavorited = false;
327 statusList.insert(0, status);
328 node = node.nextSibling();
329 }
330
331 return statusList;
332 }
333
334