The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

275 lines
9.5KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 6 technical preview.
  4. Copyright (c) 2020 - Raw Material Software Limited
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For this technical preview, this file is not subject to commercial licensing.
  8. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  9. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  10. DISCLAIMED.
  11. ==============================================================================
  12. */
  13. #pragma once
  14. //==============================================================================
  15. class LicenseQueryThread : public Thread
  16. {
  17. public:
  18. LicenseQueryThread (const String& userEmail, const String& userPassword,
  19. std::function<void(LicenseState, String)>&& cb)
  20. : Thread ("LicenseQueryThread"),
  21. email (userEmail),
  22. password (userPassword),
  23. completionCallback (std::move (cb))
  24. {
  25. startThread();
  26. }
  27. ~LicenseQueryThread() override
  28. {
  29. signalThreadShouldExit();
  30. waitForThreadToExit (6000);
  31. }
  32. void run() override
  33. {
  34. LicenseState state;
  35. auto errorMessage = runJob (std::make_unique<UserLogin> (email, password), state);
  36. if (errorMessage.isEmpty())
  37. errorMessage = runJob (std::make_unique<UserLicenseQuery> (state.authToken), state);
  38. if (errorMessage.isNotEmpty())
  39. state = {};
  40. WeakReference<LicenseQueryThread> weakThis (this);
  41. MessageManager::callAsync ([this, weakThis, state, errorMessage]
  42. {
  43. if (weakThis != nullptr)
  44. completionCallback (state, errorMessage);
  45. });
  46. }
  47. private:
  48. //==============================================================================
  49. struct AccountEnquiryBase
  50. {
  51. virtual ~AccountEnquiryBase() = default;
  52. virtual bool isPOSTLikeRequest() const = 0;
  53. virtual String getEndpointURLSuffix() const = 0;
  54. virtual StringPairArray getParameterNamesAndValues() const = 0;
  55. virtual int getSuccessCode() const = 0;
  56. virtual String errorCodeToString (int) const = 0;
  57. virtual bool parseServerResponse (const String&, LicenseState&) = 0;
  58. };
  59. struct UserLogin : public AccountEnquiryBase
  60. {
  61. UserLogin (const String& e, const String& p)
  62. : userEmail (e), userPassword (p)
  63. {
  64. }
  65. bool isPOSTLikeRequest() const override { return true; }
  66. String getEndpointURLSuffix() const override { return "/authenticate"; }
  67. int getSuccessCode() const override { return 200; }
  68. StringPairArray getParameterNamesAndValues() const override
  69. {
  70. StringPairArray namesAndValues;
  71. namesAndValues.set ("email", userEmail);
  72. namesAndValues.set ("password", userPassword);
  73. return namesAndValues;
  74. }
  75. String errorCodeToString (int errorCode) const override
  76. {
  77. switch (errorCode)
  78. {
  79. case 400: return "Please enter your email and password to log in.";
  80. case 401: return "Your email and password are incorrect.";
  81. case 451: return "Access denied.";
  82. default: return "Something went wrong, please try again.";
  83. }
  84. }
  85. bool parseServerResponse (const String& serverResponse, LicenseState& licenseState) override
  86. {
  87. auto json = JSON::parse (serverResponse);
  88. licenseState.authToken = json.getProperty ("token", {}).toString();
  89. licenseState.username = json.getProperty ("user", {}).getProperty ("username", {}).toString();
  90. auto avatarURL = json.getProperty ("user", {}).getProperty ("avatar_url", {}).toString();
  91. if (avatarURL.isNotEmpty())
  92. {
  93. URL url (avatarURL);
  94. if (auto stream = url.createInputStream (false, nullptr, nullptr, {}, 5000))
  95. {
  96. MemoryBlock mb;
  97. stream->readIntoMemoryBlock (mb);
  98. licenseState.avatar = ImageFileFormat::loadFrom (mb.getData(), mb.getSize());
  99. }
  100. }
  101. return (licenseState.authToken.isNotEmpty() && licenseState.username.isNotEmpty());
  102. }
  103. String userEmail, userPassword;
  104. };
  105. struct UserLicenseQuery : public AccountEnquiryBase
  106. {
  107. UserLicenseQuery (const String& authToken)
  108. : userAuthToken (authToken)
  109. {
  110. }
  111. bool isPOSTLikeRequest() const override { return false; }
  112. String getEndpointURLSuffix() const override { return "/user/licences"; }
  113. int getSuccessCode() const override { return 200; }
  114. StringPairArray getParameterNamesAndValues() const override
  115. {
  116. StringPairArray namesAndValues;
  117. namesAndValues.set ("token", userAuthToken);
  118. return namesAndValues;
  119. }
  120. String errorCodeToString (int errorCode) const override
  121. {
  122. switch (errorCode)
  123. {
  124. case 401: return "User not found or could not be verified.";
  125. default: return "User licenses info fetch failed (unknown error).";
  126. }
  127. }
  128. bool parseServerResponse (const String& serverResponse, LicenseState& licenseState) override
  129. {
  130. auto json = JSON::parse (serverResponse);
  131. if (auto* licensesJson = json.getArray())
  132. {
  133. StringArray licenseTypes;
  134. for (auto& license : *licensesJson)
  135. {
  136. auto name = license.getProperty ("product_name", {}).toString();
  137. auto status = license.getProperty ("status", {}).toString();
  138. if (name == "Projucer" && status == "active")
  139. licenseTypes.add (license.getProperty ("licence_type", {}).toString());
  140. }
  141. licenseTypes.removeEmptyStrings();
  142. licenseTypes.removeDuplicates (false);
  143. licenseState.type = [licenseTypes] ()
  144. {
  145. if (licenseTypes.contains ("juce-pro")) return LicenseState::Type::pro;
  146. else if (licenseTypes.contains ("juce-indie")) return LicenseState::Type::indie;
  147. else if (licenseTypes.contains ("juce-personal")) return LicenseState::Type::personal;
  148. else if (licenseTypes.contains ("juce-edu")) return LicenseState::Type::educational;
  149. return LicenseState::Type::none;
  150. }();
  151. return (licenseState.type != LicenseState::Type::none);
  152. }
  153. return false;
  154. }
  155. String userAuthToken;
  156. };
  157. //==============================================================================
  158. static String postDataStringAsJSON (const StringPairArray& parameters)
  159. {
  160. DynamicObject::Ptr d (new DynamicObject());
  161. for (auto& key : parameters.getAllKeys())
  162. d->setProperty (key, parameters[key]);
  163. return JSON::toString (var (d.get()));
  164. }
  165. String runJob (std::unique_ptr<AccountEnquiryBase> accountEnquiryJob, LicenseState& state)
  166. {
  167. const String endpointURL = "https://api.roli.com/api/v1";
  168. const String extraHeaders = "Content-Type: application/json";
  169. auto url = URL (endpointURL + accountEnquiryJob->getEndpointURLSuffix());
  170. auto isPOST = accountEnquiryJob->isPOSTLikeRequest();
  171. if (isPOST)
  172. url = url.withPOSTData (postDataStringAsJSON (accountEnquiryJob->getParameterNamesAndValues()));
  173. else
  174. url = url.withParameters (accountEnquiryJob->getParameterNamesAndValues());
  175. if (threadShouldExit())
  176. return "Cancelled.";
  177. int statusCode = 0;
  178. auto urlStream = url.createInputStream (isPOST, nullptr, nullptr, extraHeaders, 5000, nullptr, &statusCode);
  179. if (urlStream == nullptr)
  180. return "Failed to connect to the web server.";
  181. if (statusCode != accountEnquiryJob->getSuccessCode())
  182. return accountEnquiryJob->errorCodeToString (statusCode);
  183. if (threadShouldExit())
  184. return "Cancelled.";
  185. String response;
  186. for (;;)
  187. {
  188. char buffer [8192];
  189. auto num = urlStream->read (buffer, sizeof (buffer));
  190. if (threadShouldExit())
  191. return "Cancelled.";
  192. if (num <= 0)
  193. break;
  194. response += buffer;
  195. }
  196. if (threadShouldExit())
  197. return "Cancelled.";
  198. if (! accountEnquiryJob->parseServerResponse (response, state))
  199. return "Failed to parse server response.";
  200. return {};
  201. }
  202. //==============================================================================
  203. const String email, password;
  204. const std::function<void(LicenseState, String)> completionCallback;
  205. //==============================================================================
  206. JUCE_DECLARE_WEAK_REFERENCEABLE (LicenseQueryThread)
  207. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LicenseQueryThread)
  208. };