Passed
Pull Request — master (#778)
by
unknown
08:24
created

CloudApiDriver::validateConfig()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 29
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 19
c 1
b 0
f 0
dl 0
loc 29
rs 9.3222
cc 5
nc 6
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7
 * SPDX-License-Identifier: AGPL-3.0-or-later
8
 */
9
10
namespace OCA\TwoFactorGateway\Provider\Channel\WhatsApp\Drivers;
11
12
use GuzzleHttp\Exception\ClientException;
0 ignored issues
show
Bug introduced by
The type GuzzleHttp\Exception\ClientException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
13
use GuzzleHttp\Exception\ConnectException;
0 ignored issues
show
Bug introduced by
The type GuzzleHttp\Exception\ConnectException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
14
use GuzzleHttp\Exception\RequestException;
0 ignored issues
show
Bug introduced by
The type GuzzleHttp\Exception\RequestException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use GuzzleHttp\Exception\ServerException;
0 ignored issues
show
Bug introduced by
The type GuzzleHttp\Exception\ServerException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use OCA\TwoFactorGateway\Exception\ConfigurationException;
17
use OCA\TwoFactorGateway\Exception\MessageTransmissionException;
18
use OCA\TwoFactorGateway\Provider\FieldDefinition;
19
use OCA\TwoFactorGateway\Provider\Settings;
20
use OCP\Http\Client\IClient;
21
use OCP\Http\Client\IClientService;
22
use OCP\IAppConfig;
23
use Psr\Log\LoggerInterface;
24
use Symfony\Component\Console\Input\InputInterface;
25
use Symfony\Component\Console\Output\OutputInterface;
26
use Symfony\Component\Console\Question\Question;
27
use Symfony\Component\Console\Helper\QuestionHelper;
28
29
/**
30
 * Driver para Meta/Facebook WhatsApp Cloud API
31
 * Usa a API oficial v14.0+ do Meta Graph para enviar mensagens
32
 */
33
class CloudApiDriver implements IWhatsAppDriver {
34
	private const API_VERSION = 'v14.0';
35
	private const API_BASE_URL = 'https://graph.facebook.com';
36
37
	private IClient $client;
38
	private ?Settings $cachedSettings = null;
39
40
	public function __construct(
41
		private IAppConfig $appConfig,
42
		private IClientService $clientService,
43
		private LoggerInterface $logger,
44
	) {
45
		$this->client = $this->clientService->newClient();
46
	}
47
48
	public function send(string $identifier, string $message, array $extra = []): void {
49
		$this->logger->debug("Sending WhatsApp Cloud API message to $identifier");
50
51
		try {
52
			$phoneNumberId = $this->getConfig('phone_number_id');
53
			$apiKey = $this->getConfig('api_key');
54
			$apiEndpoint = $this->getConfig('api_endpoint') ?? self::API_BASE_URL;
55
56
			if (!$phoneNumberId || !$apiKey) {
57
				throw new ConfigurationException('Missing required Cloud API configuration');
58
			}
59
60
			// Normaliza o número de telefone removendo caracteres especiais
61
			$phoneNumber = preg_replace('/\D/', '', $identifier);
62
			if (strlen($phoneNumber) < 10) {
63
				throw new MessageTransmissionException('Invalid phone number format');
64
			}
65
66
			$url = sprintf(
67
				'%s/%s/%s/messages',
68
				rtrim($apiEndpoint, '/'),
69
				self::API_VERSION,
70
				$phoneNumberId
71
			);
72
73
			$response = $this->client->post($url, [
74
				'headers' => [
75
					'Authorization' => "Bearer $apiKey",
76
					'Content-Type' => 'application/json',
77
				],
78
				'json' => [
79
					'messaging_product' => 'whatsapp',
80
					'recipient_type' => 'individual',
81
					'to' => $phoneNumber,
82
					'type' => 'text',
83
					'text' => [
84
						'body' => $message,
85
					],
86
				],
87
			]);
88
89
			$statusCode = $response->getStatusCode();
90
			$responseBody = json_decode((string)$response->getBody(), true);
91
92
			if ($statusCode >= 200 && $statusCode < 300) {
93
				$this->logger->debug('WhatsApp message sent successfully', [
94
					'phone' => $phoneNumber,
95
					'message_id' => $responseBody['messages'][0]['id'] ?? 'unknown',
96
				]);
97
			} else {
98
				throw new MessageTransmissionException(
99
					'Failed to send WhatsApp message: ' . ($responseBody['error']['message'] ?? 'Unknown error')
100
				);
101
			}
102
		} catch (ConnectException $e) {
103
			$this->logger->error('Connection error sending WhatsApp message', ['exception' => $e]);
104
			throw new MessageTransmissionException('Failed to connect to WhatsApp API');
105
		} catch (ClientException | ServerException $e) {
106
			$errorMsg = 'Unknown error';
107
			try {
108
				$body = json_decode((string)$e->getResponse()->getBody(), true);
109
				$errorMsg = $body['error']['message'] ?? $body['message'] ?? $errorMsg;
110
			} catch (\Exception) {
111
				// Use default error message
112
			}
113
			$this->logger->error('WhatsApp API error', [
114
				'status' => $e->getResponse()->getStatusCode() ?? 'unknown',
115
				'error' => $errorMsg,
116
			]);
117
			throw new MessageTransmissionException("WhatsApp API error: $errorMsg");
118
		} catch (RequestException $e) {
119
			$this->logger->error('Request error sending WhatsApp message', ['exception' => $e]);
120
			throw new MessageTransmissionException('Failed to send WhatsApp message');
121
		}
122
	}
123
124
	public function getSettings(): Settings {
125
		if ($this->cachedSettings !== null) {
126
			return $this->cachedSettings;
127
		}
128
129
		$this->cachedSettings = new Settings(
130
			name: 'WhatsApp Cloud API (Meta)',
131
			allowMarkdown: true,
132
			fields: [
133
				new FieldDefinition(
134
					field: 'phone_number_id',
135
					prompt: 'Phone Number ID from Meta Business Account:',
136
				),
137
				new FieldDefinition(
138
					field: 'business_account_id',
139
					prompt: 'Business Account ID:',
140
				),
141
				new FieldDefinition(
142
					field: 'api_key',
143
					prompt: 'API Access Token (v14.0+):',
144
				),
145
				new FieldDefinition(
146
					field: 'api_endpoint',
147
					prompt: 'API Endpoint (optional, default: https://graph.facebook.com):',
148
				),
149
			],
150
		);
151
152
		return $this->cachedSettings;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->cachedSettings returns the type null which is incompatible with the type-hinted return OCA\TwoFactorGateway\Provider\Settings.
Loading history...
153
	}
154
155
	public function validateConfig(): void {
156
		$phoneNumberId = $this->getConfig('phone_number_id');
157
		$apiKey = $this->getConfig('api_key');
158
		$apiEndpoint = $this->getConfig('api_endpoint') ?? self::API_BASE_URL;
159
160
		if (!$phoneNumberId || !$apiKey) {
161
			throw new ConfigurationException('Missing required Cloud API configuration');
162
		}
163
164
		try {
165
			$url = sprintf(
166
				'%s/%s/%s',
167
				rtrim($apiEndpoint, '/'),
168
				self::API_VERSION,
169
				$phoneNumberId
170
			);
171
172
			$response = $this->client->get($url, [
173
				'headers' => [
174
					'Authorization' => "Bearer $apiKey",
175
				],
176
			]);
177
178
			if ($response->getStatusCode() !== 200) {
179
				throw new ConfigurationException('Failed to validate Cloud API credentials');
180
			}
181
		} catch (RequestException $e) {
182
			$this->logger->error('Failed to validate Cloud API config', ['exception' => $e]);
183
			throw new ConfigurationException('Invalid Cloud API credentials or endpoint');
184
		}
185
	}
186
187
	public function isConfigComplete(): bool {
188
		return (bool)($this->getConfig('phone_number_id') && $this->getConfig('api_key'));
189
	}
190
191
	public function cliConfigure(InputInterface $input, OutputInterface $output): int {
192
		$helper = new QuestionHelper();
193
		$settings = $this->getSettings();
194
195
		try {
196
			foreach ($settings->fields as $field) {
197
				$question = new Question($field->prompt . ' ');
198
				$value = $helper->ask($input, $output, $question);
199
200
				if (!$value) {
201
					$output->writeln("<error>Field '{$field->field}' is required</error>");
202
					return 1;
203
				}
204
205
				$this->setConfig($field->field, $value);
206
			}
207
208
			// Valida a configuração
209
			$this->validateConfig();
210
			$output->writeln('<info>WhatsApp Cloud API configuration validated successfully!</info>');
211
212
			return 0;
213
		} catch (\Exception $e) {
214
			$output->writeln("<error>Configuration failed: {$e->getMessage()}</error>");
215
			return 1;
216
		}
217
	}
218
219
	public static function detectDriver(array $storedConfig): ?string {
220
		// Este driver é detectado quando temos api_key (indicando Cloud API)
221
		if (!empty($storedConfig['api_key'])) {
222
			return self::class;
223
		}
224
		return null;
225
	}
226
227
	private function getConfig(string $key): ?string {
228
		$value = $this->appConfig->getValueString('twofactor_gateway', "whatsapp_cloud_$key", '');
229
		return $value ?: null;
230
	}
231
232
	private function setConfig(string $key, string $value): void {
233
		$this->appConfig->setValueString('twofactor_gateway', "whatsapp_cloud_$key", $value);
234
	}
235
}
236