You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
188 lines
6.5 KiB
188 lines
6.5 KiB
<?php
|
|
|
|
namespace App\Services\Sms;
|
|
|
|
use App\Models\SmsVerification;
|
|
use App\Support\SmsConfig;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
class TencentCloudSmsSender implements SmsSender
|
|
{
|
|
private const ACTION = 'SendSms';
|
|
private const VERSION = '2021-01-11';
|
|
private const SERVICE = 'sms';
|
|
private const ALGORITHM = 'TC3-HMAC-SHA256';
|
|
|
|
public function sendVerification(SmsVerification $record): SmsSendResult
|
|
{
|
|
try {
|
|
SmsConfig::assertReadyForRealSending();
|
|
} catch (\Throwable $e) {
|
|
return SmsSendResult::failure(
|
|
SmsVerification::PROVIDER_TENCENTCLOUD,
|
|
null,
|
|
'CONFIG_INCOMPLETE',
|
|
$e->getMessage(),
|
|
['error' => 'config_incomplete']
|
|
);
|
|
}
|
|
|
|
$endpoint = (string) config('sms.tencentcloud.endpoint');
|
|
$region = (string) config('sms.tencentcloud.region');
|
|
$secretId = (string) config('sms.tencentcloud.secret_id');
|
|
$secretKey = (string) config('sms.tencentcloud.secret_key');
|
|
$timeout = (int) config('sms.timeout', 5);
|
|
|
|
$payload = [
|
|
'PhoneNumberSet' => ['+86'.$record->mobile],
|
|
'SmsSdkAppId' => (string) config('sms.tencentcloud.sdk_app_id'),
|
|
'SignName' => (string) config('sms.tencentcloud.sign_name'),
|
|
'TemplateId' => (string) config('sms.tencentcloud.template_id'),
|
|
'TemplateParamSet' => [
|
|
(string) $record->code,
|
|
(string) data_get($record->request_payload_json, 'ttl_minutes', '5'),
|
|
],
|
|
];
|
|
|
|
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
if ($body === false) {
|
|
return SmsSendResult::failure(
|
|
SmsVerification::PROVIDER_TENCENTCLOUD,
|
|
null,
|
|
'PAYLOAD_ENCODE_FAILED',
|
|
'Tencent Cloud SMS payload encode failed.',
|
|
['error' => 'payload_encode_failed']
|
|
);
|
|
}
|
|
|
|
$timestamp = time();
|
|
$headers = $this->buildHeaders($endpoint, $region, $secretId, $secretKey, $timestamp, $body);
|
|
|
|
try {
|
|
$response = Http::timeout($timeout)
|
|
->withHeaders($headers)
|
|
->withBody($body, 'application/json; charset=utf-8')
|
|
->post('https://'.$endpoint);
|
|
} catch (\Throwable $e) {
|
|
return SmsSendResult::failure(
|
|
SmsVerification::PROVIDER_TENCENTCLOUD,
|
|
null,
|
|
'NETWORK_ERROR',
|
|
$e->getMessage(),
|
|
['error' => 'network_error']
|
|
);
|
|
}
|
|
|
|
$json = $response->json();
|
|
if (! is_array($json)) {
|
|
return SmsSendResult::failure(
|
|
SmsVerification::PROVIDER_TENCENTCLOUD,
|
|
null,
|
|
'INVALID_RESPONSE',
|
|
'Tencent Cloud SMS returned a non-json response.',
|
|
['http_status' => $response->status()]
|
|
);
|
|
}
|
|
|
|
$tencentResponse = data_get($json, 'Response', []);
|
|
$requestId = data_get($tencentResponse, 'RequestId');
|
|
|
|
if (data_get($tencentResponse, 'Error.Code')) {
|
|
return SmsSendResult::failure(
|
|
SmsVerification::PROVIDER_TENCENTCLOUD,
|
|
is_string($requestId) ? $requestId : null,
|
|
(string) data_get($tencentResponse, 'Error.Code'),
|
|
(string) data_get($tencentResponse, 'Error.Message'),
|
|
$this->responseSummary($json)
|
|
);
|
|
}
|
|
|
|
$status = data_get($tencentResponse, 'SendStatusSet.0', []);
|
|
$providerCode = (string) data_get($status, 'Code', '');
|
|
$providerMessage = (string) data_get($status, 'Message', '');
|
|
|
|
if ($response->successful() && $providerCode === 'Ok') {
|
|
return SmsSendResult::success(
|
|
SmsVerification::PROVIDER_TENCENTCLOUD,
|
|
is_string($requestId) ? $requestId : null,
|
|
$providerCode,
|
|
$providerMessage,
|
|
$this->responseSummary($json)
|
|
);
|
|
}
|
|
|
|
return SmsSendResult::failure(
|
|
SmsVerification::PROVIDER_TENCENTCLOUD,
|
|
is_string($requestId) ? $requestId : null,
|
|
$providerCode !== '' ? $providerCode : 'HTTP_'.$response->status(),
|
|
$providerMessage !== '' ? $providerMessage : 'Tencent Cloud SMS send failed.',
|
|
$this->responseSummary($json)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function buildHeaders(
|
|
string $host,
|
|
string $region,
|
|
string $secretId,
|
|
string $secretKey,
|
|
int $timestamp,
|
|
string $payload,
|
|
): array {
|
|
$date = gmdate('Y-m-d', $timestamp);
|
|
$canonicalHeaders = "content-type:application/json; charset=utf-8\nhost:{$host}\n";
|
|
$signedHeaders = 'content-type;host';
|
|
$canonicalRequest = implode("\n", [
|
|
'POST',
|
|
'/',
|
|
'',
|
|
$canonicalHeaders,
|
|
$signedHeaders,
|
|
hash('sha256', $payload),
|
|
]);
|
|
|
|
$credentialScope = $date.'/'.self::SERVICE.'/tc3_request';
|
|
$stringToSign = implode("\n", [
|
|
self::ALGORITHM,
|
|
(string) $timestamp,
|
|
$credentialScope,
|
|
hash('sha256', $canonicalRequest),
|
|
]);
|
|
|
|
$secretDate = hash_hmac('sha256', $date, 'TC3'.$secretKey, true);
|
|
$secretService = hash_hmac('sha256', self::SERVICE, $secretDate, true);
|
|
$secretSigning = hash_hmac('sha256', 'tc3_request', $secretService, true);
|
|
$signature = hash_hmac('sha256', $stringToSign, $secretSigning);
|
|
|
|
$authorization = self::ALGORITHM
|
|
.' Credential='.$secretId.'/'.$credentialScope
|
|
.', SignedHeaders='.$signedHeaders
|
|
.', Signature='.$signature;
|
|
|
|
return [
|
|
'Authorization' => $authorization,
|
|
'Content-Type' => 'application/json; charset=utf-8',
|
|
'Host' => $host,
|
|
'X-TC-Action' => self::ACTION,
|
|
'X-TC-Region' => $region,
|
|
'X-TC-Timestamp' => (string) $timestamp,
|
|
'X-TC-Version' => self::VERSION,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $response
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function responseSummary(array $response): array
|
|
{
|
|
return [
|
|
'request_id' => data_get($response, 'Response.RequestId'),
|
|
'error' => data_get($response, 'Response.Error'),
|
|
'send_status_set' => data_get($response, 'Response.SendStatusSet'),
|
|
];
|
|
}
|
|
}
|