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 #include "gnusocialapimicroblog.h"
24 
25 #include <QJsonArray>
26 #include <QJsonDocument>
27 #include <QJsonObject>
28 #include <QMimeDatabase>
29 
30 #include <KIO/StoredTransferJob>
31 #include <KJobWidgets>
32 #include <KLocalizedString>
33 #include <KMessageBox>
34 
35 #include "account.h"
36 #include "accountmanager.h"
37 #include "choqokappearancesettings.h"
38 #include "composerwidget.h"
39 #include "editaccountwidget.h"
40 #include "mediamanager.h"
41 #include "microblogwidget.h"
42 #include "postwidget.h"
43 #include "timelinewidget.h"
44 
45 #include "twitterapimicroblogwidget.h"
46 #include "twitterapipostwidget.h"
47 #include "twitterapitimelinewidget.h"
48 
49 #include "gnusocialapiaccount.h"
50 #include "gnusocialapicomposerwidget.h"
51 #include "gnusocialapidebug.h"
52 #include "gnusocialapidmessagedialog.h"
53 #include "gnusocialapipostwidget.h"
54 #include "gnusocialapisearch.h"
55 
GNUSocialApiMicroBlog(const QString & componentName,QObject * parent=nullptr)56 GNUSocialApiMicroBlog::GNUSocialApiMicroBlog(const QString &componentName, QObject *parent = nullptr)
57     : TwitterApiMicroBlog(componentName, parent), friendsPage(1)
58 {
59     qCDebug(CHOQOK);
60     setServiceName(QLatin1String("GNU social"));
61     mTimelineInfos[QLatin1String("ReTweets")]->name = i18nc("Timeline name", "Repeated");
62     mTimelineInfos[QLatin1String("ReTweets")]->description = i18nc("Timeline description", "Your posts that were repeated by others");
63 }
64 
~GNUSocialApiMicroBlog()65 GNUSocialApiMicroBlog::~GNUSocialApiMicroBlog()
66 {
67     qCDebug(CHOQOK);
68 }
69 
createNewAccount(const QString & alias)70 Choqok::Account *GNUSocialApiMicroBlog::createNewAccount(const QString &alias)
71 {
72     GNUSocialApiAccount *acc = qobject_cast<GNUSocialApiAccount *>(Choqok::AccountManager::self()->findAccount(alias));
73     if (!acc) {
74         return new GNUSocialApiAccount(this, alias);
75     } else {
76         return nullptr;
77     }
78 }
79 
createMicroBlogWidget(Choqok::Account * account,QWidget * parent)80 Choqok::UI::MicroBlogWidget *GNUSocialApiMicroBlog::createMicroBlogWidget(Choqok::Account *account, QWidget *parent)
81 {
82     return new TwitterApiMicroBlogWidget(account, parent);
83 }
84 
createTimelineWidget(Choqok::Account * account,const QString & timelineName,QWidget * parent)85 Choqok::UI::TimelineWidget *GNUSocialApiMicroBlog::createTimelineWidget(Choqok::Account *account,
86         const QString &timelineName, QWidget *parent)
87 {
88     return new TwitterApiTimelineWidget(account, timelineName, parent);
89 }
90 
createPostWidget(Choqok::Account * account,Choqok::Post * post,QWidget * parent)91 Choqok::UI::PostWidget *GNUSocialApiMicroBlog::createPostWidget(Choqok::Account *account,
92         Choqok::Post *post, QWidget *parent)
93 {
94     return new GNUSocialApiPostWidget(account, post, parent);
95 }
96 
createComposerWidget(Choqok::Account * account,QWidget * parent)97 Choqok::UI::ComposerWidget *GNUSocialApiMicroBlog::createComposerWidget(Choqok::Account *account, QWidget *parent)
98 {
99     return new GNUSocialApiComposerWidget(account, parent);
100 }
101 
readPost(Choqok::Account * account,const QVariantMap & var,Choqok::Post * post)102 Choqok::Post *GNUSocialApiMicroBlog::readPost(Choqok::Account *account, const QVariantMap &var, Choqok::Post *post)
103 {
104     if (!post) {
105         qCCritical(CHOQOK) << "post is NULL!";
106         return nullptr;
107     }
108 
109     if (var[QLatin1String("source")].toString().compare(QLatin1String("linkback")) == 0) {
110         // Skip linkback statuses
111         return nullptr;
112     }
113 
114     post = TwitterApiMicroBlog::readPost(account, var, post);
115 
116     QUrl profileUrl = var[QLatin1String("user")].toMap()[QLatin1String("statusnet_profile_url")].toUrl();
117     post->author.homePageUrl = profileUrl;
118 
119     const QVariantMap retweeted = var[QLatin1String("retweeted_status")].toMap();
120 
121     if (!retweeted.isEmpty()) {
122         profileUrl = retweeted[QLatin1String("user")].toMap()[QLatin1String("statusnet_profile_url")].toUrl();
123         post->repeatedFromUser.homePageUrl = profileUrl;
124     }
125 
126     if (var.contains(QLatin1String("uri"))) {
127         post->link = var[QLatin1String("uri")].toUrl();
128     } else if (var.contains(QLatin1String("external_url"))) {
129         post->link = var[QLatin1String("external_url")].toUrl();
130     } else {
131         if (retweeted.contains(QLatin1String("uri"))) {
132             post->link = var[QLatin1String("uri")].toUrl();
133         } else {
134             // Last try, compone the url. However this only works for GNU Social instances.
135             post->link = QUrl::fromUserInput(QStringLiteral("%1://%2/notice/%3")
136                                              .arg(profileUrl.scheme()).arg(profileUrl.host()).arg(post->postId));
137         }
138     }
139 
140     return post;
141 }
142 
profileUrl(Choqok::Account * account,const QString & username) const143 QUrl GNUSocialApiMicroBlog::profileUrl(Choqok::Account *account, const QString &username) const
144 {
145     if (username.contains(QLatin1Char('@'))) {
146         const QStringList lst = username.split(QLatin1Char('@'), QString::SkipEmptyParts);
147 
148         if (lst.count() == 2) {
149             return QUrl::fromUserInput(QStringLiteral("https://%1/%2").arg(lst[1]).arg(lst[0]));
150         } else {
151             return QUrl();
152         }
153     } else {
154         GNUSocialApiAccount *acc = qobject_cast<GNUSocialApiAccount *>(account);
155 
156         QUrl url(acc->host());
157         url = url.adjusted(QUrl::StripTrailingSlash);
158         url.setPath(QLatin1Char('/') + username);
159 
160         return url;
161     }
162 }
163 
postUrl(Choqok::Account * account,const QString & username,const QString & postId) const164 QUrl GNUSocialApiMicroBlog::postUrl(Choqok::Account *account,  const QString &username,
165                                    const QString &postId) const
166 {
167     Q_UNUSED(username)
168     TwitterApiAccount *acc = qobject_cast<TwitterApiAccount *>(account);
169     if (acc) {
170         QUrl url(acc->homepageUrl());
171         url.setPath(url.path() + QStringLiteral("/notice/%1").arg(postId));
172         return url;
173     } else {
174         return QUrl();
175     }
176 }
177 
searchBackend()178 TwitterApiSearch *GNUSocialApiMicroBlog::searchBackend()
179 {
180     if (!mSearchBackend) {
181         mSearchBackend = new GNUSocialApiSearch(this);
182     }
183     return mSearchBackend;
184 }
185 
createPostWithAttachment(Choqok::Account * theAccount,Choqok::Post * post,const QString & mediumToAttach)186 void GNUSocialApiMicroBlog::createPostWithAttachment(Choqok::Account *theAccount, Choqok::Post *post,
187         const QString &mediumToAttach)
188 {
189     if (mediumToAttach.isEmpty()) {
190         TwitterApiMicroBlog::createPost(theAccount, post);
191     } else {
192         const QUrl picUrl = QUrl::fromUserInput(mediumToAttach);
193         KIO::StoredTransferJob *picJob = KIO::storedGet(picUrl, KIO::Reload, KIO::HideProgressInfo);
194         picJob->exec();
195         if (picJob->error()) {
196             qCCritical(CHOQOK) << "Job error:" << picJob->errorString();
197             KMessageBox::detailedError(Choqok::UI::Global::mainWindow(),
198                                        i18n("Uploading medium failed: cannot read the medium file."),
199                                        picJob->errorString());
200             return;
201         }
202         const QByteArray picData = picJob->data();
203         if (picData.count() == 0) {
204             qCCritical(CHOQOK) << "Cannot read the media file, please check if it exists.";
205             KMessageBox::error(Choqok::UI::Global::mainWindow(),
206                                i18n("Uploading medium failed: cannot read the medium file."));
207             return;
208         }
209         ///Documentation: http://identi.ca/notice/17779990
210         TwitterApiAccount *account = qobject_cast<TwitterApiAccount *>(theAccount);
211         QUrl url = account->apiUrl();
212         url.setPath(url.path() + QLatin1String("/statuses/update.json"));
213         const QMimeDatabase db;
214         QByteArray fileContentType = db.mimeTypeForUrl(picUrl).name().toUtf8();
215 
216         QMap<QString, QByteArray> formdata;
217         formdata[QLatin1String("status")] = post->content.toUtf8();
218         formdata[QLatin1String("in_reply_to_status_id")] = post->replyToPostId.toLatin1();
219         formdata[QLatin1String("source")] = QCoreApplication::applicationName().toLatin1();
220 
221         QMap<QString, QByteArray> mediafile;
222         mediafile[QLatin1String("name")] = "media";
223         mediafile[QLatin1String("filename")] = picUrl.fileName().toUtf8();
224         mediafile[QLatin1String("mediumType")] = fileContentType;
225         mediafile[QLatin1String("medium")] = picData;
226         QList< QMap<QString, QByteArray> > listMediafiles;
227         listMediafiles.append(mediafile);
228 
229         QByteArray data = Choqok::MediaManager::createMultipartFormData(formdata, listMediafiles);
230 
231         KIO::StoredTransferJob *job = KIO::storedHttpPost(data, url, KIO::HideProgressInfo) ;
232         if (!job) {
233             qCCritical(CHOQOK) << "Cannot create a http POST request!";
234             return;
235         }
236         job->addMetaData(QStringLiteral("content-type"),
237                          QStringLiteral("Content-Type: multipart/form-data; boundary=AaB03x"));
238         job->addMetaData(QStringLiteral("customHTTPHeader"),
239                          QStringLiteral("Authorization: ") +
240                          QLatin1String(authorizationHeader(account, url, QNetworkAccessManager::PostOperation)));
241         mCreatePostMap[ job ] = post;
242         mJobsAccount[job] = theAccount;
243         connect(job, &KIO::StoredTransferJob::result, this, &GNUSocialApiMicroBlog::slotCreatePost);
244         job->start();
245     }
246 }
247 
generateRepeatedByUserTooltip(const QString & username)248 QString GNUSocialApiMicroBlog::generateRepeatedByUserTooltip(const QString &username)
249 {
250     if (Choqok::AppearanceSettings::showRetweetsInChoqokWay()) {
251         return i18n("Repeat of %1", username);
252     } else {
253         return i18n("Repeated by %1", username);
254     }
255 }
256 
repeatQuestion()257 QString GNUSocialApiMicroBlog::repeatQuestion()
258 {
259     return i18n("Repeat this notice?");
260 }
261 
listFriendsUsername(TwitterApiAccount * theAccount,bool active)262 void GNUSocialApiMicroBlog::listFriendsUsername(TwitterApiAccount *theAccount, bool active)
263 {
264     Q_UNUSED(active);
265     friendsList.clear();
266     if (theAccount) {
267         doRequestFriendsScreenName(theAccount, 1);
268     }
269 }
270 
readFriendsScreenName(Choqok::Account * theAccount,const QByteArray & buffer)271 QStringList GNUSocialApiMicroBlog::readFriendsScreenName(Choqok::Account *theAccount, const QByteArray &buffer)
272 {
273     QStringList list;
274     const QJsonDocument json = QJsonDocument::fromJson(buffer);
275     if (!json.isNull()) {
276         for (const QJsonValue &u: json.array()) {
277             const QJsonObject user = u.toObject();
278 
279             if (user.contains(QStringLiteral("statusnet_profile_url"))) {
280                 list.append(user.value(QLatin1String("statusnet_profile_url")).toString());
281             }
282         }
283     } else {
284         QString err = i18n("Retrieving the friends list failed. The data returned from the server is corrupted.");
285         qCDebug(CHOQOK) << "JSON parse error:the buffer is: \n" << buffer;
286         Q_EMIT error(theAccount, ParsingError, err, Critical);
287     }
288     return list;
289 }
290 
requestFriendsScreenName(TwitterApiAccount * theAccount,bool active)291 void GNUSocialApiMicroBlog::requestFriendsScreenName(TwitterApiAccount *theAccount, bool active)
292 {
293     Q_UNUSED(active);
294     doRequestFriendsScreenName(theAccount, 1);
295 }
296 
showDirectMessageDialog(TwitterApiAccount * theAccount,const QString & toUsername)297 void GNUSocialApiMicroBlog::showDirectMessageDialog(TwitterApiAccount *theAccount, const QString &toUsername)
298 {
299     qCDebug(CHOQOK);
300     if (!theAccount) {
301         QAction *act = qobject_cast<QAction *>(sender());
302         theAccount = qobject_cast<TwitterApiAccount *>(Choqok::AccountManager::self()->findAccount(act->data().toString()));
303     }
304     GNUSocialApiDMessageDialog *dmsg = new GNUSocialApiDMessageDialog(theAccount, Choqok::UI::Global::mainWindow());
305     if (!toUsername.isEmpty()) {
306         dmsg->setTo(toUsername);
307     }
308     dmsg->show();
309 }
310 
doRequestFriendsScreenName(TwitterApiAccount * theAccount,int page)311 void GNUSocialApiMicroBlog::doRequestFriendsScreenName(TwitterApiAccount *theAccount, int page)
312 {
313     qCDebug(CHOQOK);
314     TwitterApiAccount *account = qobject_cast<TwitterApiAccount *>(theAccount);
315     QUrl url = account->apiUrl();
316     url = url.adjusted(QUrl::StripTrailingSlash);
317     url.setPath(url.path() + QLatin1String("/statuses/friends.json"));
318 
319     if (page > 1) {
320         QUrlQuery urlQuery;
321         urlQuery.addQueryItem(QLatin1String("page"), QString::number(page));
322         url.setQuery(urlQuery);
323     }
324 
325     KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo) ;
326     if (!job) {
327         qCDebug(CHOQOK) << "Cannot create an http GET request!";
328         return;
329     }
330     job->addMetaData(QStringLiteral("customHTTPHeader"),
331                      QStringLiteral("Authorization: ") +
332                      QLatin1String(authorizationHeader(account, url, QNetworkAccessManager::GetOperation)));
333     mJobsAccount[job] = theAccount;
334     connect(job, &KIO::StoredTransferJob::result, this, &GNUSocialApiMicroBlog::slotRequestFriendsScreenName);
335     job->start();
336 }
337 
slotRequestFriendsScreenName(KJob * job)338 void GNUSocialApiMicroBlog::slotRequestFriendsScreenName(KJob *job)
339 {
340     qCDebug(CHOQOK);
341     TwitterApiAccount *theAccount = qobject_cast<TwitterApiAccount *>(mJobsAccount.take(job));
342     if (job->error()) {
343         Q_EMIT error(theAccount, ServerError, i18n("Friends list for account %1 could not be updated:\n%2",
344                      theAccount->username(), job->errorString()), Normal);
345         return;
346     }
347     KIO::StoredTransferJob *stJob = qobject_cast<KIO::StoredTransferJob *>(job);
348     QStringList newList = readFriendsScreenName(theAccount, stJob->data());
349     friendsList << newList;
350     if (newList.count() == 100) {
351         doRequestFriendsScreenName(theAccount, ++friendsPage);
352     } else {
353         friendsList.removeDuplicates();
354         theAccount->setFriendsList(friendsList);
355         Q_EMIT friendsUsernameListed(theAccount, friendsList);
356     }
357 }
358 
fetchConversation(Choqok::Account * theAccount,const QString & conversationId)359 void GNUSocialApiMicroBlog::fetchConversation(Choqok::Account *theAccount, const QString &conversationId)
360 {
361     qCDebug(CHOQOK);
362     if (conversationId.isEmpty()) {
363         return;
364     }
365     TwitterApiAccount *account = qobject_cast<TwitterApiAccount *>(theAccount);
366     QUrl url = account->apiUrl();
367     url.setPath(QStringLiteral("/statusnet/conversation/%1.json").arg(conversationId));
368 
369     KIO::StoredTransferJob *job = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo) ;
370     if (!job) {
371         qCDebug(CHOQOK) << "Cannot create an http GET request!";
372         return;
373     }
374     job->addMetaData(QStringLiteral("customHTTPHeader"),
375                      QStringLiteral("Authorization: ") +
376                      QLatin1String(authorizationHeader(account, url, QNetworkAccessManager::GetOperation)));
377     mFetchConversationMap[ job ] = conversationId;
378     mJobsAccount[ job ] = theAccount;
379     connect(job, &KIO::StoredTransferJob::result, this, &GNUSocialApiMicroBlog::slotFetchConversation);
380     job->start();
381 }
382 
usernameFromProfileUrl(const QString & profileUrl)383 QString GNUSocialApiMicroBlog::usernameFromProfileUrl(const QString &profileUrl)
384 {
385     // Remove the initial slash from path
386     return QUrl(profileUrl).path().remove(0, 1);
387 }
388 
hostFromProfileUrl(const QString & profileUrl)389 QString GNUSocialApiMicroBlog::hostFromProfileUrl(const QString &profileUrl)
390 {
391     return QUrl(profileUrl).host();
392 }
393 
slotFetchConversation(KJob * job)394 void GNUSocialApiMicroBlog::slotFetchConversation(KJob *job)
395 {
396     qCDebug(CHOQOK);
397     if (!job) {
398         qCWarning(CHOQOK) << "NULL Job returned";
399         return;
400     }
401     QList<Choqok::Post *> posts;
402     QString conversationId = mFetchConversationMap.take(job);
403     Choqok::Account *theAccount = mJobsAccount.take(job);
404     if (job->error()) {
405         qCDebug(CHOQOK) << "Job Error:" << job->errorString();
406         Q_EMIT error(theAccount, Choqok::MicroBlog::CommunicationError,
407                      i18n("Fetching conversation failed. %1", job->errorString()), Normal);
408     } else {
409         KIO::StoredTransferJob *stj = qobject_cast<KIO::StoredTransferJob *> (job);
410         //if(format=="json"){
411         posts = readTimeline(theAccount, stj->data());
412         //} else {
413         //    posts = readTimelineFromXml ( theAccount, stj->data() );
414         //}
415         if (!posts.isEmpty()) {
416             Q_EMIT conversationFetched(theAccount, conversationId, posts);
417         }
418     }
419 }
420