1 #include "providers/ffz/FfzEmotes.hpp"
2
3 #include <QJsonArray>
4
5 #include "common/NetworkRequest.hpp"
6 #include "common/Outcome.hpp"
7 #include "common/QLogging.hpp"
8 #include "messages/Emote.hpp"
9 #include "messages/Image.hpp"
10 #include "messages/MessageBuilder.hpp"
11 #include "providers/twitch/TwitchChannel.hpp"
12
13 namespace chatterino {
14 namespace {
15
16 const QString CHANNEL_HAS_NO_EMOTES(
17 "This channel has no FrankerFaceZ channel emotes.");
18
getEmoteLink(const QJsonObject & urls,const QString & emoteScale)19 Url getEmoteLink(const QJsonObject &urls, const QString &emoteScale)
20 {
21 auto emote = urls.value(emoteScale);
22 if (emote.isUndefined() || emote.isNull())
23 {
24 return {""};
25 }
26
27 assert(emote.isString());
28
29 return {"https:" + emote.toString()};
30 }
fillInEmoteData(const QJsonObject & urls,const EmoteName & name,const QString & tooltip,Emote & emoteData)31 void fillInEmoteData(const QJsonObject &urls, const EmoteName &name,
32 const QString &tooltip, Emote &emoteData)
33 {
34 auto url1x = getEmoteLink(urls, "1");
35 auto url2x = getEmoteLink(urls, "2");
36 auto url3x = getEmoteLink(urls, "4");
37
38 //, code, tooltip
39 emoteData.name = name;
40 emoteData.images =
41 ImageSet{Image::fromUrl(url1x, 1),
42 url2x.string.isEmpty() ? Image::getEmpty()
43 : Image::fromUrl(url2x, 0.5),
44 url3x.string.isEmpty() ? Image::getEmpty()
45 : Image::fromUrl(url3x, 0.25)};
46 emoteData.tooltip = {tooltip};
47 }
cachedOrMake(Emote && emote,const EmoteId & id)48 EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id)
49 {
50 static std::unordered_map<EmoteId, std::weak_ptr<const Emote>> cache;
51 static std::mutex mutex;
52
53 return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id);
54 }
parseGlobalEmotes(const QJsonObject & jsonRoot,const EmoteMap & currentEmotes)55 std::pair<Outcome, EmoteMap> parseGlobalEmotes(
56 const QJsonObject &jsonRoot, const EmoteMap ¤tEmotes)
57 {
58 auto jsonSets = jsonRoot.value("sets").toObject();
59 auto emotes = EmoteMap();
60
61 for (auto jsonSet : jsonSets)
62 {
63 auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray();
64
65 for (auto jsonEmoteValue : jsonEmotes)
66 {
67 auto jsonEmote = jsonEmoteValue.toObject();
68
69 auto name = EmoteName{jsonEmote.value("name").toString()};
70 auto id =
71 EmoteId{QString::number(jsonEmote.value("id").toInt())};
72 auto urls = jsonEmote.value("urls").toObject();
73
74 auto emote = Emote();
75 fillInEmoteData(urls, name,
76 name.string + "<br>Global FFZ Emote", emote);
77 emote.homePage =
78 Url{QString("https://www.frankerfacez.com/emoticon/%1-%2")
79 .arg(id.string)
80 .arg(name.string)};
81
82 emotes[name] =
83 cachedOrMakeEmotePtr(std::move(emote), currentEmotes);
84 }
85 }
86
87 return {Success, std::move(emotes)};
88 }
89
parseAuthorityBadge(const QJsonObject & badgeUrls,const QString tooltip)90 boost::optional<EmotePtr> parseAuthorityBadge(const QJsonObject &badgeUrls,
91 const QString tooltip)
92 {
93 boost::optional<EmotePtr> authorityBadge;
94
95 if (!badgeUrls.isEmpty())
96 {
97 auto authorityBadge1x = getEmoteLink(badgeUrls, "1");
98 auto authorityBadge2x = getEmoteLink(badgeUrls, "2");
99 auto authorityBadge3x = getEmoteLink(badgeUrls, "4");
100
101 auto authorityBadgeImageSet = ImageSet{
102 Image::fromUrl(authorityBadge1x, 1),
103 authorityBadge2x.string.isEmpty()
104 ? Image::getEmpty()
105 : Image::fromUrl(authorityBadge2x, 0.5),
106 authorityBadge3x.string.isEmpty()
107 ? Image::getEmpty()
108 : Image::fromUrl(authorityBadge3x, 0.25),
109 };
110
111 authorityBadge = std::make_shared<Emote>(Emote{
112 {""},
113 authorityBadgeImageSet,
114 Tooltip{tooltip},
115 authorityBadge1x,
116 });
117 }
118 return authorityBadge;
119 }
120
parseChannelEmotes(const QJsonObject & jsonRoot)121 EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot)
122 {
123 auto jsonSets = jsonRoot.value("sets").toObject();
124 auto emotes = EmoteMap();
125
126 for (auto jsonSet : jsonSets)
127 {
128 auto jsonEmotes = jsonSet.toObject().value("emoticons").toArray();
129
130 for (auto _jsonEmote : jsonEmotes)
131 {
132 auto jsonEmote = _jsonEmote.toObject();
133
134 // margins
135 auto id =
136 EmoteId{QString::number(jsonEmote.value("id").toInt())};
137 auto name = EmoteName{jsonEmote.value("name").toString()};
138 auto author = EmoteAuthor{jsonEmote.value("owner")
139 .toObject()
140 .value("display_name")
141 .toString()};
142 auto urls = jsonEmote.value("urls").toObject();
143
144 Emote emote;
145 fillInEmoteData(urls, name,
146 QString("%1<br>Channel FFZ Emote<br>By: %2")
147 .arg(name.string)
148 .arg(author.string),
149 emote);
150 emote.homePage =
151 Url{QString("https://www.frankerfacez.com/emoticon/%1-%2")
152 .arg(id.string)
153 .arg(name.string)};
154
155 emotes[name] = cachedOrMake(std::move(emote), id);
156 }
157 }
158
159 return emotes;
160 }
161 } // namespace
162
FfzEmotes()163 FfzEmotes::FfzEmotes()
164 : global_(std::make_shared<EmoteMap>())
165 {
166 }
167
emotes() const168 std::shared_ptr<const EmoteMap> FfzEmotes::emotes() const
169 {
170 return this->global_.get();
171 }
172
emote(const EmoteName & name) const173 boost::optional<EmotePtr> FfzEmotes::emote(const EmoteName &name) const
174 {
175 auto emotes = this->global_.get();
176 auto it = emotes->find(name);
177 if (it != emotes->end())
178 return it->second;
179 return boost::none;
180 }
181
loadEmotes()182 void FfzEmotes::loadEmotes()
183 {
184 QString url("https://api.frankerfacez.com/v1/set/global");
185
186 NetworkRequest(url)
187
188 .timeout(30000)
189 .onSuccess([this](auto result) -> Outcome {
190 auto emotes = this->emotes();
191 auto pair = parseGlobalEmotes(result.parseJson(), *emotes);
192 if (pair.first)
193 this->global_.set(
194 std::make_shared<EmoteMap>(std::move(pair.second)));
195 return pair.first;
196 })
197 .execute();
198 }
199
loadChannel(std::weak_ptr<Channel> channel,const QString & channelId,std::function<void (EmoteMap &&)> emoteCallback,std::function<void (boost::optional<EmotePtr>)> modBadgeCallback,std::function<void (boost::optional<EmotePtr>)> vipBadgeCallback,bool manualRefresh)200 void FfzEmotes::loadChannel(
201 std::weak_ptr<Channel> channel, const QString &channelId,
202 std::function<void(EmoteMap &&)> emoteCallback,
203 std::function<void(boost::optional<EmotePtr>)> modBadgeCallback,
204 std::function<void(boost::optional<EmotePtr>)> vipBadgeCallback,
205 bool manualRefresh)
206 {
207 qCDebug(chatterinoFfzemotes)
208 << "[FFZEmotes] Reload FFZ Channel Emotes for channel" << channelId;
209
210 NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelId)
211
212 .timeout(20000)
213 .onSuccess([emoteCallback = std::move(emoteCallback),
214 modBadgeCallback = std::move(modBadgeCallback),
215 vipBadgeCallback = std::move(vipBadgeCallback), channel,
216 manualRefresh](auto result) -> Outcome {
217 auto json = result.parseJson();
218 auto emoteMap = parseChannelEmotes(json);
219 auto modBadge = parseAuthorityBadge(
220 json.value("room").toObject().value("mod_urls").toObject(),
221 "Moderator");
222 auto vipBadge = parseAuthorityBadge(
223 json.value("room").toObject().value("vip_badge").toObject(),
224 "VIP");
225
226 bool hasEmotes = !emoteMap.empty();
227
228 emoteCallback(std::move(emoteMap));
229 modBadgeCallback(std::move(modBadge));
230 vipBadgeCallback(std::move(vipBadge));
231 if (auto shared = channel.lock(); manualRefresh)
232 {
233 if (hasEmotes)
234 {
235 shared->addMessage(makeSystemMessage(
236 "FrankerFaceZ channel emotes reloaded."));
237 }
238 else
239 {
240 shared->addMessage(
241 makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
242 }
243 }
244
245 return Success;
246 })
247 .onError([channelId, channel, manualRefresh](NetworkResult result) {
248 auto shared = channel.lock();
249 if (!shared)
250 return;
251 if (result.status() == 404)
252 {
253 // User does not have any FFZ emotes
254 if (manualRefresh)
255 shared->addMessage(
256 makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
257 }
258 else if (result.status() == NetworkResult::timedoutStatus)
259 {
260 // TODO: Auto retry in case of a timeout, with a delay
261 qCWarning(chatterinoFfzemotes)
262 << "Fetching FFZ emotes for channel" << channelId
263 << "failed due to timeout";
264 shared->addMessage(
265 makeSystemMessage("Failed to fetch FrankerFaceZ channel "
266 "emotes. (timed out)"));
267 }
268 else
269 {
270 qCWarning(chatterinoFfzemotes)
271 << "Error fetching FFZ emotes for channel" << channelId
272 << ", error" << result.status();
273 shared->addMessage(
274 makeSystemMessage("Failed to fetch FrankerFaceZ channel "
275 "emotes. (unknown error)"));
276 }
277 })
278 .execute();
279 }
280
281 } // namespace chatterino
282