1 /*
2  * Copyright © 2015-2016 Antti Lamminsalo
3  *
4  * This file is part of Orion.
5  *
6  * Orion is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * You should have received a copy of the GNU General Public License
12  * along with Orion.  If not, see <http://www.gnu.org/licenses/>.
13  */
14 
15 #include "jsonparser.h"
16 #include "../model/settingsmanager.h"
17 
parseStreams(const QByteArray & data)18 PagedResult<Channel*> JsonParser::parseStreams(const QByteArray &data)
19 {
20     PagedResult<Channel*> out;
21 
22     QJsonParseError error;
23     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
24     if (error.error == QJsonParseError::NoError){
25         QJsonObject json = doc.object();
26 
27         //Online streams
28         QJsonArray arr = json["streams"].toArray();
29         foreach (const QJsonValue &item, arr){
30             out.items.append(JsonParser::parseStreamJson(item.toObject(), true));
31         }
32 
33         out.total = json["_total"].toInt();
34 
35         //Caller must use request context to determine offline streams
36     }
37 
38     return out;
39 }
40 
parseStream(const QByteArray & data)41 Channel *JsonParser::parseStream(const QByteArray &data)
42 {
43     QJsonParseError error;
44     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
45     if (error.error == QJsonParseError::NoError){
46         return parseStreamJson(doc.object(), false);
47     }
48     return new Channel();
49 }
50 
parseStreamJson(const QJsonObject & json,const bool expectChannel)51 Channel* JsonParser::parseStreamJson(const QJsonObject &json, const bool expectChannel)
52 {
53     Channel* channel = new Channel();
54 
55     QJsonObject jsonObj;
56 
57     if (!jsonObj["stream"].isNull()) {
58         jsonObj = jsonObj["stream"].toObject();
59     } else {
60         jsonObj = json;
61     }
62 
63     if (!jsonObj["preview"].isNull()){
64 
65         QJsonObject preview = jsonObj["preview"].toObject();
66 
67         if (!preview["large"].isNull()){
68             channel->setPreviewurl(preview["large"].toString());
69         }
70     }
71 
72     if (!jsonObj["viewers"].isNull()){
73         channel->setViewers(jsonObj["viewers"].toInt());
74     }
75 
76     if (!jsonObj["game"].isNull()){
77         channel->setGame(jsonObj["game"].toString());
78     }
79 
80     if (!jsonObj["channel"].isNull()){
81 
82         Channel *c = parseChannelJson(jsonObj["channel"].toObject());
83         channel->setServiceName(c->getServiceName());
84         channel->setId(c->getId());
85         channel->setName(c->getName());
86         channel->setLogourl(c->getLogourl());
87         channel->setInfo(c->getInfo());
88 
89         delete c;
90     }
91     else if (expectChannel) {
92         qDebug() << "expected channel; stream will not have channel id to correlate";
93     }
94 
95     channel->setOnline(true);
96 
97     return channel;
98 }
99 
parseGames(const QByteArray & data)100 QList<Game*> JsonParser::parseGames(const QByteArray &data)
101 {
102     QList<Game*> games;
103 
104     QJsonParseError error;
105     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
106     if (error.error == QJsonParseError::NoError) {
107         QJsonObject json = doc.object();
108 
109         QString arg = (!json["top"].isNull() ? "top" : (!json["games"].isNull() ? "games" : ""));
110 
111         if (!arg.isEmpty()){
112             QJsonArray arr = json[arg].toArray();
113             foreach (const QJsonValue &item, arr){
114                 Game* game = parseGame(item.toObject());
115                 if (!game->getName().isEmpty()){
116                     games.append(game);
117                 }
118             }
119         }
120     }
121 
122     return games;
123 }
124 
125 
parseGame(const QJsonObject & json)126 Game* JsonParser::parseGame(const QJsonObject &json)
127 {
128     Game* game = new Game();
129 
130     //From top games
131     if (json.contains("game") && !json["game"].isNull()){
132         const QJsonObject gameObj = json["game"].toObject();
133 
134         if (!gameObj["_id"].isNull())
135             game->setId(gameObj["_id"].toInt());
136 
137         if (!json["viewers"].isNull())
138             game->setViewers(json["viewers"].toInt());
139 
140         if (!gameObj["name"].isNull())
141             game->setName(gameObj["name"].toString());
142 
143         if (!gameObj["box"].isNull() && !gameObj["box"].toObject()["medium"].isNull())
144             game->setLogo(gameObj["box"].toObject()["medium"].toString());
145 
146         if (!gameObj["logo"].isNull() && !gameObj["logo"].toObject()["medium"].isNull())
147             game->setPreview(gameObj["logo"].toObject()["medium"].toString());
148     }
149     //From games search
150     else {
151         if (!json["_id"].isNull())
152             game->setId(json["_id"].toInt());
153 
154         if (!json["name"].isNull())
155             game->setName(json["name"].toString());
156 
157         if (!json["viewers"].isNull())
158             game->setViewers(json["viewers"].toInt());
159 
160         if (!json["box"].isNull() && !json["box"].toObject()["medium"].isNull())
161             game->setLogo(json["box"].toObject()["medium"].toString());
162 
163         if (!json["logo"].isNull() && !json["logo"].toObject()["medium"].isNull())
164             game->setPreview(json["logo"].toObject()["medium"].toString());
165     }
166 
167     return game;
168 }
169 
parseChannel(const QByteArray & data)170 Channel* JsonParser::parseChannel(const QByteArray &data){
171     QJsonParseError error;
172     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
173     if (error.error == QJsonParseError::NoError){
174         return parseChannelJson(doc.object());
175     }
176     return new Channel();
177 }
178 
parseChannelJson(const QJsonObject & json)179 Channel* JsonParser::parseChannelJson(const QJsonObject &json)
180 {
181     Channel* channel = new Channel();
182 
183     if (!json["name"].isNull()){
184 
185         channel->setServiceName(json["name"].toString());
186 
187        // qDebug() << "Parsing channel data for " <<  channel.getUriName();
188 
189         if (!json["name"].isNull()){
190             channel->setServiceName(json["name"].toString());
191         }
192 
193         if (!json["display_name"].isNull()){
194             channel->setName(json["display_name"].toString());
195         }
196 
197         if (!json["status"].isNull()){
198             channel->setInfo(json["status"].toString());
199         }
200 
201         if (!json["logo"].isNull()){
202             channel->setLogourl(json["logo"].toString());
203         }
204 
205         if (!json["_id"].isNull()){
206             const QJsonValue & _id = json["_id"];
207             channel->setId(_id.isString() ? _id.toString().toInt() : static_cast<quint32>(_id.toDouble()));
208         }
209     }
210 
211     return channel;
212 }
213 
parseVod(const QJsonObject & json)214 Vod *JsonParser::parseVod(const QJsonObject &json)
215 {
216     Vod *vod = new Vod();
217 
218     if (!json["_id"].isNull())
219         vod->setId(json["_id"].toString());
220 
221     if (!json["preview"].isNull()) {
222         const QJsonValue & preview = json["preview"];
223         if (preview.isString()) {
224             vod->setPreview(preview.toString());
225         }
226         else if (preview.isObject()) {
227             const QJsonValue & previewUrl = preview.toObject()["large"];
228             if (previewUrl.isString()) {
229                 vod->setPreview(previewUrl.toString());
230             }
231         }
232     }
233 
234     if (!json["seek_previews_url"].isNull())
235         vod->setSeekPreviews(json["seek_previews_url"].toString());
236 
237     if (!json["title"].isNull())
238         vod->setTitle(json["title"].toString());
239 
240     if (!json["length"].isNull())
241         vod->setDuration(json["length"].toInt());
242 
243     if (!json["game"].isNull())
244         vod->setGame(json["game"].toString());
245 
246     if (!json["views"].isNull())
247         vod->setViews(json["views"].toInt());
248 
249     if (!json["created_at"].isNull())
250         vod->setCreatedAt(json["created_at"].toString());
251 
252     return vod;
253 }
254 
parseChannels(const QByteArray & data)255 PagedResult<Channel*> JsonParser::parseChannels(const QByteArray &data)
256 {
257     PagedResult<Channel*> out;
258 
259     QJsonParseError error;
260     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
261     if (error.error == QJsonParseError::NoError){
262         QJsonObject json = doc.object();
263 
264         QJsonArray arr = json["channels"].toArray();
265         foreach (const QJsonValue &item, arr){
266             out.items.append(JsonParser::parseChannelJson(item.toObject()));
267         }
268 
269         out.total = json["_total"].toInt();
270     }
271 
272     return out;
273 }
274 
parseFavourites(const QByteArray & data)275 PagedResult<Channel *> JsonParser::parseFavourites(const QByteArray &data)
276 {
277     PagedResult<Channel *> out;
278 
279     QJsonParseError error;
280     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
281     if (error.error == QJsonParseError::NoError){
282         QJsonObject json = doc.object();
283 
284         out.total = json["_total"].toInt();
285 
286         QJsonArray arr = json["follows"].toArray();
287         foreach (const QJsonValue &item, arr){
288             out.items.append(JsonParser::parseChannelJson(item.toObject()["channel"].toObject()));
289         }
290     }
291 
292     return out;
293 }
294 
parseFeatured(const QByteArray & data)295 QList<Channel *> JsonParser::parseFeatured(const QByteArray &data)
296 {
297     QList<Channel*> channels;
298 
299     QJsonParseError error;
300     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
301     if (error.error == QJsonParseError::NoError){
302         QJsonObject json = doc.object();
303 
304         if (!json["featured"].isNull()){
305             foreach (const QJsonValue &item, json["featured"].toArray()){
306                 channels.append(JsonParser::parseStreamJson(item.toObject()["stream"].toObject(), true));
307             }
308         }
309     }
310 
311     return channels;
312 }
313 
parseVods(const QByteArray & data)314 QList<Vod *> JsonParser::parseVods(const QByteArray &data)
315 {
316     QList<Vod *> vods;
317 
318     QJsonParseError error;
319     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
320     if (error.error == QJsonParseError::NoError){
321         QJsonObject json = doc.object();
322 
323         if (!json["videos"].isNull()){
324             foreach (const QJsonValue &item, json["videos"].toArray()){
325                 vods.append(JsonParser::parseVod(item.toObject()));
326             }
327         }
328     }
329 
330     return vods;
331 }
332 
parseChannelStreamExtractionInfo(const QByteArray & data)333 QString JsonParser::parseChannelStreamExtractionInfo(const QByteArray &data)
334 {
335     QString url;
336 
337     QJsonParseError error;
338     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
339     if (error.error == QJsonParseError::NoError){
340         QJsonObject json = doc.object();
341 
342         QString tokenData = json["token"].toString();
343 
344         //Strip escape markings and spaces
345         //tokenData = tokenData.trimmed().remove("\\");
346 
347         QString channel;
348 
349         QJsonDocument tokenDoc = QJsonDocument::fromJson(tokenData.toUtf8(), &error);
350         if (error.error == QJsonParseError::NoError){
351             QJsonObject tokenJson = tokenDoc.object();
352             channel = tokenJson["channel"].toString();
353         }
354 
355         QString sig = json["sig"].toString();
356 
357         url = QString("http://usher.twitch.tv/api/channel/hls/%1").arg(channel + QString(".m3u8"))
358                 + QString("?player=twitchweb")
359                 + QString("&token=") + QUrl::toPercentEncoding(tokenData)
360                 + QString("&sig=%1").arg(sig)
361                 + QString("&allow_source=true&$allow_audio_only=true");
362     }
363 
364     return url;
365 }
366 
parseVodExtractionInfo(const QByteArray & data)367 QString JsonParser::parseVodExtractionInfo(const QByteArray &data)
368 {
369     QString url;
370 
371     QJsonParseError error;
372     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
373     if (error.error == QJsonParseError::NoError){
374         QJsonObject json = doc.object();
375 
376         QString tokenData = json["token"].toString();
377         QString sig = json["sig"].toString();
378 
379         //Strip escape markings and spaces
380         tokenData = tokenData.trimmed().remove("\\");
381 
382         QString vod;
383 
384         QJsonDocument tokenDoc = QJsonDocument::fromJson(tokenData.toUtf8(), &error);
385         if (error.error == QJsonParseError::NoError){
386             QJsonObject tokenJson = tokenDoc.object();
387             vod = QString::number(tokenJson["vod_id"].toInt());
388         }
389 
390         url = QString("http://usher.twitch.tv/vod/%1").arg(vod)
391                 + QString("?nauth=%1").arg(tokenData)
392                 + QString("&nauthsig=%1").arg(sig)
393                 + QString("&p=%1").arg(qrand() * 999999)
394                 + "&type=any"
395                   "&player=twitchweb"
396                   "&allow_source=true"
397                   "&allow_audio_only=true";
398     }
399 
400     return url;
401 }
402 
parseUser(const QByteArray & data)403 QPair<QString, quint64> JsonParser::parseUser(const QByteArray &data)
404 {
405     QString displayName;
406     quint64 userId = 0;
407     QJsonParseError error;
408     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
409 
410     if (error.error == QJsonParseError::NoError){
411         QJsonObject json = doc.object();
412         if (!json["name"].isNull())
413             displayName = json["name"].toString();
414         userId = json["_id"].toString().toULongLong();
415     }
416 
417     return qMakePair(displayName, userId);
418 }
419 
parseUsers(const QByteArray & data)420 QList<quint64> JsonParser::parseUsers(const QByteArray &data)
421 {
422     QList<quint64> out;
423 
424     QJsonParseError error;
425     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
426 
427     if (error.error == QJsonParseError::NoError) {
428         QJsonObject json = doc.object();
429         for (const auto & user : json["users"].toArray()) {
430             auto userId = user.toObject()["_id"];
431             if (userId.isDouble()) {
432                 out.append(static_cast<quint64>(userId.toDouble()));
433             }
434             else {
435                 out.append(userId.toString().toULongLong());
436             }
437         }
438     }
439 
440     return out;
441 }
442 
parseEmoteSets(const QByteArray & data)443 QMap<int, QMap<int, QString>> JsonParser::parseEmoteSets(const QByteArray &data) {
444     QMap<int, QMap<int, QString>> out;
445 
446     QJsonParseError error;
447     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
448 
449     //qDebug() << "parsing emote sets response" << data;
450 
451     if (error.error == QJsonParseError::NoError) {
452         QJsonObject json = doc.object();
453         if (!json["emoticon_sets"].isNull()) {
454             auto emoticon_sets = json["emoticon_sets"].toObject();
455             for (auto emoticonSetEntry = emoticon_sets.begin(); emoticonSetEntry != emoticon_sets.end(); emoticonSetEntry++) {
456                 auto emoticonSetID = emoticonSetEntry.key();
457                 QMap<int, QString> curSetEmoticons;
458                 auto emoticons = emoticonSetEntry.value().toArray();
459                 for (auto emoticonEntry = emoticons.begin(); emoticonEntry != emoticons.end(); emoticonEntry++) {
460                     auto emoticonObj = emoticonEntry->toObject();
461                     auto id = emoticonObj["id"];
462                     auto code = emoticonObj["code"];
463                     if (id.isDouble() && code.isString()) {
464                         curSetEmoticons.insert(id.toInt(), code.toString());
465                     }
466                 }
467                 int setId = emoticonSetID.toInt();
468                 //qDebug() << "saving set id" << setId;
469                 out.insert(setId, curSetEmoticons);
470             }
471         }
472     }
473 
474     return out;
475 }
476 
parseChannelBadgeUrls(const QByteArray & data)477 QMap<QString, QMap<QString, QString>> JsonParser::parseChannelBadgeUrls(const QByteArray &data) {
478     QMap<QString, QMap<QString, QString>> out;
479 
480     QJsonParseError error;
481     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
482 
483     if (error.error == QJsonParseError::NoError) {
484         QJsonObject json = doc.object();
485         for (auto badgeEntry = json.begin(); badgeEntry != json.end(); badgeEntry++) {
486             if (badgeEntry.value().isNull()) continue;
487             QMap<QString, QString> urls;
488             QJsonObject badgeEntryJson = badgeEntry.value().toObject();
489             for (auto urlEntry = badgeEntryJson.begin(); urlEntry != badgeEntryJson.end(); urlEntry++) {
490                 urls.insert(urlEntry.key(), urlEntry.value().toString());
491             }
492             out.insert(badgeEntry.key(), urls);
493         }
494     }
495 
496     return out;
497 }
498 
convertJsonStringMap(const QJsonObject & obj)499 QMap<QString, QString> convertJsonStringMap(const QJsonObject & obj) {
500     QMap<QString, QString> out;
501 
502     for (auto entry = obj.constBegin(); entry != obj.constEnd(); entry++) {
503         if (entry.value().isString()) {
504             out.insert(entry.key(), entry.value().toString());
505         }
506     }
507 
508     return out;
509 }
510 
parseBadgeUrlsBetaFormat(const QByteArray & data)511 QMap<QString, QMap<QString, QMap<QString, QString>>> JsonParser::parseBadgeUrlsBetaFormat(const QByteArray &data) {
512     QMap<QString, QMap<QString, QMap<QString, QString>>> out;
513 
514     QJsonParseError error;
515     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
516 
517     if (error.error == QJsonParseError::NoError) {
518         QJsonObject json = doc.object();
519         if (!json["badge_sets"].isNull()) {
520             auto badge_sets = json["badge_sets"].toObject();
521             for (auto badge_set_entry = badge_sets.constBegin(); badge_set_entry != badge_sets.end(); badge_set_entry++) {
522                 QString badge_set_name = badge_set_entry.key();
523 
524                 if (!badge_set_entry.value().isNull()) {
525                     auto badge_set_json = badge_set_entry.value().toObject();
526                     if (!badge_set_json["versions"].isNull()) {
527 
528                         QMap<QString, QMap<QString, QString>> loadedBadgeSet;
529 
530                         auto versions_json = badge_set_json["versions"].toObject();
531                         for (auto version_entry = versions_json.constBegin(); version_entry != versions_json.constEnd(); version_entry++) {
532                             QString version_str = version_entry.key();
533 
534                             if (version_entry.value().isObject()) {
535                                 loadedBadgeSet.insert(version_str, convertJsonStringMap(version_entry.value().toObject()));
536                             }
537                         }
538 
539                         out.insert(badge_set_name, loadedBadgeSet);
540 
541                     }
542 
543                 }
544             }
545         }
546     }
547 
548     return out;
549 }
550 
parseBitsData(const QByteArray & data,QMap<QString,QMap<QString,QString>> & outUrls,QMap<QString,QMap<QString,QString>> & outColors)551 void JsonParser::parseBitsData(const QByteArray &data, QMap<QString, QMap<QString, QString>> & outUrls, QMap<QString, QMap<QString, QString>> & outColors)
552 {
553     const QString BITS_THEME = "dark";
554     const QString BITS_TYPE = "animated";
555     const QString BITS_SIZE_LODPI = "1";
556     const QString BITS_SIZE_HIDPI = "2";
557 
558     const QString BITS_SIZE = SettingsManager::getInstance()->hiDpi() ? BITS_SIZE_HIDPI : BITS_SIZE_LODPI;
559 
560     QJsonParseError error;
561     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
562 
563     if (error.error == QJsonParseError::NoError) {
564         QJsonObject json = doc.object();
565 
566         auto actions = json["actions"].toArray();
567         for (const auto & actionEntry : actions) {
568 
569             QMap<QString, QString> actionUrlsMap;
570             QMap<QString, QString> actionColorsMap;
571 
572             const QJsonObject & actionObj = actionEntry.toObject();
573             QString actionPrefix = actionObj["prefix"].toString();
574 
575             const QJsonArray & tiers = actionObj["tiers"].toArray();
576             for (const auto & tierEntry : tiers) {
577                 const QJsonObject & tierObj = tierEntry.toObject();
578 
579                 int minBits = tierObj["min_bits"].toInt();
580 
581                 const QString & url = tierObj["images"].toObject()[BITS_THEME].toObject()[BITS_TYPE].toObject()[BITS_SIZE].toString();
582                 qDebug() << "bits url for" << actionPrefix << "minBits" << minBits << "is" << url;
583                 actionUrlsMap.insert(QString::number(minBits), url);
584 
585                 const QString & color = tierObj["color"].toString();
586                 actionColorsMap.insert(QString::number(minBits), color);
587             }
588 
589             if (actionUrlsMap.size() > 0) {
590                 outUrls.insert(actionPrefix, actionUrlsMap);
591             }
592 
593             if (actionColorsMap.size() > 0) {
594                 outColors.insert(actionPrefix, actionColorsMap);
595             }
596         }
597     }
598 }
599 
parseTotal(const QByteArray & data)600 int JsonParser::parseTotal(const QByteArray &data)
601 {
602     int total = 0;
603     QJsonParseError error;
604     QJsonDocument doc = QJsonDocument::fromJson(data,&error);
605 
606     if (error.error == QJsonParseError::NoError){
607         QJsonObject json = doc.object();
608         if (!json["_total"].isNull())
609             total = json["_total"].toInt();
610     }
611 
612     return total;
613 }
614 
unicodeLen(const QString & text)615 int unicodeLen(const QString & text) {
616     int out = 0;
617     for (auto i = 0; i < text.length(); i++) {
618         auto ch = text.at(i);
619         if ((ch < 0xd800) || (ch > 0xdbff)) {
620             ++out;
621         }
622     }
623     return out;
624 }
625 
parseVodChatEntry(const QJsonValue & entry)626 ReplayChatMessage parseVodChatEntry(const QJsonValue &entry) {
627     ReplayChatMessage out;
628 
629     const QJsonObject & attributes = entry.toObject();
630 
631     auto msgId = attributes["_id"].toString();
632     out.id = msgId;
633 
634     auto commenter = attributes["commenter"].toObject();
635 
636     auto name = commenter["name"].toString();
637     out.from = name;
638     QString state = attributes["state"].toString();
639     bool deleted = state != QString("published");
640     out.deleted = deleted; // XXX other types to take as valid?
641 
642     auto message = attributes["message"].toObject();
643     out.message = message["body"].toString();
644     auto channelId = attributes["channel_id"].toString();
645     out.room = channelId;
646     out.videoOffset = attributes["content_offset_seconds"].toDouble() * 1000.0;
647     out.timestamp = out.videoOffset;
648 
649     auto source = attributes["source"].toString();
650     if (source == QString("chat")) {
651         out.command = "PRIVMSG";
652     }
653     else {
654         // XXX need some more guesses for these
655         qDebug() << "unknown message source" << source; // XXX remove
656         out.command = "PRIVMSG";
657     }
658 
659     int unicodePos = 0;
660     auto fragments = message["fragments"].toArray();
661 
662     QSet<int> emoteIdsSeen;
663 
664     for (const auto & fragment : fragments) {
665         auto fragmentObj = fragment.toObject();
666         auto text = fragmentObj["text"].toString();
667 
668         auto curTextUnicodeLen = unicodeLen(text);
669 
670         if (!fragmentObj["emoticon"].isNull()) {
671             auto emoteObj = fragmentObj["emoticon"].toObject();
672 
673             QString emotioconIDStr = emoteObj["emoticon_id"].toString();
674             int first = unicodePos;
675             int last = unicodePos + curTextUnicodeLen - 1; // XXX off by one?
676             int emoteId = emotioconIDStr.toInt();
677             if (emoteIdsSeen.constFind(emoteId) == emoteIdsSeen.constEnd()) {
678                 emoteIdsSeen.insert(emoteId);
679                 out.emoteList.append(emoteId);
680             }
681             out.emotePositionsMap.insert(first, qMakePair(last, emoteId));
682         }
683 
684         unicodePos += curTextUnicodeLen;
685 
686     }
687 
688     // XXX tags collection stuff not hooked up yet
689     // system-msg
690 
691     // @badges=staff/1,broadcaster/1,turbo/1;color=#008000;display-name=TWITCH_UserName;emotes=;mod=0;msg-id=resub;msg-param-months=6;room-id=1337;subscriber=1;system-msg=TWITCH_UserName\shas\ssubscribed\sfor\s6\smonths!;login=twitch_username;turbo=1;user-id=1337;user-type=staff :tmi.twitch.tv USERNOTICE #channel :Great stream -- keep it up!
692     QList<QString> badges;
693     for (const auto & badge : message["user_badges"].toArray()) {
694         auto badgeObj = badge.toObject();
695         auto badgeId = badgeObj["_id"].toString();
696         badges.append(badgeId + QString("/") + badgeObj["version"].toString());
697 
698         // not sure if anything in the front end needs these as tags if we added the badges directly
699         if (badgeId == QString("moderator")) {
700             out.tags.insert("mod", true);
701         }
702 
703         if (badgeId == QString("subscriber")) {
704             out.tags.insert("subscriber", true);
705         }
706 
707         if (badgeId == QString("turbo")) {
708             out.tags.insert("turbo", true);
709         }
710     }
711 
712     if (!badges.isEmpty()) {
713         out.tags.insert("badges", badges.join(","));
714     }
715 
716     // most of this stuff is being populated for backward compatibility with things that currently operate on the tags
717     // and can be taken out if nothing uses it
718     out.tags.insert("msg-id", msgId);
719     out.tags.insert("display-name", commenter["display_name"].toString());
720     out.tags.insert("login", name);
721     out.tags.insert("color", message["user_color"].toString());
722     out.tags.insert("room-id", channelId);
723     out.tags.insert("user-id", commenter["_id"].toString());
724     out.tags.insert("user-type", commenter["type"].toString());
725 
726     return out;
727 }
728 
parseVodChatPiece(const QByteArray & data)729 ReplayChatPiece JsonParser::parseVodChatPiece(const QByteArray &data)
730 {
731     ReplayChatPiece out;
732     QJsonParseError error;
733     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
734 
735     if (error.error == QJsonParseError::NoError) {
736         QJsonObject json = doc.object();
737 
738         if (!json["comments"].isNull() && json["comments"].isArray()) {
739             const QJsonArray & chatEntries = json["comments"].toArray();
740             for (const auto & entry : chatEntries) {
741                 out.comments.append(parseVodChatEntry(entry));
742             }
743         }
744 
745         out.next = json["_next"].toString();
746         out.prev = json["_prev"].toString();
747     }
748 
749     return out;
750 }
751 
parseChatterList(const QByteArray & data)752 QMap<QString, QList<QString>> JsonParser::parseChatterList(const QByteArray &data)
753 {
754     QMap<QString, QList<QString>> out;
755 
756     QJsonParseError error;
757     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
758 
759     if (error.error == QJsonParseError::NoError) {
760         QJsonObject json = doc.object();
761 
762         QJsonObject chatters = json["chatters"].toObject();
763 
764         for (auto groupEntry = chatters.constBegin(); groupEntry != chatters.constEnd(); groupEntry++) {
765             QList<QString> groupChatters;
766             const QJsonArray & groupChattersJson = groupEntry.value().toArray();
767             for (const auto & chatter : groupChattersJson) {
768                 groupChatters.append(chatter.toString());
769             }
770             out.insert(groupEntry.key(), groupChatters);
771         }
772     }
773 
774     return out;
775 
776 }
777 
parseBlockList(const QByteArray & data)778 PagedResult<QString> JsonParser::parseBlockList(const QByteArray &data)
779 {
780     PagedResult<QString> out;
781 
782     QJsonParseError error;
783     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
784 
785     if (error.error == QJsonParseError::NoError) {
786         QJsonObject json = doc.object();
787 
788         out.total = json["_total"].toInt();
789 
790         QJsonArray blocks = json["blocks"].toArray();
791 
792         for (const auto & block : blocks) {
793             const auto & blockObj = block.toObject();
794             const auto & name = blockObj["user"].toObject()["name"].toString();
795             if (!name.isEmpty()) {
796                 out.items.append(name);
797             }
798         }
799     }
800 
801     return out;
802 }
803 
804 
parseBttvEmotesData(const QByteArray & data)805 QMap<QString, QString> JsonParser::parseBttvEmotesData(const QByteArray &data)
806 {
807     QMap<QString, QString> out;
808 
809     QJsonParseError error;
810     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
811 
812     if (error.error == QJsonParseError::NoError) {
813 
814         QJsonArray emotes;
815 
816         if (doc.isArray()) {
817             // handle global emotes
818             emotes = doc.array();
819         } else {
820             // handle channel and shared emotes
821             QJsonObject obj = doc.object();
822             emotes = doc["sharedEmotes"].toArray();
823 
824             // merge channelEmotes into sharedEmotes
825             for (const auto & emote : doc["channelEmotes"].toArray()) {
826                 emotes.push_back(emote);
827             }
828         }
829 
830         for (const auto & emote : emotes) {
831             const auto & emoteObj = emote.toObject();
832             const auto & id = emoteObj["id"].toString();
833             const auto & code = emoteObj["code"].toString();
834             if (!id.isEmpty() && !code.isEmpty()) {
835                 out.insert(code, id);
836             }
837         }
838     }
839 
840     return out;
841 }
842 
parseVersion(const QByteArray & data)843 QPair<QString,QString> JsonParser::parseVersion(const QByteArray &data)
844 {
845     QJsonParseError error;
846     QJsonDocument doc = QJsonDocument::fromJson(data, &error);
847     QString version;
848     QString url;
849 
850     if (error.error == QJsonParseError::NoError) {
851         QJsonObject json = doc.object();
852         version = json["name"].toString();
853         url = json["html_url"].toString();
854     }
855 
856     return qMakePair<QString,QString>(version, url);
857 }