Files
RelaisSMS/app/Services/Sms/OvhSmsProvider.php
T

257 lines
8.7 KiB
PHP

<?php
namespace App\Services\Sms;
use App\Contracts\Sms\SmsProviderInterface;
use App\Models\SmsCampaign;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
class OvhSmsProvider implements SmsProviderInterface
{
private ?int $serverTimestamp = null;
private int $timestampFetchedAt = 0;
public function __construct(private readonly ClientInterface $client) {}
/**
* @param array<int, array<string, mixed>> $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<int, array<string, mixed>> */
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<int, array<string, mixed>> $recipients
* @return array<string, mixed>
*/
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<string, mixed>|null $payload
* @return array<string, mixed>|array<int, mixed>
*/
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<string, mixed>|array<int, mixed>
*/
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<int, string>
*/
private function stringList(mixed $values): array
{
if (! is_array($values)) {
return [];
}
return array_values(array_map('strval', $values));
}
}