Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* 🌉 **Sync with other chat solutions** With [Matterbridge](https://github.com/42wim/matterbridge/) being integrated in Talk, you can easily sync a lot of other chat solutions to Nextcloud Talk and vice-versa.
]]></description>

<version>26.0.0-dev.15</version>
<version>26.0.0-dev.16</version>
<licence>AGPL-3.0-or-later</licence>

<author>Christian Lorang</author>
Expand Down
2 changes: 1 addition & 1 deletion docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Legend:
| `sip_bridge_dialin_info` | string | | Yes | 🖌️ | Additional information added in the SIP dial-in invitation mail and sidebar |
| `sip_bridge_dialout_anonymous` | bool | `false` | No | | When set to true, all out-going SIP calls will be anonymous calls (CLIR) |
| `sip_bridge_dialout_number` | string | | No | | Fallback phone number that is used as caller for out-going calls when the user has no phone-number configured (has to be E164 `+49123456789` formatted) |
| `sip_bridge_dialout_prefix` | string | `+` | No | | Prefix that is added before the first configured phone-number of a user to make it a valid E164 number to be used as a caller for out-going calls |
| `sip_bridge_dialout_prefix` | string | `+` | No | | Deprecated: Only used for direct dial-in from older SIP bridges, which send phone numbers without this prefix |
| `sip_bridge_shared_secret` | string | | Yes | 🖌️ | Shared secret allowing the SIP bridge to authenticate on the Nextcloud server |
| `signaling_ticket_secret` | string | | Yes | | Secret used to secure the signaling tickets for guests (255 character random string) |
| `signaling_token_alg` | string<br>`ES256`, `ES384`, `RS256`, `RS384`, `RS512` or `EdDSA` | `ES256` | Yes | | Algorithm for the signaling tickets |
Expand Down
9 changes: 8 additions & 1 deletion lib/Command/PhoneNumber/AddPhoneNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}
$userId = $user->getUID();

$phoneNumber = $this->phoneNumberValidation->cleanNumber($phoneNumber);

if ($phoneNumber === '') {
$output->writeln('<error>Phone number is invalid.</error>');
return self::FAILURE;
}

