Publication initiale de RelaisSMS : documentation et sources nettoyees
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user