257 lines
8.7 KiB
PHP
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));
|
|
}
|
|
}
|