try {
$phoneNumber = $this->phoneNumberValidation->validateNumber($phoneNumber);
$this->phoneNumberValidation->validateNumber($phoneNumber);
} catch (\InvalidArgumentException) {
$output->writeln('<error>Not a valid phone number ' . $phoneNumber . '. The format is invalid.</error>');
return self::FAILURE;
Expand Down
4 changes: 4 additions & 0 deletions lib/Command/PhoneNumber/FindPhoneNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use OC\Core\Command\Base;
use OCA\Talk\Model\PhoneNumberMapper;
use OCA\Talk\Service\PhoneNumberValidation;
use OCP\AppFramework\Db\DoesNotExistException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
Expand All @@ -19,6 +20,7 @@ class FindPhoneNumber extends Base {

public function __construct(
private readonly PhoneNumberMapper $mapper,
private readonly PhoneNumberValidation $phoneNumberValidation,
) {
parent::__construct();
}
Expand Down Expand Up @@ -48,6 +50,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$userId = (string)$input->getOption('user');

if ($phoneNumber !== '') {
$phoneNumber = $this->phoneNumberValidation->cleanNumber($phoneNumber);

try {
$entry = $this->mapper->findByPhoneNumber($phoneNumber);
} catch (DoesNotExistException) {
Expand Down
4 changes: 3 additions & 1 deletion lib/Command/PhoneNumber/ImportPhoneNumbers.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
continue;
}

$row[0] = $this->phoneNumberValidation->cleanNumber($row[0]);

try {
$row[0] = $this->phoneNumberValidation->validateNumber($row[0]);
$this->phoneNumberValidation->validateNumber($row[0]);
} catch (\InvalidArgumentException) {
$output->writeln('<error>Not a valid phone number ' . $row[0] . '. The format is invalid.</error>');
return self::FAILURE;
Expand Down
10 changes: 9 additions & 1 deletion lib/Command/PhoneNumber/RemovePhoneNumber.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use OC\Core\Command\Base;
use OCA\Talk\Model\PhoneNumberMapper;
use OCA\Talk\Service\PhoneNumberValidation;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
Expand All @@ -18,6 +19,7 @@ class RemovePhoneNumber extends Base {

public function __construct(
private readonly PhoneNumberMapper $mapper,
private readonly PhoneNumberValidation $phoneNumberValidation,
) {
parent::__construct();
}
Expand All @@ -36,7 +38,13 @@ protected function configure(): void {
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$phoneNumber = $input->getArgument('phone');
$phoneNumber = $this->phoneNumberValidation->cleanNumber($input->getArgument('phone'));

if ($phoneNumber === '') {
$output->writeln('<error>Phone number is invalid.</error>');
return self::FAILURE;
}

$this->mapper->deleteByPhoneNumber($phoneNumber);
return self::SUCCESS;
}
Expand Down
6 changes: 2 additions & 4 deletions lib/Controller/CallController.php
Original file line number Diff line number Diff line change
Expand Up @@ -454,14 +454,12 @@ public function sipDialOut(int $attendeeId): DataResponse {
}

// No elseif, so we have the fallback to sip_bridge_dialout_number when the caller is no user or doesn't have a number
if ($callerNumber !== false && $this->appConfig->getAppValueString('sip_bridge_dialout_prefix', '+') !== '') {
if ($callerNumber !== false) {
$attendee = $this->participant->getAttendee();
if ($attendee->getActorType() === Attendee::ACTOR_USERS) {
$numbers = $this->phoneNumberMapper->findByUser($attendee->getActorId());
if (!empty($numbers)) {
$number = array_shift($numbers);
$callerNumber = $this->appConfig->getAppValueString('sip_bridge_dialout_prefix', '+');
$callerNumber .= $number->getPhoneNumber();
$callerNumber = array_shift($numbers)->getPhoneNumber();
}
}
}
Expand Down
29 changes: 25 additions & 4 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2663,12 +2663,26 @@ public function directDialIn(string $phoneNumber, string $caller): DataResponse
try {
$entity = $this->phoneService->getAccountToCallForPhoneNumber($phoneNumber);
} catch (DoesNotExistException) {
$this->logger->info('No account found for direct dial-in with number: ' . $phoneNumber);
return new DataResponse(null, Http::STATUS_NOT_FOUND);
$dialoutPrefix = $this->appConfig->getAppValueString('sip_bridge_dialout_prefix', '+');

if ($dialoutPrefix === '') {
$this->logger->info('No account found for direct dial-in with number: ' . $phoneNumber);
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}

try {
// Try to find the number with added dialout prefix as a fallback for old sip-bridges
// TODO: Log as error in 26, remove fallback and dialout prefix with 27
$entity = $this->phoneService->getAccountToCallForPhoneNumber($dialoutPrefix . $phoneNumber);
} catch (DoesNotExistException) {
$this->logger->info('No account found for direct dial-in with number: ' . $phoneNumber);
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}

$this->logger->warning('Found account for direct dial-in only by adding the dial-out prefix. Please adjust the mapping for number: ' . $phoneNumber);
}

$caller = trim($caller);
// TODO: Use later and get name from addressbook? $cleanedCaller = $this->phoneNumberUtil->convertToStandardFormat($caller);
$user = $this->userManager->get($entity->getActorId());
try {
$room = $this->roomService->createConversation(
Expand All @@ -2684,7 +2698,14 @@ public function directDialIn(string $phoneNumber, string $caller): DataResponse
return new DataResponse(null, Http::STATUS_INTERNAL_SERVER_ERROR);
}

$participant = $this->participantService->joinRoomAsNewGuest($this->roomService, $room, '', true, displayName: $caller);
$participant = $this->participantService->joinRoomAsNewGuest(
$this->roomService,
$room,
'',
true,
displayName: $caller,
phoneNumber: $caller,
);
return new DataResponse($this->formatRoom($room, $participant));
}

Expand Down
79 changes: 79 additions & 0 deletions lib/Migration/Version23001Date20260918142246.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Talk\Migration;

use Closure;
use OCP\AppFramework\Services\IAppConfig;
use OCP\DB\Exception;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Override;

class Version23001Date20260918142246 extends SimpleMigrationStep {

public function __construct(
private readonly IDBConnection $connection,
private readonly IAppConfig $appConfig,
) {
}

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
#[Override]
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
$dialoutPrefix = $this->appConfig->getAppValueString('sip_bridge_dialout_prefix', '+');

if ($dialoutPrefix === '') {
return;
}

$query = $this->connection->getQueryBuilder();
$query->select('id', 'phone_number')
->from('talk_phone_numbers');

$phoneNumbers = [];
$result = $query->executeQuery();
while ($row = $result->fetchAssociative()) {
$phoneNumbers[(int)$row['id']] = (string)$row['phone_number'];
}
$result->closeCursor();

$update = $this->connection->getQueryBuilder();
$update->update('talk_phone_numbers')
->set('phone_number', $update->createParameter('phone_number'))
->where($update->expr()->eq('id', $update->createParameter('id')));

foreach ($phoneNumbers as $id => $phoneNumber) {
if (strlen($phoneNumber) < 6
// Numbers added before the leading + was stripped already have it
|| str_starts_with($phoneNumber, '+')
|| str_starts_with($phoneNumber, $dialoutPrefix)) {
continue;
}

$update->setParameter('phone_number', $dialoutPrefix . $phoneNumber)
->setParameter('id', $id, IQueryBuilder::PARAM_INT);
try {
$update->executeStatement();
} catch (Exception $e) {
if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
throw $e;
}
$output->warning('Phone number ' . $phoneNumber . ' was not migrated, because ' . $dialoutPrefix . $phoneNumber . ' is already assigned');
}
}
}
}
6 changes: 5 additions & 1 deletion lib/Service/ParticipantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ public function joinRoomAsFederatedUser(Room $room, string $actorType, string $a
* @throws InvalidPasswordException
* @throws UnauthorizedException
*/
public function joinRoomAsNewGuest(RoomService $roomService, Room $room, string $password, bool $passedPasswordProtection = false, ?Participant $previousParticipant = null, ?string $displayName = null): Participant {
public function joinRoomAsNewGuest(RoomService $roomService, Room $room, string $password, bool $passedPasswordProtection = false, ?Participant $previousParticipant = null, ?string $displayName = null, ?string $phoneNumber = null): Participant {
$event = new BeforeGuestJoinedRoomEvent($room, $password, $passedPasswordProtection);
$this->dispatcher->dispatchTyped($event);

Expand Down Expand Up @@ -655,6 +655,10 @@ public function joinRoomAsNewGuest(RoomService $roomService, Room $room, string
if ($displayName !== null && $displayName !== '') {
$attendee->setDisplayName($displayName);
}
if ($phoneNumber !== null && $phoneNumber !== '') {
$attendee->setPhoneNumber($phoneNumber);
$attendee->setActorType(Attendee::ACTOR_PHONES);
}

$this->attendeeMapper->insert($attendee);

Expand Down
22 changes: 11 additions & 11 deletions lib/Service/PhoneNumberValidation.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,17 @@ public function __construct(
* Validate input as a phone number
*
* - Local number: allow
* - International number
* a. If valid, strip + and allow
* b. If invalid, throw
* - International number: If invalid, throw
* @throws \InvalidArgumentException When the number is invalid
*/
public function validateNumber(string $phoneNumber): string {

public function validateNumber(string $phoneNumber): void {
if (
// Not an internation number
!str_starts_with($phoneNumber, '0')
// And matches a local number or dial-through
&& preg_match('/^[0-9]{1,20}$/', $phoneNumber)
) {
return $phoneNumber;
return;
}

$defaultRegion = $this->config->getSystemValueString('default_phone_region') ?: null;
Expand All @@ -45,11 +42,14 @@ public function validateNumber(string $phoneNumber): string {
if ($standardPhoneNumber === null) {
throw new \InvalidArgumentException();
}
}

if (str_starts_with($standardPhoneNumber, '+')) {
return substr($standardPhoneNumber, 1);
}

throw new \InvalidArgumentException();
/**
* Removes any character that is not a number or "+"
*
* @param string $phoneNumber Phonenumber to clean
*/
public function cleanNumber(string $phoneNumber): string {
return preg_replace('/[^0-9+]/', '', $phoneNumber) ?? '';
}
}
26 changes: 0 additions & 26 deletions lib/SetupCheck/SIPConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

use OCA\Talk\Config;
use OCP\AppFramework\Services\IAppConfig;
use OCP\IDBConnection;
use OCP\IL10N;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
Expand All @@ -19,7 +18,6 @@ class SIPConfiguration implements ISetupCheck {
public function __construct(
private readonly IAppConfig $appConfig,
private readonly Config $talkConfig,
private readonly IDBConnection $connection,
private readonly IL10N $l,
) {
}
Expand All @@ -44,30 +42,6 @@ public function run(): SetupResult {
return SetupResult::success($this->l->t('Using the SIP functionality requires a High-performance backend.'));
}

$query = $this->connection->getQueryBuilder();
$query->select('phone_number')
->from('talk_phone_numbers')
->where($query->expr()->like('phone_number', $query->createNamedParameter(
$this->connection->escapeLikeParameter('+') . '%'
)))
->orWhere($query->expr()->like('phone_number', $query->createNamedParameter(
$this->connection->escapeLikeParameter('0') . '%'
)));

$result = $query->executeQuery();
$invalidNumbers = $result->fetchFirstColumn();
$result->closeCursor();

if (!empty($invalidNumbers)) {
$message = $this->l->t("Assigned Talk phone numbers must not start with + or 0. Please remove or update the following numbers:\n{list}");
$message = str_replace(
'{list}',
implode("\n", $invalidNumbers),
$message
);
return SetupResult::error($message, 'https://portal.nextcloud.com/article/Nextcloud-Talk/Nextcloud-Talk-Phone/Direct-Dial-in#content-provisioning');
}

if ($this->talkConfig->getSIPSharedSecret() === ''
&& $this->talkConfig->getDialInInfo() === ''
&& $this->appConfig->getAppValueInt(Config::FEATURE_HINTS_HIDDEN) < 34) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/AdminSettings/SIPBridge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
:disabled="loading"
labelOutside />
<p class="settings-hint additional-top-margin">
{{ t('spreed', 'Prefix to configured user number for outgoing calls (default is `+`)') }}
{{ t('spreed', 'Deprecated: Only used for direct dial-in from older SIP bridges, which send phone numbers without this prefix (default is `+`)') }}
</p>
</template>

Expand Down
Loading
Loading