Gateway::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 2
c 1
b 0
f 1
dl 0
loc 7
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 3
crap 1
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\GoWhatsApp;
11
12
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...
13
use OCA\TwoFactorGateway\Exception\MessageTransmissionException;
14
use OCA\TwoFactorGateway\Provider\FieldDefinition;
15
use OCA\TwoFactorGateway\Provider\Gateway\AGateway;
16
use OCA\TwoFactorGateway\Provider\Settings;
17
use OCP\Http\Client\IClient;
18
use OCP\Http\Client\IClientService;
19
use OCP\IAppConfig;
20
use Psr\Log\LoggerInterface;
21
use Symfony\Component\Console\Helper\QuestionHelper;
22
use Symfony\Component\Console\Input\InputInterface;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Question\Question;
25
26
/**
27
 * @method string getBaseUrl()
28
 * @method static setBaseUrl(string $baseUrl)
29
 * @method string getPhone()
30
 * @method static setPhone(string $phone)
31
 * @method string getUsername()
32
 * @method static setUsername(string $username)
33
 * @method string getPassword()
34
 * @method static setPassword(string $password)
35
 */
36
class Gateway extends AGateway {
37
	private IClient $client;
38
	private string $lazyBaseUrl = '';
39
	private string $lazyPhone = '';
40
	private string $lazyUsername = '';
41
	private string $lazyPassword = '';
42
43 1
	public function __construct(
44
		public IAppConfig $appConfig,
45
		private IClientService $clientService,
46
		private LoggerInterface $logger,
47
	) {
48 1
		parent::__construct($appConfig);
49 1
		$this->client = $this->clientService->newClient();
50
	}
51
52 1
	#[\Override]
53
	public function createSettings(): Settings {
54 1
		return new Settings(
55 1
			name: 'GoWhatsApp',
56 1
			allowMarkdown: true,
57 1
			fields: [
58 1
				new FieldDefinition(
59 1
					field: 'base_url',
60 1
					prompt: 'Base URL to your WhatsApp API endpoint:',
61 1
				),
62 1
				new FieldDefinition(
63 1
					field: 'username',
64 1
					prompt: 'API Username:',
65 1
				),
66 1
				new FieldDefinition(
67 1
					field: 'password',
68 1
					prompt: 'API Password:',
69 1
				),
70 1
				new FieldDefinition(
71 1
					field: 'phone',
72 1
					prompt: 'Phone number for WhatsApp Web access:',
73 1
				),
74 1
			],
75 1
		);
76
	}
77
78
	#[\Override]
79
	public function send(string $identifier, string $message, array $extra = []): void {
80
		$this->logger->debug("sending whatsapp message to $identifier, message: $message");
81
82
		$isOnWhatsApp = $this->checkUserOnWhatsApp($identifier);
83
		if (!$isOnWhatsApp) {
84
			throw new MessageTransmissionException('The phone number is not registered on WhatsApp.');
85
		}
86
87
		$phone = $this->formatPhoneNumber($identifier);
88
89
		try {
90
			$response = $this->client->post($this->getBaseUrl() . '/send/message', [
91
				'json' => [
92
					'phone' => $phone,
93
					'message' => $message,
94
				],
95
				'auth' => $this->getBasicAuth(),
96
			]);
97
98
			$body = (string)$response->getBody();
99
			$data = json_decode($body, true);
100
101
			if (($data['code'] ?? '') !== 'SUCCESS') {
102
				throw new MessageTransmissionException($data['message'] ?? 'Failed to send message');
103
			}
104
105
			$this->logger->debug("whatsapp message to $identifier sent successfully", [
106
				'message_id' => $data['results']['message_id'] ?? null,
107
			]);
108
		} catch (\Exception $e) {
109
			$this->logger->error('Could not send WhatsApp message', [
110
				'identifier' => $identifier,
111
				'exception' => $e,
112
			]);
113
			throw new MessageTransmissionException('Failed to send WhatsApp message: ' . $e->getMessage());
114
		}
115
	}
116
117
	#[\Override]
118
	public function cliConfigure(InputInterface $input, OutputInterface $output): int {
119
		$helper = new QuestionHelper();
120
121
		$baseUrlQuestion = new Question($this->getSettings()->fields[0]->prompt . ' ');
122
		$this->lazyBaseUrl = $helper->ask($input, $output, $baseUrlQuestion);
123
		$this->lazyBaseUrl = rtrim($this->lazyBaseUrl, '/');
124
125
		$usernameQuestion = new Question($this->getSettings()->fields[1]->prompt . ' ');
126
		$this->lazyUsername = $helper->ask($input, $output, $usernameQuestion);
127
128
		$passwordQuestion = new Question($this->getSettings()->fields[2]->prompt . ' ');
129
		$passwordQuestion->setHidden(true);
130
		$passwordQuestion->setHiddenFallback(false);
131
		$this->lazyPassword = $helper->ask($input, $output, $passwordQuestion);
132
133
		$phoneQuestion = new Question($this->getSettings()->fields[3]->prompt . ' ');
134
		$this->lazyPhone = $helper->ask($input, $output, $phoneQuestion);
135
		$this->lazyPhone = preg_replace('/\D/', '', $this->lazyPhone);
136
137
		try {
138
			$output->writeln('<info>Requesting pairing code...</info>');
139
			$response = $this->client->get($this->getBaseUrl() . '/app/login-with-code', [
140
				'query' => ['phone' => $this->lazyPhone],
141
			]);
142
143
			$body = (string)$response->getBody();
144
			$data = json_decode($body, true);
145
146
			if (($data['code'] ?? '') !== 'SUCCESS') {
147
				$output->writeln('<error>' . ($data['message'] ?? 'Failed to get pairing code') . '</error>');
148
				return 1;
149
			}
150
151
			$pairCode = $data['results']['pair_code'] ?? '';
152
			if (empty($pairCode)) {
153
				$output->writeln('<error>No pairing code received</error>');
154
				return 1;
155
			}
156
157
			$output->writeln('');
158
			$output->writeln('<info>════════════════════════════════════</info>');
159
			$output->writeln('<info>    PAIRING CODE: ' . $pairCode . '</info>');
160
			$output->writeln('<info>════════════════════════════════════</info>');
161
			$output->writeln('');
162
			$output->writeln('Open WhatsApp on your phone and enter this code:');
163
			$output->writeln('1. Open WhatsApp');
164
			$output->writeln('2. Tap Menu or Settings');
165
			$output->writeln('3. Tap Linked Devices');
166
			$output->writeln('4. Tap Link a Device');
167
			$output->writeln('5. Select "Link with phone number instead"');
168
			$output->writeln('6. Enter the code: <comment>' . $pairCode . '</comment>');
169
			$output->writeln('');
170
171
			$output->writeln('<info>Waiting for confirmation...</info>');
172
			$maxAttempts = 60;
173
			$attempt = 0;
174
175
			while ($attempt < $maxAttempts) {
176
				sleep(2);
177
178
				try {
179
					$userInfoResponse = $this->client->get($this->getBaseUrl() . '/user/info', [
180
						'query' => ['phone' => $this->lazyPhone . '@s.whatsapp.net'],
181
					]);
182
183
					$userInfoBody = (string)$userInfoResponse->getBody();
184
					$userInfoData = json_decode($userInfoBody, true);
185
186
					if (($userInfoData['code'] ?? '') === 'SUCCESS') {
187
						$output->writeln('');
188
						$output->writeln('<info>✓ Successfully connected to WhatsApp!</info>');
189
						$output->writeln('');
190
191
						$results = $userInfoData['results'] ?? [];
192
						if (!empty($results['verified_name'])) {
193
							$output->writeln('<comment>Verified Name:</comment> ' . $results['verified_name']);
194
						}
195
						if (!empty($results['status'])) {
196
							$output->writeln('<comment>Status:</comment> ' . $results['status']);
197
						}
198
						if (!empty($results['devices']) && is_array($results['devices'])) {
199
							$deviceCount = count($results['devices']);
200
							$output->writeln('<comment>Connected Devices:</comment> ' . $deviceCount);
201
						}
202
						$output->writeln('');
203
204
						$this->setBaseUrl($this->lazyBaseUrl);
0 ignored issues
show
Bug introduced by
It seems like $this->lazyBaseUrl can also be of type OCA\TwoFactorGateway\Provider\Gateway\AGateway; however, parameter $baseUrl of OCA\TwoFactorGateway\Pro...p\Gateway::setBaseUrl() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

204
						$this->setBaseUrl(/** @scrutinizer ignore-type */ $this->lazyBaseUrl);
Loading history...
205
						$this->setUsername($this->lazyUsername);
206
						$this->setPassword($this->lazyPassword);
207
						$this->setPhone($this->lazyPhone);
208
						return 0;
209
					}
210
				} catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
211
				}
212
213
				$attempt++;
214
				if ($attempt % 5 === 0) {
215
					$output->write('.');
216
				}
217
			}
218
219
			$output->writeln('');
220
			$output->writeln('<error>Timeout waiting for WhatsApp confirmation</error>');
221
			return 1;
222
223
		} catch (RequestException $e) {
224
			$output->writeln('<error>Could not connect to the WhatsApp API. Please check the URL.</error>');
225
			$this->logger->error('API connection error', ['exception' => $e]);
226
			return 1;
227
		} catch (\Exception $e) {
228
			$output->writeln('<error>' . $e->getMessage() . '</error>');
229
			$this->logger->error('Configuration error', ['exception' => $e]);
230
			return 1;
231
		}
232
	}
