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