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