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 }