PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.34.2
MailPoet – Newsletters, Email Marketing, and Automation v5.34.2
5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / API / JSON / v1 / Services.php
mailpoet / lib / API / JSON / v1 Last commit date
Analytics.php 3 years ago AutomatedLatestContent.php 2 months ago AutomaticEmails.php 3 years ago Captcha.php 1 year ago Coupons.php 2 years ago CustomFields.php 2 months ago DynamicProducts.php 1 year ago DynamicSegments.php 2 months ago FeatureFlags.php 3 years ago Forms.php 2 months ago Help.php 1 year ago ImportExport.php 2 months ago Mailer.php 1 year ago NewsletterLinks.php 2 months ago NewsletterTemplates.php 2 months ago Newsletters.php 2 months ago Premium.php 10 months ago RedirectResponse.php 1 year ago Segments.php 2 months ago SendingQueue.php 2 months ago Services.php 6 months ago Settings.php 2 months ago Setup.php 1 year ago StatisticsExport.php 3 months ago SubscriberStats.php 1 month ago Subscribers.php 2 months ago Tags.php 3 years ago UserFlags.php 2 years ago WoocommerceProductVariations.php 2 months ago WoocommerceSettings.php 3 years ago index.php 3 years ago
Services.php
388 lines
1 <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
2
3 namespace MailPoet\API\JSON\v1;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use MailPoet\Analytics\Analytics as AnalyticsHelper;
9 use MailPoet\API\JSON\Endpoint as APIEndpoint;
10 use MailPoet\API\JSON\Error as APIError;
11 use MailPoet\API\JSON\Response;
12 use MailPoet\Config\AccessControl;
13 use MailPoet\Config\Installer;
14 use MailPoet\Config\ServicesChecker;
15 use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck;
16 use MailPoet\Cron\Workers\KeyCheck\SendingServiceKeyCheck;
17 use MailPoet\Mailer\MailerLog;
18 use MailPoet\Services\AuthorizedEmailsController;
19 use MailPoet\Services\AuthorizedSenderDomainController;
20 use MailPoet\Services\Bridge;
21 use MailPoet\Services\CongratulatoryMssEmailController;
22 use MailPoet\Services\SubscribersCountReporter;
23 use MailPoet\Settings\SettingsController;
24 use MailPoet\Util\Helpers;
25 use MailPoet\WP\DateTime;
26 use MailPoet\WP\Functions as WPFunctions;
27
28 class Services extends APIEndpoint {
29 /** @var Bridge */
30 private $bridge;
31
32 /** @var SettingsController */
33 private $settings;
34
35 /** @var AnalyticsHelper */
36 private $analytics;
37
38 /** @var DateTime */
39 public $dateTime;
40
41 /** @var SendingServiceKeyCheck */
42 private $mssWorker;
43
44 /** @var PremiumKeyCheck */
45 private $premiumWorker;
46
47 /** @var ServicesChecker */
48 private $servicesChecker;
49
50 /** @var CongratulatoryMssEmailController */
51 private $congratulatoryMssEmailController;
52
53 /** @var WPFunctions */
54 private $wp;
55
56 /** @var AuthorizedSenderDomainController */
57 private $senderDomainController;
58
59 /** @var AuthorizedEmailsController */
60 private $authorizedEmailsController;
61
62 /** @var SubscribersCountReporter */
63 private $subscribersCountReporter;
64
65 public $permissions = [
66 'global' => AccessControl::PERMISSION_MANAGE_SETTINGS,
67 'methods' => ['pingBridge' => AccessControl::PERMISSION_ACCESS_PLUGIN_ADMIN],
68 ];
69
70 public function __construct(
71 Bridge $bridge,
72 SettingsController $settings,
73 AnalyticsHelper $analytics,
74 SendingServiceKeyCheck $mssWorker,
75 PremiumKeyCheck $premiumWorker,
76 ServicesChecker $servicesChecker,
77 SubscribersCountReporter $subscribersCountReporter,
78 CongratulatoryMssEmailController $congratulatoryMssEmailController,
79 WPFunctions $wp,
80 AuthorizedSenderDomainController $senderDomainController,
81 AuthorizedEmailsController $authorizedEmailsController
82 ) {
83 $this->bridge = $bridge;
84 $this->settings = $settings;
85 $this->analytics = $analytics;
86 $this->mssWorker = $mssWorker;
87 $this->premiumWorker = $premiumWorker;
88 $this->dateTime = new DateTime();
89 $this->servicesChecker = $servicesChecker;
90 $this->subscribersCountReporter = $subscribersCountReporter;
91 $this->congratulatoryMssEmailController = $congratulatoryMssEmailController;
92 $this->wp = $wp;
93 $this->senderDomainController = $senderDomainController;
94 $this->authorizedEmailsController = $authorizedEmailsController;
95 }
96
97 public function checkMSSKey($data = []) {
98 $key = isset($data['key']) ? trim($data['key']) : null;
99
100 if (!$key) {
101 return $this->badRequest(
102 [
103 APIError::BAD_REQUEST => __('Please specify a key.', 'mailpoet'),
104 ],
105 $this->getMetaForLogging($key)
106 );
107 }
108
109 $wasPendingApproval = $this->servicesChecker->isMailPoetAPIKeyPendingApproval();
110
111 try {
112 $result = $this->bridge->checkMSSKey($key);
113 $this->bridge->storeMSSKeyAndState($key, $result);
114 } catch (\Exception $e) {
115 return $this->errorResponse(
116 [
117 $e->getCode() => $e->getMessage(),
118 ],
119 $this->getMetaForLogging($key)
120 );
121 }
122
123 // pause sending when key is pending approval, resume when not pending anymore
124 $isPendingApproval = $this->servicesChecker->isMailPoetAPIKeyPendingApproval();
125 if (!$wasPendingApproval && $isPendingApproval) {
126 MailerLog::pauseSending(MailerLog::getMailerLog());
127 } elseif ($wasPendingApproval && !$isPendingApproval) {
128 MailerLog::resumeSending();
129 }
130
131 $state = !empty($result['state']) ? $result['state'] : null;
132
133 $successMessage = null;
134 if ($state == Bridge::KEY_VALID) {
135 $successMessage = __('Your MailPoet Sending Service key has been successfully validated', 'mailpoet');
136 } else if ($state == Bridge::KEY_VALID_UNDERPRIVILEGED) {
137 $successMessage = __('Your Premium key has been successfully validated, but is not valid for MailPoet Sending Service', 'mailpoet');
138 } elseif ($state == Bridge::KEY_EXPIRING) {
139 $successMessage = sprintf(
140 // translators: %s is the expiration date.
141 __('Your MailPoet Sending Service key expires on %s!', 'mailpoet'),
142 $this->dateTime->formatDate(strtotime($result['data']['expire_at']))
143 );
144 }
145
146 if (!empty($result['data']['public_id'])) {
147 $this->analytics->setPublicId($result['data']['public_id']);
148 }
149
150 if ($successMessage) {
151 return $this->successResponse(['message' => $successMessage, 'state' => $state, 'result' => $result]);
152 }
153
154 switch ($state) {
155 case Bridge::KEY_INVALID:
156 $error = __('Your key is not valid for the MailPoet Sending Service', 'mailpoet');
157 break;
158 case Bridge::KEY_ALREADY_USED:
159 $error = __('Your MailPoet Sending Service key is already <a>used on another site</a>', 'mailpoet'); // we will use createInterpolateElement to replace <a> element
160 break;
161 default:
162 $code = !empty($result['code']) ? $result['code'] : Bridge::CHECK_ERROR_UNKNOWN;
163 // translators: %s is the error message.
164 $errorMessage = __('Error validating MailPoet Sending Service key, please try again later (%s).', 'mailpoet');
165 // If site runs on localhost
166 if (1 === preg_match("/^(http|https)\:\/\/(localhost|127\.0\.0\.1)/", $this->wp->siteUrl())) {
167 $errorMessage .= ' ' . __("Note that it doesn't work on localhost.", 'mailpoet');
168 }
169 $error = sprintf(
170 $errorMessage,
171 $this->getErrorDescriptionByCode($code)
172 );
173 break;
174 }
175
176 return $this->errorResponse(
177 [APIError::BAD_REQUEST => $error],
178 $this->getMetaForLogging($key)
179 );
180 }
181
182 public function checkPremiumKey($data = []) {
183 $key = isset($data['key']) ? trim($data['key']) : null;
184
185 if (!$key) {
186 return $this->badRequest(
187 [
188 APIError::BAD_REQUEST => __('Please specify a key.', 'mailpoet'),
189 ],
190 $this->getMetaForLogging($key)
191 );
192 }
193
194 try {
195 $result = $this->bridge->checkPremiumKey($key);
196 $this->bridge->storePremiumKeyAndState($key, $result);
197 } catch (\Exception $e) {
198 return $this->errorResponse(
199 [
200 $e->getCode() => $e->getMessage(),
201 ],
202 $this->getMetaForLogging($key)
203 );
204 }
205
206 $state = !empty($result['state']) ? $result['state'] : null;
207
208 $successMessage = null;
209 if ($state == Bridge::KEY_VALID) {
210 $successMessage = __('Your Premium key has been successfully validated', 'mailpoet');
211 } else if ($state == Bridge::KEY_VALID_UNDERPRIVILEGED) {
212 $successMessage = __('Your Premium key has been successfully validated, but is not valid for MailPoet Sending Service', 'mailpoet');
213 } elseif ($state == Bridge::KEY_EXPIRING) {
214 $successMessage = sprintf(
215 // translators: %s is the expiration date.
216 __('Your Premium key expires on %s', 'mailpoet'),
217 $this->dateTime->formatDate(strtotime($result['data']['expire_at']))
218 );
219 }
220
221 if (!empty($result['data']['public_id'])) {
222 $this->analytics->setPublicId($result['data']['public_id']);
223 }
224
225 if ($successMessage) {
226 return $this->successResponse(
227 ['message' => $successMessage, 'state' => $state, 'result' => $result],
228 Installer::getPremiumStatus()
229 );
230 }
231
232 switch ($state) {
233 case Bridge::KEY_INVALID:
234 $error = __('Your key is not valid for MailPoet Premium', 'mailpoet');
235 break;
236 case Bridge::KEY_ALREADY_USED:
237 $error = __('Your Premium key is already <a>used on another site</a>', 'mailpoet'); // we will use createInterpolateElement to replace <a> element
238 break;
239 default:
240 $code = !empty($result['code']) ? $result['code'] : Bridge::CHECK_ERROR_UNKNOWN;
241 $error = sprintf(
242 // translators: %s is the error message.
243 __('Error validating Premium key, please try again later (%s)', 'mailpoet'),
244 $this->getErrorDescriptionByCode($code)
245 );
246 break;
247 }
248
249 return $this->errorResponse(
250 [
251 APIError::BAD_REQUEST => $error,
252 ],
253 array_merge(
254 $this->getMetaForLogging($key),
255 ['code' => $result['code'] ?? null]
256 )
257 );
258 }
259
260 private function getMetaForLogging(?string $key): array {
261 $obfuscatedKey = $key ? substr($key, 0, 4) . "..." . substr($key, -4) : '';
262 return [
263 'key' => $obfuscatedKey,
264 'home_url' => $this->wp->homeUrl(),
265 'site_url' => $this->wp->siteUrl(),
266 ];
267 }
268
269 public function recheckKeys() {
270 // Report subscribers count before rechecking keys so that shop can lift access restrictions in case
271 // user deleted subscribers and no longer exceeds the limit.
272 $key = $this->servicesChecker->getValidAccountKey();
273 if ($key) {
274 $this->subscribersCountReporter->report($key);
275 }
276 $this->mssWorker->init();
277 $mssCheck = $this->mssWorker->checkKey();
278 $this->premiumWorker->init();
279 $premiumCheck = $this->premiumWorker->checkKey();
280 // continue sending when it is paused and states are valid
281 $mailerLog = MailerLog::getMailerLog();
282 if (
283 (isset($mailerLog['status']) && $mailerLog['status'] === MailerLog::STATUS_PAUSED)
284 && (isset($mssCheck['state']) && $mssCheck['state'] === Bridge::KEY_VALID)
285 && (isset($premiumCheck['state']) && $premiumCheck['state'] === Bridge::PREMIUM_KEY_VALID)
286 ) {
287 MailerLog::resumeSending();
288 }
289 return $this->successResponse();
290 }
291
292 public function sendCongratulatoryMssEmail() {
293 if (!Bridge::isMPSendingServiceEnabled()) {
294 return $this->createBadRequest(__('MailPoet Sending Service is not active.', 'mailpoet'));
295 }
296
297 $fromEmail = $this->settings->get('sender.address');
298 if (!$fromEmail) {
299 return $this->createBadRequest(__('Sender email address is not set.', 'mailpoet'));
300 }
301
302 $verifiedDomains = $this->senderDomainController->getVerifiedSenderDomainsIgnoringCache();
303
304 $emailDomain = Helpers::extractEmailDomain($fromEmail);
305
306 if (!$this->isItemInArray($emailDomain, $verifiedDomains)) {
307 $authorizedEmails = $this->authorizedEmailsController->getAuthorizedEmailAddresses();
308
309 if (!$authorizedEmails) {
310 return $this->createBadRequest(__('No FROM email addresses are authorized.', 'mailpoet'));
311 }
312
313 if (!$this->isItemInArray($fromEmail, $authorizedEmails)) {
314 // translators: %s is the email address, which is not authorized.
315 return $this->createBadRequest(sprintf(__("Sender email address '%s' is not authorized.", 'mailpoet'), $fromEmail));
316 }
317 }
318
319 try {
320 // congratulatory email is sent to the current FROM address (authorized at this point)
321 $this->congratulatoryMssEmailController->sendCongratulatoryEmail($fromEmail);
322 } catch (\Throwable $e) {
323 return $this->errorResponse([
324 APIError::UNKNOWN => __('Sending of congratulatory email failed.', 'mailpoet'),
325 ], [], Response::STATUS_UNKNOWN);
326 }
327 return $this->successResponse([
328 'email_address' => $fromEmail,
329 ]);
330 }
331
332 public function pingBridge() {
333 $response = $this->bridge->pingBridge();
334 if ($this->wp->isWpError($response)) {
335 /** @var \WP_Error $response */
336 $errorDesc = $this->getErrorDescriptionByCode(Bridge::CHECK_ERROR_UNKNOWN);
337 return $this->errorResponse([
338 APIError::UNKNOWN => "{$errorDesc}: {$response->get_error_message()}",
339 ]);
340 }
341
342 if (!$this->bridge->validateBridgePingResponse($response)) {
343 $code = $this->wp->wpRemoteRetrieveResponseCode($response) ?: Bridge::CHECK_ERROR_UNKNOWN;
344 return $this->errorResponse([
345 APIError::UNKNOWN => $this->getErrorDescriptionByCode($code),
346 ]);
347 }
348 return $this->successResponse();
349 }
350
351 public function refreshMSSKeyStatus() {
352 $key = $this->settings->get('mta.mailpoet_api_key');
353 return $this->checkMSSKey(['key' => $key]);
354 }
355
356 public function refreshPremiumKeyStatus() {
357 $key = $this->settings->get('premium.premium_key');
358 return $this->checkPremiumKey(['key' => $key]);
359 }
360
361 private function isItemInArray($item, $array): bool {
362 return in_array($item, $array, true);
363 }
364
365 private function getErrorDescriptionByCode($code) {
366 switch ($code) {
367 case Bridge::CHECK_ERROR_UNAVAILABLE:
368 $text = __('Service unavailable', 'mailpoet');
369 break;
370 case Bridge::CHECK_ERROR_UNKNOWN:
371 $text = __('Contact your hosting support to check the connection between your host and https://bridge.mailpoet.com', 'mailpoet');
372 break;
373 default:
374 // translators: %s is the code.
375 $text = sprintf(_x('code: %s', 'Error code (inside parentheses)', 'mailpoet'), $code);
376 break;
377 }
378
379 return $text;
380 }
381
382 private function createBadRequest(string $message) {
383 return $this->badRequest([
384 APIError::BAD_REQUEST => $message,
385 ]);
386 }
387 }
388