File indexing completed on 2024-05-05 04:57:14
0001 /* 0002 This file is part of Choqok, the KDE micro-blogging client 0003 0004 SPDX-FileCopyrightText: 2008-2012 Mehrdad Momeny <mehrdad.momeny@gmail.com> 0005 0006 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0007 */ 0008 0009 #include "gnusocialapisearch.h" 0010 0011 #include <QDomElement> 0012 0013 #include <KIO/StoredTransferJob> 0014 #include <KLocalizedString> 0015 0016 #include "twitterapiaccount.h" 0017 0018 #include "gnusocialapidebug.h" 0019 0020 const QRegExp GNUSocialApiSearch::m_rId(QLatin1String("tag:.+,[\\d-]+:(\\d+)")); 0021 const QRegExp GNUSocialApiSearch::mIdRegExp(QLatin1String("(?:user|(?:.*notice))/([0-9]+)")); 0022 0023 GNUSocialApiSearch::GNUSocialApiSearch(QObject *parent): TwitterApiSearch(parent) 0024 { 0025 qCDebug(CHOQOK); 0026 mSearchCode[ReferenceGroup] = QLatin1Char('!'); 0027 mSearchCode[ToUser] = QLatin1Char('@'); 0028 mSearchCode[FromUser].clear(); 0029 mSearchCode[ReferenceHashtag] = QLatin1Char('#'); 0030 0031 mSearchTypes[ReferenceHashtag].first = i18nc("Dents are Identica posts", "Dents Including This Hashtag"); 0032 mSearchTypes[ReferenceHashtag].second = true; 0033 0034 mSearchTypes[ReferenceGroup].first = i18nc("Dents are Identica posts", "Dents Including This Group"); 0035 mSearchTypes[ReferenceGroup].second = false; 0036 0037 mSearchTypes[FromUser].first = i18nc("Dents are Identica posts", "Dents From This User"); 0038 mSearchTypes[FromUser].second = false; 0039 0040 mSearchTypes[ToUser].first = i18nc("Dents are Identica posts", "Dents To This User"); 0041 mSearchTypes[ToUser].second = false; 0042 0043 } 0044 0045 GNUSocialApiSearch::~GNUSocialApiSearch() 0046 { 0047 0048 } 0049 0050 QUrl GNUSocialApiSearch::buildUrl(const SearchInfo &searchInfo, 0051 QString sinceStatusId, uint count, uint page) 0052 { 0053 qCDebug(CHOQOK); 0054 0055 QString formattedQuery; 0056 switch (searchInfo.option) { 0057 case ToUser: 0058 formattedQuery = searchInfo.query + QLatin1String("/replies/rss"); 0059 break; 0060 case FromUser: 0061 formattedQuery = searchInfo.query + QLatin1String("/rss"); 0062 break; 0063 case ReferenceGroup: 0064 formattedQuery = QLatin1String("group/") + searchInfo.query + QLatin1String("/rss"); 0065 break; 0066 case ReferenceHashtag: 0067 formattedQuery = searchInfo.query; 0068 break; 0069 default: 0070 formattedQuery = searchInfo.query + QLatin1String("/rss"); 0071 break; 0072 }; 0073 0074 QUrl url; 0075 TwitterApiAccount *theAccount = qobject_cast<TwitterApiAccount *>(searchInfo.account); 0076 Q_ASSERT(theAccount); 0077 if (searchInfo.option == ReferenceHashtag) { 0078 url = theAccount->apiUrl(); 0079 url = url.adjusted(QUrl::StripTrailingSlash); 0080 url.setPath(url.path() + QLatin1String("/search.atom")); 0081 QUrlQuery urlQuery; 0082 urlQuery.addQueryItem(QLatin1String("q"), formattedQuery); 0083 if (!sinceStatusId.isEmpty()) { 0084 urlQuery.addQueryItem(QLatin1String("since_id"), sinceStatusId); 0085 } 0086 int cntStr; 0087 if (count && count <= 100) { // GNU Social allows max 100 notices 0088 cntStr = count; 0089 } else { 0090 cntStr = 100; 0091 } 0092 urlQuery.addQueryItem(QLatin1String("rpp"), QString::number(cntStr)); 0093 if (page > 1) { 0094 urlQuery.addQueryItem(QLatin1String("page"), QString::number(page)); 0095 } 0096 url.setQuery(urlQuery); 0097 } else { 0098 url = QUrl(theAccount->apiUrl().url().remove(QLatin1String("/api"), Qt::CaseInsensitive)); 0099 url = url.adjusted(QUrl::StripTrailingSlash); 0100 url.setPath(url.path() + QLatin1Char('/') + (formattedQuery)); 0101 } 0102 return url; 0103 } 0104 0105 void GNUSocialApiSearch::requestSearchResults(const SearchInfo &searchInfo, 0106 const QString &sinceStatusId, 0107 uint count, uint page) 0108 { 0109 qCDebug(CHOQOK); 0110 QUrl url = buildUrl(searchInfo, sinceStatusId, count, page); 0111 qCDebug(CHOQOK) << url; 0112 KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo); 0113 if (!job) { 0114 qCCritical(CHOQOK) << "Cannot create an http GET request!"; 0115 return; 0116 } 0117 mSearchJobs[job] = searchInfo; 0118 connect(job, &KIO::StoredTransferJob::result, this, 0119 (void (GNUSocialApiSearch::*)(KJob*))&GNUSocialApiSearch::searchResultsReturned); 0120 job->start(); 0121 } 0122 0123 void GNUSocialApiSearch::searchResultsReturned(KJob *job) 0124 { 0125 qCDebug(CHOQOK); 0126 if (job == nullptr) { 0127 qCDebug(CHOQOK) << "job is a null pointer"; 0128 Q_EMIT error(i18n("Unable to fetch search results.")); 0129 return; 0130 } 0131 0132 SearchInfo info = mSearchJobs.take(job); 0133 0134 if (job->error()) { 0135 qCCritical(CHOQOK) << "Error:" << job->errorString(); 0136 Q_EMIT error(i18n("Unable to fetch search results: %1", job->errorString())); 0137 return; 0138 } 0139 KIO::StoredTransferJob *jj = qobject_cast<KIO::StoredTransferJob *>(job); 0140 QList<Choqok::Post *> postsList; 0141 if (info.option == ReferenceHashtag) { 0142 postsList = parseAtom(jj->data()); 0143 } else { 0144 postsList = parseRss(jj->data()); 0145 } 0146 0147 qCDebug(CHOQOK) << "Emiting searchResultsReceived()"; 0148 Q_EMIT searchResultsReceived(info, postsList); 0149 } 0150 0151 QString GNUSocialApiSearch::optionCode(int option) 0152 { 0153 return mSearchCode[option]; 0154 } 0155 0156 QList< Choqok::Post * > GNUSocialApiSearch::parseAtom(const QByteArray &buffer) 0157 { 0158 QDomDocument document; 0159 QList<Choqok::Post *> statusList; 0160 0161 document.setContent(buffer); 0162 0163 QDomElement root = document.documentElement(); 0164 0165 if (root.tagName() != QLatin1String("feed")) { 0166 qCDebug(CHOQOK) << "There is no feed element in Atom feed " << buffer.data(); 0167 return statusList; 0168 } 0169 0170 QDomNode node = root.firstChild(); 0171 QString timeStr; 0172 while (!node.isNull()) { 0173 if (node.toElement().tagName() != QLatin1String("entry")) { 0174 node = node.nextSibling(); 0175 continue; 0176 } 0177 0178 QDomNode entryNode = node.firstChild(); 0179 Choqok::Post *status = new Choqok::Post; 0180 status->isPrivate = false; 0181 0182 while (!entryNode.isNull()) { 0183 QDomElement elm = entryNode.toElement(); 0184 if (elm.tagName() == QLatin1String("id")) { 0185 // Fomatting example: "tag:search.twitter.com,2005:1235016836" 0186 QString id; 0187 if (m_rId.exactMatch(elm.text())) { 0188 id = m_rId.cap(1); 0189 } 0190 /* sscanf( qPrintable( elm.text() ), 0191 "tag:search.twitter.com,%*d:%d", &id);*/ 0192 status->postId = id; 0193 } else if (elm.tagName() == QLatin1String("published")) { 0194 // Formatting example: "2009-02-21T19:42:39Z" 0195 // Need to extract date in similar fashion to dateFromString 0196 int year, month, day, hour, minute, second; 0197 sscanf(qPrintable(elm.text()), 0198 "%d-%d-%dT%d:%d:%d%*s", &year, &month, &day, &hour, &minute, &second); 0199 QDateTime recognized(QDate(year, month, day), QTime(hour, minute, second)); 0200 recognized.setTimeSpec(Qt::UTC); 0201 status->creationDateTime = recognized; 0202 } else if (elm.tagName() == QLatin1String("title")) { 0203 status->content = elm.text(); 0204 } else if (elm.tagName() == QLatin1String("link")) { 0205 if (elm.attribute(QLatin1String("rel")) == QLatin1String("related")) { 0206 status->author.profileImageUrl = QUrl::fromUserInput(elm.attribute(QLatin1String("href"))); 0207 } else if (elm.attribute(QLatin1String("rel")) == QLatin1String("alternate")) { 0208 status->link = QUrl::fromUserInput(elm.attribute(QLatin1String("href"))); 0209 } 0210 } else if (elm.tagName() == QLatin1String("author")) { 0211 QDomNode userNode = entryNode.firstChild(); 0212 while (!userNode.isNull()) { 0213 if (userNode.toElement().tagName() == QLatin1String("name")) { 0214 QString fullName = userNode.toElement().text(); 0215 int bracketPos = fullName.indexOf(QLatin1Char(' '), 0); 0216 QString screenName = fullName.left(bracketPos); 0217 QString name = fullName.right(fullName.size() - bracketPos - 2); 0218 name.chop(1); 0219 status->author.realName = name; 0220 status->author.userName = screenName; 0221 } 0222 userNode = userNode.nextSibling(); 0223 } 0224 } else if (elm.tagName() == QLatin1String("twitter:source")) { 0225 status->source = QUrl::fromPercentEncoding(elm.text().toLatin1()); 0226 } 0227 entryNode = entryNode.nextSibling(); 0228 } 0229 status->isFavorited = false; 0230 statusList.insert(0, status); 0231 node = node.nextSibling(); 0232 } 0233 return statusList; 0234 } 0235 0236 QList< Choqok::Post * > GNUSocialApiSearch::parseRss(const QByteArray &buffer) 0237 { 0238 qCDebug(CHOQOK); 0239 QDomDocument document; 0240 QList<Choqok::Post *> statusList; 0241 0242 document.setContent(buffer); 0243 0244 QDomElement root = document.documentElement(); 0245 0246 if (root.tagName() != QLatin1String("rdf:RDF")) { 0247 qCDebug(CHOQOK) << "There is no rdf:RDF element in RSS feed " << buffer.data(); 0248 return statusList; 0249 } 0250 0251 QDomNode node = root.firstChild(); 0252 QString timeStr; 0253 while (!node.isNull()) { 0254 if (node.toElement().tagName() != QLatin1String("item")) { 0255 node = node.nextSibling(); 0256 continue; 0257 } 0258 0259 Choqok::Post *status = new Choqok::Post; 0260 0261 QDomAttr statusIdAttr = node.toElement().attributeNode(QLatin1String("rdf:about")); 0262 QString statusId; 0263 if (mIdRegExp.exactMatch(statusIdAttr.value())) { 0264 statusId = mIdRegExp.cap(1); 0265 } 0266 0267 status->postId = statusId; 0268 0269 QDomNode itemNode = node.firstChild(); 0270 0271 while (!itemNode.isNull()) { 0272 if (itemNode.toElement().tagName() == QLatin1String("title")) { 0273 QString content = itemNode.toElement().text(); 0274 0275 int nameSep = content.indexOf(QLatin1Char(':'), 0); 0276 QString screenName = content.left(nameSep); 0277 QString statusText = content.right(content.size() - nameSep - 2); 0278 0279 status->author.userName = screenName; 0280 status->content = statusText; 0281 } else if (itemNode.toElement().tagName() == QLatin1String("dc:date")) { 0282 int year, month, day, hour, minute, second; 0283 sscanf(qPrintable(itemNode.toElement().text()), 0284 "%d-%d-%dT%d:%d:%d%*s", &year, &month, &day, &hour, &minute, &second); 0285 QDateTime recognized(QDate(year, month, day), QTime(hour, minute, second)); 0286 recognized.setTimeSpec(Qt::UTC); 0287 status->creationDateTime = recognized; 0288 } else if (itemNode.toElement().tagName() == QLatin1String("dc:creator")) { 0289 status->author.realName = itemNode.toElement().text(); 0290 } else if (itemNode.toElement().tagName() == QLatin1String("sioc:reply_of")) { 0291 QDomAttr userIdAttr = itemNode.toElement().attributeNode(QLatin1String("rdf:resource")); 0292 QString id; 0293 if (mIdRegExp.exactMatch(userIdAttr.value())) { 0294 id = mIdRegExp.cap(1); 0295 } 0296 status->replyToPostId = id; 0297 } else if (itemNode.toElement().tagName() == QLatin1String("statusnet:postIcon")) { 0298 QDomAttr imageAttr = itemNode.toElement().attributeNode(QLatin1String("rdf:resource")); 0299 status->author.profileImageUrl = QUrl::fromUserInput(imageAttr.value()); 0300 } else if (itemNode.toElement().tagName() == QLatin1String("link")) { 0301 // QDomAttr imageAttr = itemNode.toElement().attributeNode( "rdf:resource" ); 0302 status->link = QUrl::fromUserInput(itemNode.toElement().text()); 0303 } else if (itemNode.toElement().tagName() == QLatin1String("sioc:has_discussion")) { 0304 status->conversationId = itemNode.toElement().attributeNode(QLatin1String("rdf:resource")).value(); 0305 } 0306 0307 itemNode = itemNode.nextSibling(); 0308 } 0309 0310 status->isPrivate = false; 0311 status->isFavorited = false; 0312 statusList.insert(0, status); 0313 node = node.nextSibling(); 0314 } 0315 0316 return statusList; 0317 } 0318 0319 #include "moc_gnusocialapisearch.cpp"