1 #include "providers/twitch/TwitchAccount.hpp"
2 
3 #include <QThread>
4 
5 #include "Application.hpp"
6 #include "common/Channel.hpp"
7 #include "common/Env.hpp"
8 #include "common/NetworkRequest.hpp"
9 #include "common/Outcome.hpp"
10 #include "common/QLogging.hpp"
11 #include "controllers/accounts/AccountController.hpp"
12 #include "messages/Message.hpp"
13 #include "messages/MessageBuilder.hpp"
14 #include "providers/IvrApi.hpp"
15 #include "providers/irc/IrcMessageBuilder.hpp"
16 #include "providers/twitch/TwitchCommon.hpp"
17 #include "providers/twitch/TwitchUser.hpp"
18 #include "providers/twitch/api/Helix.hpp"
19 #include "providers/twitch/api/Kraken.hpp"
20 #include "singletons/Emotes.hpp"
21 #include "util/QStringHash.hpp"
22 #include "util/RapidjsonHelpers.hpp"
23 
24 namespace chatterino {
25 
getEmoteSetBatches(QStringList emoteSetKeys)26 std::vector<QStringList> getEmoteSetBatches(QStringList emoteSetKeys)
27 {
28     // splitting emoteSetKeys to batches of 100, because Ivr API endpoint accepts a maximum of 100 emotesets at once
29     constexpr int batchSize = 100;
30 
31     int batchCount = (emoteSetKeys.size() / batchSize) + 1;
32 
33     std::vector<QStringList> batches;
34     batches.reserve(batchCount);
35 
36     for (int i = 0; i < batchCount; i++)
37     {
38         QStringList batch;
39 
40         int last = std::min(batchSize, emoteSetKeys.size() - batchSize * i);
41         for (int j = 0; j < last; j++)
42         {
43             batch.push_back(emoteSetKeys.at(j + (batchSize * i)));
44         }
45         batches.emplace_back(batch);
46     }
47 
48     return batches;
49 }
50 
TwitchAccount(const QString & username,const QString & oauthToken,const QString & oauthClient,const QString & userID)51 TwitchAccount::TwitchAccount(const QString &username, const QString &oauthToken,
52                              const QString &oauthClient, const QString &userID)
53     : Account(ProviderId::Twitch)
54     , oauthClient_(oauthClient)
55     , oauthToken_(oauthToken)
56     , userName_(username)
57     , userId_(userID)
58     , isAnon_(username == ANONYMOUS_USERNAME)
59 {
60 }
61 
toString() const62 QString TwitchAccount::toString() const
63 {
64     return this->getUserName();
65 }
66 
getUserName() const67 const QString &TwitchAccount::getUserName() const
68 {
69     return this->userName_;
70 }
71 
getOAuthClient() const72 const QString &TwitchAccount::getOAuthClient() const
73 {
74     return this->oauthClient_;
75 }
76 
getOAuthToken() const77 const QString &TwitchAccount::getOAuthToken() const
78 {
79     return this->oauthToken_;
80 }
81 
getUserId() const82 const QString &TwitchAccount::getUserId() const
83 {
84     return this->userId_;
85 }
86 
color()87 QColor TwitchAccount::color()
88 {
89     return this->color_.get();
90 }
91 
setColor(QColor color)92 void TwitchAccount::setColor(QColor color)
93 {
94     this->color_.set(std::move(color));
95 }
96 
setOAuthClient(const QString & newClientID)97 bool TwitchAccount::setOAuthClient(const QString &newClientID)
98 {
99     if (this->oauthClient_.compare(newClientID) == 0)
100     {
101         return false;
102     }
103 
104     this->oauthClient_ = newClientID;
105 
106     return true;
107 }
108 
setOAuthToken(const QString & newOAuthToken)109 bool TwitchAccount::setOAuthToken(const QString &newOAuthToken)
110 {
111     if (this->oauthToken_.compare(newOAuthToken) == 0)
112     {
113         return false;
114     }
115 
116     this->oauthToken_ = newOAuthToken;
117 
118     return true;
119 }
120 
isAnon() const121 bool TwitchAccount::isAnon() const
122 {
123     return this->isAnon_;
124 }
125 
loadBlocks()126 void TwitchAccount::loadBlocks()
127 {
128     getHelix()->loadBlocks(
129         getApp()->accounts->twitch.getCurrent()->userId_,
130         [this](std::vector<HelixBlock> blocks) {
131             auto ignores = this->ignores_.access();
132             auto userIds = this->ignoresUserIds_.access();
133             ignores->clear();
134             userIds->clear();
135 
136             for (const HelixBlock &block : blocks)
137             {
138                 TwitchUser blockedUser;
139                 blockedUser.fromHelixBlock(block);
140                 ignores->insert(blockedUser);
141                 userIds->insert(blockedUser.id);
142             }
143         },
144         [] {
145             qCWarning(chatterinoTwitch) << "Fetching blocks failed!";
146         });
147 }
148 
blockUser(QString userId,std::function<void ()> onSuccess,std::function<void ()> onFailure)149 void TwitchAccount::blockUser(QString userId, std::function<void()> onSuccess,
150                               std::function<void()> onFailure)
151 {
152     getHelix()->blockUser(
153         userId,
154         [this, userId, onSuccess] {
155             TwitchUser blockedUser;
156             blockedUser.id = userId;
157             {
158                 auto ignores = this->ignores_.access();
159                 auto userIds = this->ignoresUserIds_.access();
160 
161                 ignores->insert(blockedUser);
162                 userIds->insert(blockedUser.id);
163             }
164             onSuccess();
165         },
166         std::move(onFailure));
167 }
168 
unblockUser(QString userId,std::function<void ()> onSuccess,std::function<void ()> onFailure)169 void TwitchAccount::unblockUser(QString userId, std::function<void()> onSuccess,
170                                 std::function<void()> onFailure)
171 {
172     getHelix()->unblockUser(
173         userId,
174         [this, userId, onSuccess] {
175             TwitchUser ignoredUser;
176             ignoredUser.id = userId;
177             {
178                 auto ignores = this->ignores_.access();
179                 auto userIds = this->ignoresUserIds_.access();
180 
181                 ignores->erase(ignoredUser);
182                 userIds->erase(ignoredUser.id);
183             }
184             onSuccess();
185         },
186         std::move(onFailure));
187 }
188 
accessBlocks() const189 SharedAccessGuard<const std::set<TwitchUser>> TwitchAccount::accessBlocks()
190     const
191 {
192     return this->ignores_.accessConst();
193 }
194 
accessBlockedUserIds() const195 SharedAccessGuard<const std::set<QString>> TwitchAccount::accessBlockedUserIds()
196     const
197 {
198     return this->ignoresUserIds_.accessConst();
199 }
200 
loadEmotes()201 void TwitchAccount::loadEmotes()
202 {
203     qCDebug(chatterinoTwitch)
204         << "Loading Twitch emotes for user" << this->getUserName();
205 
206     if (this->getOAuthClient().isEmpty() || this->getOAuthToken().isEmpty())
207     {
208         qCDebug(chatterinoTwitch)
209             << "Aborted loadEmotes due to missing Client ID and/or OAuth token";
210         return;
211     }
212 
213     {
214         auto emoteData = this->emotes_.access();
215         emoteData->emoteSets.clear();
216         emoteData->emotes.clear();
217         qCDebug(chatterinoTwitch) << "Cleared emotes!";
218     }
219 
220     // TODO(zneix): Once Helix adds Get User Emotes we could remove this hacky solution
221     // For now, this is necessary as Kraken's equivalent doesn't return all emotes
222     // See: https://twitch.uservoice.com/forums/310213-developers/suggestions/43599900
223     this->loadUserstateEmotes([=] {
224         // Fill up emoteData with emote sets that were returned in a Kraken call, but aren't present in emoteData.
225         this->loadKrakenEmotes();
226     });
227 }
228 
setUserstateEmoteSets(QStringList newEmoteSets)229 bool TwitchAccount::setUserstateEmoteSets(QStringList newEmoteSets)
230 {
231     newEmoteSets.sort();
232 
233     if (this->userstateEmoteSets_ == newEmoteSets)
234     {
235         // Nothing has changed
236         return false;
237     }
238 
239     this->userstateEmoteSets_ = newEmoteSets;
240 
241     return true;
242 }
243 
loadUserstateEmotes(std::function<void ()> callback)244 void TwitchAccount::loadUserstateEmotes(std::function<void()> callback)
245 {
246     if (this->userstateEmoteSets_.isEmpty())
247     {
248         callback();
249         return;
250     }
251 
252     QStringList newEmoteSetKeys, krakenEmoteSetKeys;
253 
254     auto emoteData = this->emotes_.access();
255     auto userEmoteSets = emoteData->emoteSets;
256 
257     // get list of already fetched emote sets
258     for (const auto &userEmoteSet : userEmoteSets)
259     {
260         krakenEmoteSetKeys.push_back(userEmoteSet->key);
261     }
262 
263     // filter out emote sets from userstate message, which are not in fetched emote set list
264     for (const auto &emoteSetKey : qAsConst(this->userstateEmoteSets_))
265     {
266         if (!krakenEmoteSetKeys.contains(emoteSetKey))
267         {
268             newEmoteSetKeys.push_back(emoteSetKey);
269         }
270     }
271 
272     // return if there are no new emote sets
273     if (newEmoteSetKeys.isEmpty())
274     {
275         callback();
276         return;
277     }
278 
279     // requesting emotes
280     auto batches = getEmoteSetBatches(newEmoteSetKeys);
281     for (int i = 0; i < batches.size(); i++)
282     {
283         qCDebug(chatterinoTwitch)
284             << QString(
285                    "Loading %1 emotesets from IVR; batch %2/%3 (%4 sets): %5")
286                    .arg(newEmoteSetKeys.size())
287                    .arg(i + 1)
288                    .arg(batches.size())
289                    .arg(batches.at(i).size())
290                    .arg(batches.at(i).join(","));
291         getIvr()->getBulkEmoteSets(
292             batches.at(i).join(","),
293             [this](QJsonArray emoteSetArray) {
294                 auto emoteData = this->emotes_.access();
295                 auto localEmoteData = this->localEmotes_.access();
296                 for (auto emoteSet_ : emoteSetArray)
297                 {
298                     auto emoteSet = std::make_shared<EmoteSet>();
299 
300                     IvrEmoteSet ivrEmoteSet(emoteSet_.toObject());
301 
302                     QString setKey = ivrEmoteSet.setId;
303                     emoteSet->key = setKey;
304 
305                     // check if the emoteset is already in emoteData
306                     auto isAlreadyFetched =
307                         std::find_if(emoteData->emoteSets.begin(),
308                                      emoteData->emoteSets.end(),
309                                      [setKey](std::shared_ptr<EmoteSet> set) {
310                                          return (set->key == setKey);
311                                      });
312                     if (isAlreadyFetched != emoteData->emoteSets.end())
313                     {
314                         continue;
315                     }
316 
317                     emoteSet->channelName = ivrEmoteSet.login;
318                     emoteSet->text = ivrEmoteSet.displayName;
319 
320                     for (const auto &emoteObj : ivrEmoteSet.emotes)
321                     {
322                         IvrEmote ivrEmote(emoteObj.toObject());
323 
324                         auto id = EmoteId{ivrEmote.id};
325                         auto code = EmoteName{
326                             TwitchEmotes::cleanUpEmoteCode(ivrEmote.code)};
327 
328                         emoteSet->emotes.push_back(TwitchEmote{id, code});
329 
330                         auto emote =
331                             getApp()->emotes->twitch.getOrCreateEmote(id, code);
332 
333                         // Follower emotes can be only used in their origin channel
334                         if (ivrEmote.emoteType == "FOLLOWER")
335                         {
336                             emoteSet->local = true;
337 
338                             // EmoteMap for target channel wasn't initialized yet, doing it now
339                             if (localEmoteData->find(ivrEmoteSet.channelId) ==
340                                 localEmoteData->end())
341                             {
342                                 localEmoteData->emplace(ivrEmoteSet.channelId,
343                                                         EmoteMap());
344                             }
345 
346                             localEmoteData->at(ivrEmoteSet.channelId)
347                                 .emplace(code, emote);
348                         }
349                         else
350                         {
351                             emoteData->emotes.emplace(code, emote);
352                         }
353                     }
354                     std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
355                               [](const TwitchEmote &l, const TwitchEmote &r) {
356                                   return l.name.string < r.name.string;
357                               });
358                     emoteData->emoteSets.emplace_back(emoteSet);
359                 }
360             },
361             [] {
362                 // fetching emotes failed, ivr API might be down
363             },
364             [=] {
365                 // XXX(zneix): We check if this is the last iteration and if so, call the callback
366                 if (i + 1 == batches.size())
367                 {
368                     qCDebug(chatterinoTwitch)
369                         << "Finished loading emotes from IVR, attempting to "
370                            "load Kraken emotes now";
371                     callback();
372                 }
373             });
374     };
375 }
376 
377 SharedAccessGuard<const TwitchAccount::TwitchAccountEmoteData>
accessEmotes() const378     TwitchAccount::accessEmotes() const
379 {
380     return this->emotes_.accessConst();
381 }
382 
383 SharedAccessGuard<const std::unordered_map<QString, EmoteMap>>
accessLocalEmotes() const384     TwitchAccount::accessLocalEmotes() const
385 {
386     return this->localEmotes_.accessConst();
387 }
388 
389 // AutoModActions
autoModAllow(const QString msgID,ChannelPtr channel)390 void TwitchAccount::autoModAllow(const QString msgID, ChannelPtr channel)
391 {
392     getHelix()->manageAutoModMessages(
393         this->getUserId(), msgID, "ALLOW",
394         [] {
395             // success
396         },
397         [channel](auto error) {
398             // failure
399             QString errorMessage("Failed to allow AutoMod message - ");
400 
401             switch (error)
402             {
403                 case HelixAutoModMessageError::MessageAlreadyProcessed: {
404                     errorMessage += "message has already been processed.";
405                 }
406                 break;
407 
408                 case HelixAutoModMessageError::UserNotAuthenticated: {
409                     errorMessage += "you need to re-authenticate.";
410                 }
411                 break;
412 
413                 case HelixAutoModMessageError::UserNotAuthorized: {
414                     errorMessage +=
415                         "you don't have permission to perform that action";
416                 }
417                 break;
418 
419                 case HelixAutoModMessageError::MessageNotFound: {
420                     errorMessage += "target message not found.";
421                 }
422                 break;
423 
424                 // This would most likely happen if the service is down, or if the JSON payload returned has changed format
425                 case HelixAutoModMessageError::Unknown:
426                 default: {
427                     errorMessage += "an unknown error occured.";
428                 }
429                 break;
430             }
431 
432             channel->addMessage(makeSystemMessage(errorMessage));
433         });
434 }
435 
autoModDeny(const QString msgID,ChannelPtr channel)436 void TwitchAccount::autoModDeny(const QString msgID, ChannelPtr channel)
437 {
438     getHelix()->manageAutoModMessages(
439         this->getUserId(), msgID, "DENY",
440         [] {
441             // success
442         },
443         [channel](auto error) {
444             // failure
445             QString errorMessage("Failed to deny AutoMod message - ");
446 
447             switch (error)
448             {
449                 case HelixAutoModMessageError::MessageAlreadyProcessed: {
450                     errorMessage += "message has already been processed.";
451                 }
452                 break;
453 
454                 case HelixAutoModMessageError::UserNotAuthenticated: {
455                     errorMessage += "you need to re-authenticate.";
456                 }
457                 break;
458 
459                 case HelixAutoModMessageError::UserNotAuthorized: {
460                     errorMessage +=
461                         "you don't have permission to perform that action";
462                 }
463                 break;
464 
465                 case HelixAutoModMessageError::MessageNotFound: {
466                     errorMessage += "target message not found.";
467                 }
468                 break;
469 
470                 // This would most likely happen if the service is down, or if the JSON payload returned has changed format
471                 case HelixAutoModMessageError::Unknown:
472                 default: {
473                     errorMessage += "an unknown error occured.";
474                 }
475                 break;
476             }
477 
478             channel->addMessage(makeSystemMessage(errorMessage));
479         });
480 }
481 
loadKrakenEmotes()482 void TwitchAccount::loadKrakenEmotes()
483 {
484     getKraken()->getUserEmotes(
485         this,
486         [this](KrakenEmoteSets data) {
487             // no emotes available
488             if (data.emoteSets.isEmpty())
489             {
490                 qCWarning(chatterinoTwitch)
491                     << "\"emoticon_sets\" either empty or not present in "
492                        "Kraken::getUserEmotes response";
493                 return;
494             }
495 
496             auto emoteData = this->emotes_.access();
497 
498             for (auto emoteSetIt = data.emoteSets.begin();
499                  emoteSetIt != data.emoteSets.end(); ++emoteSetIt)
500             {
501                 auto emoteSet = std::make_shared<EmoteSet>();
502 
503                 QString setKey = emoteSetIt.key();
504                 emoteSet->key = setKey;
505                 this->loadEmoteSetData(emoteSet);
506 
507                 // check if the emoteset is already in emoteData
508                 auto isAlreadyFetched = std::find_if(
509                     emoteData->emoteSets.begin(), emoteData->emoteSets.end(),
510                     [setKey](std::shared_ptr<EmoteSet> set) {
511                         return (set->key == setKey);
512                     });
513                 if (isAlreadyFetched != emoteData->emoteSets.end())
514                 {
515                     continue;
516                 }
517 
518                 for (const auto emoteArrObj : emoteSetIt->toArray())
519                 {
520                     if (!emoteArrObj.isObject())
521                     {
522                         qCWarning(chatterinoTwitch)
523                             << QString("Emote value from set %1 was invalid")
524                                    .arg(emoteSet->key);
525                         continue;
526                     }
527                     KrakenEmote krakenEmote(emoteArrObj.toObject());
528 
529                     auto id = EmoteId{krakenEmote.id};
530                     auto code = EmoteName{
531                         TwitchEmotes::cleanUpEmoteCode(krakenEmote.code)};
532 
533                     emoteSet->emotes.emplace_back(TwitchEmote{id, code});
534 
535                     if (!emoteSet->local)
536                     {
537                         auto emote =
538                             getApp()->emotes->twitch.getOrCreateEmote(id, code);
539                         emoteData->emotes.emplace(code, emote);
540                     }
541                 }
542 
543                 std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
544                           [](const TwitchEmote &l, const TwitchEmote &r) {
545                               return l.name.string < r.name.string;
546                           });
547                 emoteData->emoteSets.emplace_back(emoteSet);
548             }
549         },
550         [] {
551             // kraken request failed
552         });
553 }
554 
loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)555 void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
556 {
557     if (!emoteSet)
558     {
559         qCWarning(chatterinoTwitch) << "null emote set sent";
560         return;
561     }
562 
563     auto staticSetIt = this->staticEmoteSets.find(emoteSet->key);
564     if (staticSetIt != this->staticEmoteSets.end())
565     {
566         const auto &staticSet = staticSetIt->second;
567         emoteSet->channelName = staticSet.channelName;
568         emoteSet->text = staticSet.text;
569         return;
570     }
571 
572     getHelix()->getEmoteSetData(
573         emoteSet->key,
574         [emoteSet](HelixEmoteSetData emoteSetData) {
575             // Follower emotes can be only used in their origin channel
576             if (emoteSetData.emoteType == "follower")
577             {
578                 emoteSet->local = true;
579             }
580 
581             if (emoteSetData.ownerId.isEmpty() ||
582                 emoteSetData.setId != emoteSet->key)
583             {
584                 qCDebug(chatterinoTwitch)
585                     << QString("Failed to fetch emoteSetData for %1, assuming "
586                                "Twitch is the owner")
587                            .arg(emoteSet->key);
588 
589                 // most (if not all) emotes that fail to load are time limited event emotes owned by Twitch
590                 emoteSet->channelName = "twitch";
591                 emoteSet->text = "Twitch";
592 
593                 return;
594             }
595 
596             // emote set 0 = global emotes
597             if (emoteSetData.ownerId == "0")
598             {
599                 // emoteSet->channelName = QString();
600                 emoteSet->text = "Twitch Global";
601                 return;
602             }
603 
604             getHelix()->getUserById(
605                 emoteSetData.ownerId,
606                 [emoteSet](HelixUser user) {
607                     emoteSet->channelName = user.login;
608                     emoteSet->text = user.displayName;
609                 },
610                 [emoteSetData] {
611                     qCWarning(chatterinoTwitch)
612                         << "Failed to query user by id:" << emoteSetData.ownerId
613                         << emoteSetData.setId;
614                 });
615         },
616         [emoteSet] {
617             // fetching emoteset data failed
618             return;
619         });
620 }
621 
622 }  // namespace chatterino
623