1 /++
2     Helper functions for song request modules.
3 
4     See_Also:
5         [kameloso.plugins.twitch.base],
6         [kameloso.plugins.twitch.api],
7         [kameloso.plugins.twitch.google],
8         [kameloso.plugins.twitch.spotify]
9 
10     Copyright: [JR](https://github.com/zorael)
11     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
12 
13     Authors:
14         [JR](https://github.com/zorael)
15  +/
16 module kameloso.plugins.twitch.common;
17 
18 version(TwitchSupport):
19 version(WithTwitchPlugin):
20 
21 private:
22 
23 import std.json : JSONValue;
24 
25 package:
26 
27 
28 // getHTTPClient
29 /++
30     Returns a static [arsd.http2.HttpClient|HttpClient] for reuse across function calls.
31 
32     Returns:
33         A static [arsd.http2.HttpClient|HttpClient].
34  +/
35 auto getHTTPClient()
36 {
37     import kameloso.constants : KamelosoInfo, Timeout;
38     import arsd.http2 : HttpClient;
39     import core.time : seconds;
40 
41     static HttpClient client;
42 
43     if (!client)
44     {
45         client = new HttpClient;
46         client.useHttp11 = true;
47         client.keepAlive = true;
48         client.acceptGzip = false;
49         client.defaultTimeout = Timeout.httpGET.seconds;
50         client.userAgent = "kameloso/" ~ cast(string)KamelosoInfo.version_;
51     }
52 
53     return client;
54 }
55 
56 
57 // readNamedString
58 /++
59     Prompts the user to enter a string.
60 
61     Params:
62         wording = Wording to use in the prompt.
63         expectedLength = Optional expected length of the input string.
64             A value of `0` disables checks.
65         abort = Abort pointer.
66 
67     Returns:
68         A string read from standard in, stripped.
69  +/
70 auto readNamedString(
71     const string wording,
72     const size_t expectedLength,
73     ref bool abort)
74 {
75     import kameloso.common : logger;
76     import kameloso.logger : LogLevel;
77     import kameloso.terminal.colours.tags : expandTags;
78     import lu.string : stripped;
79     import std.stdio : readln, stdin, stdout, write, writeln;
80 
81     string string_;
82 
83     while (!string_.length)
84     {
85         scope(exit) stdout.flush();
86 
87         write(wording.expandTags(LogLevel.off));
88         stdout.flush();
89 
90         stdin.flush();
91         string_ = readln().stripped;
92 
93         if (abort)
94         {
95             writeln();
96             logger.warning("Aborting.");
97             logger.trace();
98             return string.init;
99         }
100         else if ((expectedLength > 0) && (string_.length != expectedLength))
101         {
102             writeln();
103             enum invalidMessage = "Invalid length. Try copying again or file a bug.";
104             logger.error(invalidMessage);
105             writeln();
106             continue;
107         }
108     }
109 
110     return string_;
111 }
112 
113 
114 // printManualURL
115 /++
116     Prints a URL for manual copy/pasting.
117 
118     Params:
119         url = URL string.
120  +/
121 void printManualURL(const string url)
122 {
123     import kameloso.logger : LogLevel;
124     import kameloso.terminal.colours.tags : expandTags;
125     import std.stdio : writefln;
126 
127     enum copyPastePattern = `
128 <l>Copy and paste this link manually into your browser, and log in as asked:
129 
130 <i>8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8\<</>
131 
132 %s
133 
134 <i>8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8\<</>
135 `;
136     writefln(copyPastePattern.expandTags(LogLevel.off), url);
137 }
138 
139 
140 // TwitchJSONException
141 /++
142     Abstract class for Twitch JSON exceptions, to deduplicate catching.
143  +/
144 abstract class TwitchJSONException : Exception
145 {
146     /++
147         Accessor to a [std.json.JSONValue|JSONValue] that this exception refers to.
148      +/
149     JSONValue json();
150 
151     /++
152         Constructor.
153      +/
154     this(
155         const string message,
156         const string file = __FILE__,
157         const size_t line = __LINE__,
158         Throwable nextInChain = null) pure nothrow @nogc @safe
159     {
160         super(message, file, line, nextInChain);
161     }
162 }
163 
164 
165 // UnexpectedJSONException
166 /++
167     A normal [object.Exception|Exception] but where its type conveys the specific
168     context of a [std.json.JSONValue|JSONValue] having unexpected contents.
169 
170     It optionally embeds the JSON.
171  +/
172 final class UnexpectedJSONException : TwitchJSONException
173 {
174 private:
175     /++
176         [std.json.JSONValue|JSONValue] in question.
177      +/
178     JSONValue _json;
179 
180 public:
181     /++
182         Accessor to [_json].
183      +/
184     override JSONValue json()
185     {
186         return _json;
187     }
188 
189     /++
190         Create a new [UnexpectedJSONException], attaching a [std.json.JSONValue|JSONValue].
191      +/
192     this(
193         const string message,
194         const JSONValue _json,
195         const string file = __FILE__,
196         const size_t line = __LINE__,
197         Throwable nextInChain = null) pure nothrow @nogc @safe
198     {
199         this._json = _json;
200         super(message, file, line, nextInChain);
201     }
202 
203     /++
204         Constructor.
205      +/
206     this(
207         const string message,
208         const string file = __FILE__,
209         const size_t line = __LINE__,
210         Throwable nextInChain = null) pure nothrow @nogc @safe
211     {
212         super(message, file, line, nextInChain);
213     }
214 }
215 
216 
217 // ErrorJSONException
218 /++
219     A normal [object.Exception|Exception] but where its type conveys the specific
220     context of a [std.json.JSONValue|JSONValue] having an `"error"` field.
221 
222     It optionally embeds the JSON.
223  +/
224 final class ErrorJSONException : TwitchJSONException
225 {
226 private:
227     /++
228         [std.json.JSONValue|JSONValue] in question.
229      +/
230     JSONValue _json;
231 
232 public:
233     /++
234         Accessor to [_json].
235      +/
236     override JSONValue json()
237     {
238         return _json;
239     }
240 
241     /++
242         Create a new [ErrorJSONException], attaching a [std.json.JSONValue|JSONValue].
243      +/
244     this(
245         const string message,
246         const JSONValue _json,
247         const string file = __FILE__,
248         const size_t line = __LINE__,
249         Throwable nextInChain = null) pure nothrow @nogc @safe
250     {
251         this._json = _json;
252         super(message, file, line, nextInChain);
253     }
254 
255     /++
256         Constructor.
257      +/
258     this(
259         const string message,
260         const string file = __FILE__,
261         const size_t line = __LINE__,
262         Throwable nextInChain = null) pure nothrow @nogc @safe
263     {
264         super(message, file, line, nextInChain);
265     }
266 }
267 
268 
269 // EmptyDataJSONException
270 /++
271     Exception, to be thrown when an API query to the Twitch servers failed,
272     due to having received empty JSON data.
273 
274     It is a normal [object.Exception|Exception] but with attached metadata.
275  +/
276 final class EmptyDataJSONException : TwitchJSONException
277 {
278 private:
279     /++
280         The response body that was received.
281      +/
282     JSONValue _json;
283 
284 public:
285     /++
286         Accessor to [_json].
287      +/
288     override JSONValue json()
289     {
290         return _json;
291     }
292 
293     /++
294         Create a new [EmptyDataJSONException], attaching a response body.
295      +/
296     this(
297         const string message,
298         const JSONValue _json,
299         const string file = __FILE__,
300         const size_t line = __LINE__,
301         Throwable nextInChain = null) pure nothrow @nogc @safe
302     {
303         this._json = _json;
304         super(message, file, line, nextInChain);
305     }
306 
307     /++
308         Create a new [EmptyDataJSONException], without attaching anything.
309      +/
310     this(
311         const string message,
312         const string file = __FILE__,
313         const size_t line = __LINE__,
314         Throwable nextInChain = null) pure nothrow @nogc @safe
315     {
316         super(message, file, line, nextInChain);
317     }
318 }
319 
320 
321 // TwitchQueryException
322 /++
323     Exception, to be thrown when an API query to the Twitch servers failed,
324     for whatever reason.
325 
326     It is a normal [object.Exception|Exception] but with attached metadata.
327  +/
328 final class TwitchQueryException : Exception
329 {
330 @safe:
331     /// The response body that was received.
332     string responseBody;
333 
334     /// The message of any thrown exception, if the query failed.
335     string error;
336 
337     /// The HTTP code that was received.
338     uint code;
339 
340     /++
341         Create a new [TwitchQueryException], attaching a response body, an error
342         and an HTTP status code.
343      +/
344     this(
345         const string message,
346         const string responseBody,
347         const string error,
348         const uint code,
349         const string file = __FILE__,
350         const size_t line = __LINE__,
351         Throwable nextInChain = null) pure nothrow @nogc @safe
352     {
353         this.responseBody = responseBody;
354         this.error = error;
355         this.code = code;
356         super(message, file, line, nextInChain);
357     }
358 
359     /++
360         Create a new [TwitchQueryException], without attaching anything.
361      +/
362     this(
363         const string message,
364         const string file = __FILE__,
365         const size_t line = __LINE__,
366         Throwable nextInChain = null) pure nothrow @nogc @safe
367     {
368         super(message, file, line, nextInChain);
369     }
370 }
371 
372 
373 // MissingBroadcasterTokenException
374 /++
375     Exception, to be thrown when an API query to the Twitch servers failed,
376     due to missing broadcaster-level token.
377  +/
378 final class MissingBroadcasterTokenException : Exception
379 {
380 @safe:
381     /// The channel name for which a broadcaster token was needed.
382     string channelName;
383 
384     /++
385         Create a new [MissingBroadcasterTokenException], attaching a channel name.
386      +/
387     this(
388         const string message,
389         const string channelName,
390         const string file = __FILE__,
391         const size_t line = __LINE__,
392         Throwable nextInChain = null) pure nothrow @nogc @safe
393     {
394         this.channelName = channelName;
395         super(message, file, line, nextInChain);
396     }
397 
398     /++
399         Create a new [MissingBroadcasterTokenException], without attaching anything.
400      +/
401     this(
402         const string message,
403         const string file = __FILE__,
404         const size_t line = __LINE__,
405         Throwable nextInChain = null) pure nothrow @nogc @safe
406     {
407         super(message, file, line, nextInChain);
408     }
409 }
410 
411 
412 // InvalidCredentialsException
413 /++
414     Exception, to be thrown when credentials or grants are invalid.
415 
416     It is a normal [object.Exception|Exception] but with attached metadata.
417  +/
418 final class InvalidCredentialsException : Exception
419 {
420 @safe:
421     /// The response body that was received.
422     JSONValue json;
423 
424     /++
425         Create a new [InvalidCredentialsException], attaching a response body.
426      +/
427     this(
428         const string message,
429         const JSONValue json,
430         const string file = __FILE__,
431         const size_t line = __LINE__,
432         Throwable nextInChain = null) pure nothrow @nogc @safe
433     {
434         this.json = json;
435         super(message, file, line, nextInChain);
436     }
437 
438     /++
439         Create a new [InvalidCredentialsException], without attaching anything.
440      +/
441     this(
442         const string message,
443         const string file = __FILE__,
444         const size_t line = __LINE__,
445         Throwable nextInChain = null) pure nothrow @nogc @safe
446     {
447         super(message, file, line, nextInChain);
448     }
449 }
450 
451 
452 // EmptyResponseException
453 /++
454     Exception, to be thrown when an API query to the Twitch servers failed,
455     with only an empty response received.
456  +/
457 final class EmptyResponseException : Exception
458 {
459 @safe:
460     /++
461         Create a new [EmptyResponseException].
462      +/
463     this(
464         const string message,
465         const string file = __FILE__,
466         const size_t line = __LINE__,
467         Throwable nextInChain = null) pure nothrow @nogc @safe
468     {
469         super(message, file, line, nextInChain);
470     }
471 }