233
234
	private function formatPhoneNumber(string $phoneNumber): string {
235
		$phone = preg_replace('/\D/', '', $phoneNumber);
236
		return $phone . '@s.whatsapp.net';
237
	}
238
239
	private function checkUserOnWhatsApp(string $phoneNumber): bool {
240
		try {
241
			$phone = preg_replace('/\D/', '', $phoneNumber);
242
			$response = $this->client->get($this->getBaseUrl() . '/user/check', [
243
				'query' => ['phone' => $phone],
244
				'auth' => $this->getBasicAuth(),
245
			]);
246
247
			$body = (string)$response->getBody();
248
			$data = json_decode($body, true);
249
250
			return ($data['code'] ?? '') === 'SUCCESS'
251
				&& ($data['results']['is_on_whatsapp'] ?? false) === true;
252
		} catch (\Exception $e) {
253
			$this->logger->error('Error checking if user is on WhatsApp', [
254
				'phone' => $phoneNumber,
255
				'exception' => $e,
256
			]);
257
			return false;
258
		}
259
	}
260
261
	private function getBasicAuth(): array {
262
		try {
263
			$username = $this->lazyUsername ?: $this->getUsername();
264
			$password = $this->lazyPassword ?: $this->getPassword();
265
			return [$username, $password];
266
		} catch (\Exception $e) {
267
			return ['', ''];
268
		}
269
	}
270
271
	private function getBaseUrl(): string {
272
		if ($this->lazyBaseUrl !== '') {
273
			return $this->lazyBaseUrl;
274
		}
275
		/** @var string */
276
		$this->lazyBaseUrl = parent::__call('getBaseUrl', []);
0 ignored issues
show
Bug introduced by
The property lazyBaseUrl does not exist on string.
Loading history...
277
		return $this->lazyBaseUrl;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->lazyBaseUrl returns the type OCA\TwoFactorGateway\Provider\Gateway\AGateway which is incompatible with the type-hinted return string.
Loading history...
278
	}
279
}
280