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