> $recipients */ public function send(SmsCampaign $campaign, array $recipients): SmsSendResult { $payload = $this->payload($campaign, $recipients); $serviceName = $this->serviceName(); $path = '/sms/'.rawurlencode($serviceName).'/jobs'; // Keep the exact request even if the process loses OVH's response. if ($campaign->exists) { $campaign->update([ 'provider_service_name' => $serviceName, 'request_payload' => $payload, 'message' => $payload['message'], ]); } $response = $this->request('POST', $path, $payload); if (! isset($response['ids']) || ! is_array($response['ids']) || count($response['ids']) + count($this->stringList($response['invalidReceivers'] ?? [])) < count($recipients)) { throw new SmsProviderException('Reponse OVH incomplete : envoi a verifier.', $response, outcomeUnknown: true); } return new SmsSendResult( serviceName: $serviceName, providerJobId: $this->stringValue($response['id'] ?? $response['jobId'] ?? null), requestPayload: $payload, responsePayload: $response, providerMessageIds: $this->stringList($response['ids'] ?? []), failedNumbers: $this->stringList($response['invalidReceivers'] ?? []), ); } /** @return array> */ public function outgoing(SmsCampaign $campaign): array { if (! $campaign->provider_tag) { throw new SmsProviderException('Verification impossible sans reference de campagne OVH.'); } $path = '/sms/'.rawurlencode($campaign->provider_service_name ?: $this->serviceName()).'/outgoing'; $ids = $this->request('GET', $path.'?'.http_build_query(['tag' => $campaign->provider_tag])); $messages = []; foreach ($ids as $id) { $messages[] = $this->request('GET', $path.'/'.rawurlencode((string) $id)); } return $messages; } private function serviceName(): string { $configured = trim((string) config('relais_sms.ovh.sms_service_name', '')); if ($configured !== '') { return $configured; } $services = $this->request('GET', '/sms'); $service = $services[0] ?? null; if (! is_string($service) || $service === '') { throw new SmsProviderException('Aucun service SMS OVH disponible pour ces identifiants.', [ 'services' => $services, ]); } return $service; } /** * @param array> $recipients * @return array */ private function payload(SmsCampaign $campaign, array $recipients): array { $shortCode = (bool) config('relais_sms.ovh.sms_short_code'); $sender = trim((string) config('relais_sms.ovh.sms_sender', '')); if ($shortCode && $sender !== '') { throw new SmsProviderException('OVH_SMS_SHORT_CODE=true est incompatible avec OVH_SMS_SENDER.'); } if (! $shortCode && $sender === '') { throw new SmsProviderException('Configurer OVH_SMS_SENDER pour utiliser OVH_SMS_SHORT_CODE=false.'); } $message = $shortCode ? (new OvhShortCodeMessage)->prepare($campaign->message) : $campaign->message; if (trim($message) === '') { throw new SmsProviderException('Le SMS est vide apres retrait des liens interdits par OVH en numero court.'); } $payload = [ 'message' => $message, 'receivers' => array_values(array_map( fn (array $recipient): string => (string) $recipient['number'], $recipients, )), 'noStopClause' => (bool) config('relais_sms.ovh.sms_no_stop_clause'), 'tag' => $campaign->provider_tag, ]; if ($sender !== '') { $payload['sender'] = $sender; } elseif ($shortCode) { $payload['senderForResponse'] = true; } return $payload; } /** * @param array|null $payload * @return array|array */ private function request(string $method, string $path, ?array $payload = null): array { $body = $payload === null ? '' : json_encode($payload, JSON_THROW_ON_ERROR); $url = $this->baseUrl().$path; $timestamp = $this->timestamp(); try { $response = $this->client->request($method, $url, [ 'headers' => [ 'Content-Type' => 'application/json', 'X-Ovh-Application' => $this->requiredConfig('application_key'), 'X-Ovh-Consumer' => $this->requiredConfig('consumer_key'), 'X-Ovh-Signature' => $this->signature($method, $url, $body, $timestamp), 'X-Ovh-Timestamp' => (string) $timestamp, ], 'body' => $body, 'timeout' => $method === 'POST' ? (float) config('relais_sms.ovh.send_timeout', 60) : (float) config('relais_sms.ovh.timeout', 20), 'connect_timeout' => 10, 'allow_redirects' => false, ]); } catch (RequestException $exception) { $responsePayload = $this->decode((string) $exception->getResponse()?->getBody()); $message = $responsePayload['message'] ?? $exception->getMessage(); throw new SmsProviderException((string) $message, $responsePayload, outcomeUnknown: $method === 'POST' && ($exception->getResponse() === null || $exception->getResponse()->getStatusCode() >= 500)); } catch (GuzzleException $exception) { throw new SmsProviderException($exception->getMessage(), outcomeUnknown: $method === 'POST'); } return $this->decode((string) $response->getBody()); } private function timestamp(): int { if ($this->serverTimestamp !== null) { return $this->serverTimestamp + time() - $this->timestampFetchedAt; } try { $response = $this->client->request('GET', $this->baseUrl().'/auth/time', [ 'timeout' => (float) config('relais_sms.ovh.timeout', 20), ]); $this->timestampFetchedAt = time(); return $this->serverTimestamp = (int) trim((string) $response->getBody()); } catch (GuzzleException) { return time(); } } private function signature(string $method, string $url, string $body, int $timestamp): string { return '$1$'.sha1(implode('+', [ $this->requiredConfig('application_secret'), $this->requiredConfig('consumer_key'), strtoupper($method), $url, $body, (string) $timestamp, ])); } private function baseUrl(): string { return match ((string) config('relais_sms.ovh.endpoint', 'ovh-eu')) { 'ovh-ca' => 'https://ca.api.ovh.com/1.0', 'ovh-eu' => 'https://eu.api.ovh.com/1.0', default => throw new SmsProviderException('Endpoint OVH non pris en charge : utiliser ovh-eu ou ovh-ca.'), }; } private function requiredConfig(string $key): string { $value = trim((string) config('relais_sms.ovh.'.$key, '')); if ($value === '') { throw new SmsProviderException('Configuration OVH incomplete : '.$key.'.'); } return $value; } /** * @return array|array */ private function decode(string $body): array { if (trim($body) === '') { return []; } $decoded = json_decode($body, true); return is_array($decoded) ? $decoded : []; } private function stringValue(mixed $value): ?string { if ($value === null || $value === '') { return null; } return (string) $value; } /** * @return array */ private function stringList(mixed $values): array { if (! is_array($values)) { return []; } return array_values(array_map('strval', $values)); } }