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"