From 7708d5e539201cbd40e2d61fb0eb6af88d07316c Mon Sep 17 00:00:00 2001 From: olel Date: Sat, 22 Nov 2025 22:13:20 +0100 Subject: [PATCH 01/12] =?UTF-8?q?=F0=9F=94=A7=20add=20ami=20related=20conf?= =?UTF-8?q?iguration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gregor Michels --- app/core/config.py | 6 ++++++ asterisk/entrypoint.sh | 7 +++++++ asterisk/etc/asterisk/manager.conf.tmpl | 9 +++++++++ docs/configuration.md | 25 ++++++++++++++++++++++--- 4 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 asterisk/etc/asterisk/manager.conf.tmpl diff --git a/app/core/config.py b/app/core/config.py index 38b61b5..d14010d 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -244,5 +244,11 @@ def logging_loglevel(self) -> int: FEDERATION_IAX2_HOST: str = ASTERISK_HOST FEDERATION_UURU_HOST: str = WEB_HOST + ## AMI + ASTERISK_AMI_USER: str = "uuru_ami_user" + ASTERISK_AMI_PASS: str = "uuru_ami_secret" + ASTERISK_AMI_ADDR: str = "172.17.0.1" + ASTERSIK_AMI_PORT: int = 5038 + settings = Settings() diff --git a/asterisk/entrypoint.sh b/asterisk/entrypoint.sh index 02dd4af..2c11e95 100755 --- a/asterisk/entrypoint.sh +++ b/asterisk/entrypoint.sh @@ -2,6 +2,12 @@ set -e +# set default values for AMI handling +UURU_ASTERISK_AMI_USER="${UURU_ASTERISK_AMI_USER:-uuru_ami_user}" +UURU_ASTERISK_AMI_PASS="${UURU_ASTERISK_AMI_PASS:-uuru_ami_secret}" +UURU_ASTERISK_AMI_ADDR="${UURU_ASTERISK_AMI_ADDR:-172.17.0.1}" +UURU_ASTERISK_AMI_PORT="${UURU_ASTERISK_AMI_ADDR:-5038}" + # execute alembic database migrations cd /etc/alembic envsubst < config.ini.tmpl > config.ini @@ -11,4 +17,5 @@ alembic -c config.ini upgrade head cd / envsubst < /etc/odbc.ini.tmpl > /etc/odbc.ini envsubst < /etc/asterisk/res_odbc.conf.tmpl > /etc/asterisk/res_odbc.conf +envsubst < /etc/asterisk/manager.conf.tmpl > /etc/asterisk/manager.conf exec asterisk -f \ No newline at end of file diff --git a/asterisk/etc/asterisk/manager.conf.tmpl b/asterisk/etc/asterisk/manager.conf.tmpl new file mode 100644 index 0000000..36588db --- /dev/null +++ b/asterisk/etc/asterisk/manager.conf.tmpl @@ -0,0 +1,9 @@ +[general] +enabled=yes +port=${UURU_ASTERISK_AMI_PORT} +bindaddr=${UURU_ASTERISK_AMI_ADDR} + +[${UURU_ASTERISK_AMI_USER}] +secret=${UURU_ASTERISK_AMI_PASS} +read=all +write=all diff --git a/docs/configuration.md b/docs/configuration.md index 619146a..1cd1aae 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -23,8 +23,8 @@ The credentials for the first created account are configured by ### HTTP routing !!! warning - Do not change those values, currently uURU contains some hardcoded links - which will break otherwise! +Do not change those values, currently uURU contains some hardcoded links +which will break otherwise! | Key | Description | Default | | ----------------------- | --------------------------------------- | ------------ | @@ -122,7 +122,7 @@ The database must be a mariadb / mysql db ### WebSIP !!! note - The `UURU_WEBSIP_EXTENSION_RANGE` should be reserved via the `UURU_RESERVED_EXTENSIONS` key. +The `UURU_WEBSIP_EXTENSION_RANGE` should be reserved via the `UURU_RESERVED_EXTENSIONS` key. | Key | Description | Default | | --------------------------- | ------------------------------------------------------ | ---------------------- | @@ -141,3 +141,22 @@ The database must be a mariadb / mysql db | UURU_MEDIA_ALLOW_RAW | Allow upload of raw files | 0 | | UURU_MEDIA_IMAGE_STORAGE_FORMAT | Format in which images should be stored on disk | png | | UURU_MEDIA_AUDIO_STORAGE_FORMAT | Format in which audio files should be stored on disk | mp3 | + +### Asterisk Manager Interface + +!!! info + Because the `asterisk` container is exposed via `--network=host` we have to bind the AMI interface to a specific (non 127.0.0.0/8) ip. + We assume that your docker installation ships with `docker0` on `172.17.0.1` - change to your setup if needed. + +!!! danger + By default all other containers/processes on your machine can talk via `AMI` to the `asterisk`. + We assume a trusted environment. + Please generate a secure `UURU_ASTERISK_AMI_PASS` for production setups. + + +| Key | Description | Default | +| ---------------------- | ------------------------------------------- | -------------------------- | +| UURU_ASTERISK_AMI_USER | Username for the asterisk manager interface | uuru_ami_user | +| UURU_ASTERISK_AMI_PASS | Secret for the asterisk manager interface | uuru_ami_secret | +| UURU_ASTERISK_AMI_ADDR | Host of the asterisk manager interface | 172.17.0.1 (Docker bridge) | +| UURU_ASTERSIK_AMI_PORT | Port of the asterisk manager interface | 5038 | From 1e94f990527093023dd157c880161500f9e4dbc8 Mon Sep 17 00:00:00 2001 From: olel Date: Sat, 22 Nov 2025 22:13:55 +0100 Subject: [PATCH 02/12] =?UTF-8?q?=F0=9F=94=96=20fixed=20ubuntu=20version?= =?UTF-8?q?=20/=20updated=20asterisk=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- asterisk/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/asterisk/Dockerfile b/asterisk/Dockerfile index 8ce9f1c..2264399 100644 --- a/asterisk/Dockerfile +++ b/asterisk/Dockerfile @@ -1,6 +1,6 @@ -FROM ubuntu:24.04 +FROM ubuntu:noble-20251013 -ARG ASTERISK_SOURCE_VERSION=20.16.0 +ARG ASTERISK_SOURCE_VERSION=20.17.0 # download needed packages RUN apt-get update && apt-get install -y --no-install-recommends \ From f13477c0c118116224711edd6393cf4406adf305 Mon Sep 17 00:00:00 2001 From: olel Date: Sat, 22 Nov 2025 22:54:24 +0100 Subject: [PATCH 03/12] =?UTF-8?q?=E2=9C=A8=20add=20api=20endpoint=20to=20o?= =?UTF-8?q?riginate=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/telephoning.py | 40 ++++++++++++++++++++++++++++++++++++++++ app/telephoning/main.py | 18 ++++++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/app/api/telephoning.py b/app/api/telephoning.py index 34b8a76..3e35f59 100644 --- a/app/api/telephoning.py +++ b/app/api/telephoning.py @@ -5,6 +5,12 @@ Licensed under the MIT license. See LICENSE file in the project root for details. """ +from asterisk.ami.response import FutureResponse +from logging import getLogger +from asterisk.ami.client import AMIClientAdapter +from app.models.crud.asterisk import get_contact +from app.core.db import SessionDep +from app.models.crud.extension import get_extension_by_id from datetime import datetime from typing import Optional from fastapi import APIRouter, HTTPException @@ -22,6 +28,7 @@ router = APIRouter(prefix="/telephoning", tags=["telephoning"]) +logger = getLogger(__name__) class PhoneType(BaseModel): schema: Optional[dict] = None @@ -97,3 +104,36 @@ def put_websip(extension: str): ext.last_seen = datetime.now() return {} + + +@router.get("/originate", status_code=status.HTTP_204_NO_CONTENT) +def originate_call(session: SessionDep, session_asterisk: SessionAsteriskDep, user: CurrentUser, source: str, dest: str): + source_extension = get_extension_by_id(session, source, public=False) + if source_extension is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Source extension unknown") + if source_extension.user_id != user.id and not user.role == UserRole.ADMIN: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You may not originate calls from this extension") + + contact = get_contact(session_asterisk, source_extension, user) + if contact is None: + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail="The source extension is offline") + + if not dest.isnumeric(): + raise HTTPException(status_code=status.HTTP_406_NOT_ACCEPTABLE, detail="Destination must be a number!") + + client = Telephoning.instance().get_ami_client() + adapter = AMIClientAdapter(client) + response: FutureResponse = adapter.Originate( + Channel=f"PJSIP/{source}", + Exten=dest, + Priority=1, + Context="pjsip_internal" + ) + + logger.info(f"Originated call from {source} to {dest}") + logger.debug(f"Received AMI response:\n{response.response}") + + if response is not None: + raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="unable to originate call due to error from AMI") + + return {} \ No newline at end of file diff --git a/app/telephoning/main.py b/app/telephoning/main.py index 0f8f1a0..dd2c756 100644 --- a/app/telephoning/main.py +++ b/app/telephoning/main.py @@ -5,6 +5,7 @@ Licensed under the MIT license. See LICENSE file in the project root for details. """ +from typing import Self import importlib import pkgutil from fastapi import APIRouter, FastAPI @@ -12,6 +13,8 @@ import json from logging import getLogger +from asterisk.ami import AMIClient + from app.core.config import settings from app.telephoning.flavor import PhoneFlavor @@ -47,7 +50,7 @@ def load_phone_flavors() -> list[PhoneFlavor]: class Telephoning(object): - _instance = None + _instance: Self | None = None @staticmethod def instance(): @@ -65,8 +68,12 @@ def __init__(self): self.flavor_by_type = {} self.all_types = [] + self.ami_client = AMIClient(settings.ASTERISK_AMI_ADDR, settings.ASTERSIK_AMI_PORT) + def start(self, app: FastAPI, scheduler: BackgroundScheduler): + self.setup_ami() + for cls in self.flavor_classes: # 1st: create instance flavor_name = cls.__name__.lower() @@ -109,7 +116,14 @@ def start(self, app: FastAPI, scheduler: BackgroundScheduler): # include router app.include_router(self.router) - def stop(self): ... + def stop(self): + self.ami_client.logoff() + + def setup_ami(self): + self.ami_client.login(settings.ASTERISK_AMI_USER, settings.ASTERISK_AMI_PASS) + + def get_ami_client(self): + return self.ami_client @staticmethod def get_flavor_by_type(phone_type: str) -> PhoneFlavor | None: From 7e696071bcb12c1dcd232aafd2c7353e355b39b5 Mon Sep 17 00:00:00 2001 From: olel Date: Sat, 22 Nov 2025 23:02:11 +0100 Subject: [PATCH 04/12] =?UTF-8?q?=E2=9E=95=20add=20asterisk=20ami=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 1 + uv.lock | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4df03a3..444259b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "alembic>=1.16.5", "apscheduler>=3.11.0", "argon2-cffi>=25.1.0", + "asterisk-ami>=0.1.7", "coverage>=7.9.2", "dicttoxml>=1.7.16", "fastapi[standard]>=0.116.1", diff --git a/uv.lock b/uv.lock index 749e871..ce43d49 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" [[package]] @@ -46,6 +46,7 @@ dependencies = [ { name = "alembic" }, { name = "apscheduler" }, { name = "argon2-cffi" }, + { name = "asterisk-ami" }, { name = "coverage" }, { name = "dicttoxml" }, { name = "fastapi", extra = ["standard"] }, @@ -74,6 +75,7 @@ requires-dist = [ { name = "alembic", specifier = ">=1.16.5" }, { name = "apscheduler", specifier = ">=3.11.0" }, { name = "argon2-cffi", specifier = ">=25.1.0" }, + { name = "asterisk-ami", specifier = ">=0.1.7" }, { name = "coverage", specifier = ">=7.9.2" }, { name = "dicttoxml", specifier = ">=1.7.16" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.116.1" }, @@ -142,6 +144,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104, upload-time = "2021-12-01T09:09:31.335Z" }, ] +[[package]] +name = "asterisk-ami" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/01/ae378a9b0ae4516bc2eee7332e2296a3574bb42feae5ccf2bd8cf7f1968c/asterisk-ami-0.1.7.tar.gz", hash = "sha256:de954116b7b03fb1b5420d9d83d847a3ba0d4cf1449847eada88b4bfde080136", size = 33440, upload-time = "2024-01-05T12:16:17.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/98/a29af5201e7a2c9c948debe134def3a636f73fb884710e6afb30771e487f/asterisk_ami-0.1.7-py3-none-any.whl", hash = "sha256:b94baabc1107214f3369a804fd1efda0649ba6afcb21095c6b438b6245fef7ff", size = 9745, upload-time = "2024-01-05T12:16:16.387Z" }, +] + [[package]] name = "certifi" version = "2025.7.14" From 8e3e307c869c566d85d7d852c39912171c37de29 Mon Sep 17 00:00:00 2001 From: olel Date: Sat, 22 Nov 2025 23:08:54 +0100 Subject: [PATCH 05/12] =?UTF-8?q?=F0=9F=A9=B9=20fix=20missing=20exports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- asterisk/entrypoint.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/asterisk/entrypoint.sh b/asterisk/entrypoint.sh index 2c11e95..a675ef5 100755 --- a/asterisk/entrypoint.sh +++ b/asterisk/entrypoint.sh @@ -3,10 +3,10 @@ set -e # set default values for AMI handling -UURU_ASTERISK_AMI_USER="${UURU_ASTERISK_AMI_USER:-uuru_ami_user}" -UURU_ASTERISK_AMI_PASS="${UURU_ASTERISK_AMI_PASS:-uuru_ami_secret}" -UURU_ASTERISK_AMI_ADDR="${UURU_ASTERISK_AMI_ADDR:-172.17.0.1}" -UURU_ASTERISK_AMI_PORT="${UURU_ASTERISK_AMI_ADDR:-5038}" +export UURU_ASTERISK_AMI_USER="${UURU_ASTERISK_AMI_USER:-uuru_ami_user}" +export UURU_ASTERISK_AMI_PASS="${UURU_ASTERISK_AMI_PASS:-uuru_ami_secret}" +export UURU_ASTERISK_AMI_ADDR="${UURU_ASTERISK_AMI_ADDR:-172.17.0.1}" +export UURU_ASTERISK_AMI_PORT="${UURU_ASTERISK_AMI_PORT:-5038}" # execute alembic database migrations cd /etc/alembic From ed47866b10ff17c8f93f5eedcb6c4f14a3280394 Mon Sep 17 00:00:00 2001 From: olel Date: Sat, 22 Nov 2025 23:14:30 +0100 Subject: [PATCH 06/12] =?UTF-8?q?=F0=9F=9A=91=20fixed=20check=20if=20respo?= =?UTF-8?q?nse=20is=20empty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/telephoning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/telephoning.py b/app/api/telephoning.py index 3e35f59..ec98572 100644 --- a/app/api/telephoning.py +++ b/app/api/telephoning.py @@ -133,7 +133,7 @@ def originate_call(session: SessionDep, session_asterisk: SessionAsteriskDep, us logger.info(f"Originated call from {source} to {dest}") logger.debug(f"Received AMI response:\n{response.response}") - if response is not None: + if response.response is not None: raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="unable to originate call due to error from AMI") return {} \ No newline at end of file From ca49101b436d7beddd3e1439f16178561df1adb9 Mon Sep 17 00:00:00 2001 From: olel Date: Sat, 22 Nov 2025 23:52:41 +0100 Subject: [PATCH 07/12] =?UTF-8?q?=F0=9F=91=BD=EF=B8=8F=20update=20api=20cl?= =?UTF-8?q?ient?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/client/sdk.gen.ts | 19 +++++++++++++++++- frontend/src/client/types.gen.ts | 34 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/frontend/src/client/sdk.gen.ts b/frontend/src/client/sdk.gen.ts index 660d6e0..ea5144c 100644 --- a/frontend/src/client/sdk.gen.ts +++ b/frontend/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import { type Options as ClientOptions, type TDataShape, type Client, formDataBodySerializer } from './client'; -import type { LoginApiV1UserLoginPostData, LoginApiV1UserLoginPostResponses, LoginApiV1UserLoginPostErrors, LogoutApiV1UserLogoutGetData, LogoutApiV1UserLogoutGetResponses, DeleteApiV1UserDeleteData, DeleteApiV1UserDeleteResponses, DeleteApiV1UserDeleteErrors, InfoApiV1UserGetData, InfoApiV1UserGetResponses, InfoApiV1UserGetErrors, UpdateApiV1UserPatchData, UpdateApiV1UserPatchResponses, UpdateApiV1UserPatchErrors, RegisterApiV1UserRegisterPostData, RegisterApiV1UserRegisterPostResponses, RegisterApiV1UserRegisterPostErrors, ChangePasswordApiV1UserPasswordPatchData, ChangePasswordApiV1UserPasswordPatchResponses, ChangePasswordApiV1UserPasswordPatchErrors, AllUsersApiV1UserAllGetData, AllUsersApiV1UserAllGetResponses, DeleteInviteApiV1UserInviteDeleteData, DeleteInviteApiV1UserInviteDeleteResponses, DeleteInviteApiV1UserInviteDeleteErrors, GetInvitesApiV1UserInviteGetData, GetInvitesApiV1UserInviteGetResponses, CreateInviteApiV1UserInvitePostData, CreateInviteApiV1UserInvitePostResponses, CreateInviteApiV1UserInvitePostErrors, CreateApiV1ExtensionPostData, CreateApiV1ExtensionPostResponses, CreateApiV1ExtensionPostErrors, DeleteApiV1ExtensionExtensionDeleteData, DeleteApiV1ExtensionExtensionDeleteResponses, DeleteApiV1ExtensionExtensionDeleteErrors, UpdateApiV1ExtensionExtensionPatchData, UpdateApiV1ExtensionExtensionPatchResponses, UpdateApiV1ExtensionExtensionPatchErrors, GetApiV1ExtensionInfoExtensionGetData, GetApiV1ExtensionInfoExtensionGetResponses, GetApiV1ExtensionInfoExtensionGetErrors, GetOwnApiV1ExtensionOwnGetData, GetOwnApiV1ExtensionOwnGetResponses, PhonebookApiV1ExtensionPhonebookGetData, PhonebookApiV1ExtensionPhonebookGetResponses, PhonebookApiV1ExtensionPhonebookGetErrors, AdminPhonebookApiV1ExtensionAllGetData, AdminPhonebookApiV1ExtensionAllGetResponses, AdminPhonebookApiV1ExtensionAllGetErrors, GetExtensionsOnlineApiV1ExtensionOnlineGetData, GetExtensionsOnlineApiV1ExtensionOnlineGetResponses, IsExtensionOnlineApiV1ExtensionIsOnlineExtensionGetData, IsExtensionOnlineApiV1ExtensionIsOnlineExtensionGetResponses, IsExtensionOnlineApiV1ExtensionIsOnlineExtensionGetErrors, GetExtensionContactApiV1ExtensionContactExtensionGetData, GetExtensionContactApiV1ExtensionContactExtensionGetResponses, GetExtensionContactApiV1ExtensionContactExtensionGetErrors, GetSettingsApiV1SettingsGetData, GetSettingsApiV1SettingsGetResponses, GetPhoneTypesApiV1TelephoningTypesGetData, GetPhoneTypesApiV1TelephoningTypesGetResponses, DeleteWebsipApiV1TelephoningWebsipDeleteData, DeleteWebsipApiV1TelephoningWebsipDeleteResponses, DeleteWebsipApiV1TelephoningWebsipDeleteErrors, CreateWebsipApiV1TelephoningWebsipGetData, CreateWebsipApiV1TelephoningWebsipGetResponses, PutWebsipApiV1TelephoningWebsipPutData, PutWebsipApiV1TelephoningWebsipPutResponses, PutWebsipApiV1TelephoningWebsipPutErrors, GetPagesApiV1PagesGetData, GetPagesApiV1PagesGetResponses, RevokeOutgoingPeeringRequestApiV1FederationOutgoingRequestDeleteData, RevokeOutgoingPeeringRequestApiV1FederationOutgoingRequestDeleteResponses, RevokeOutgoingPeeringRequestApiV1FederationOutgoingRequestDeleteErrors, CreateOutgoingPeeringRequestApiV1FederationOutgoingRequestPostData, CreateOutgoingPeeringRequestApiV1FederationOutgoingRequestPostResponses, CreateOutgoingPeeringRequestApiV1FederationOutgoingRequestPostErrors, GetOutgoingPeeringRequestsApiV1FederationOutgoingRequestsGetData, GetOutgoingPeeringRequestsApiV1FederationOutgoingRequestsGetResponses, GetIncomingPeeringRequestsApiV1FederationIncomingRequestsGetData, GetIncomingPeeringRequestsApiV1FederationIncomingRequestsGetResponses, RevokeIncomingPeeringRequestApiV1FederationIncomingRequestRequestIdDeleteData, RevokeIncomingPeeringRequestApiV1FederationIncomingRequestRequestIdDeleteResponses, RevokeIncomingPeeringRequestApiV1FederationIncomingRequestRequestIdDeleteErrors, SetIncomingPeeringRequestStatusApiV1FederationIncomingRequestRequestIdPutData, SetIncomingPeeringRequestStatusApiV1FederationIncomingRequestRequestIdPutResponses, SetIncomingPeeringRequestStatusApiV1FederationIncomingRequestRequestIdPutErrors, CreateIncomingPeeringRequestApiV1FederationIncomingRequestPostData, CreateIncomingPeeringRequestApiV1FederationIncomingRequestPostResponses, CreateIncomingPeeringRequestApiV1FederationIncomingRequestPostErrors, SetOutgoingPeeringRequestStatusApiV1FederationOutgoingRequestRequestIdPutData, SetOutgoingPeeringRequestStatusApiV1FederationOutgoingRequestRequestIdPutResponses, SetOutgoingPeeringRequestStatusApiV1FederationOutgoingRequestRequestIdPutErrors, GetPeersApiV1FederationPeersGetData, GetPeersApiV1FederationPeersGetResponses, DeletePeerApiV1FederationPeerPeerIdDeleteData, DeletePeerApiV1FederationPeerPeerIdDeleteResponses, DeletePeerApiV1FederationPeerPeerIdDeleteErrors, TeardownRequestApiV1FederationPeerTeardownPostData, TeardownRequestApiV1FederationPeerTeardownPostResponses, TeardownRequestApiV1FederationPeerTeardownPostErrors, GetPeerPhonebooksApiV1FederationPhonebookGetData, GetPeerPhonebooksApiV1FederationPhonebookGetResponses, GetMediaApiV1MediaGetData, GetMediaApiV1MediaGetResponses, GetMediaApiV1MediaGetErrors, CreateMediaApiV1MediaPostData, CreateMediaApiV1MediaPostResponses, CreateMediaApiV1MediaPostErrors, DeleteMediaApiV1MediaMediaIdDeleteData, DeleteMediaApiV1MediaMediaIdDeleteResponses, DeleteMediaApiV1MediaMediaIdDeleteErrors, UpdateMediaApiV1MediaMediaIdPutData, UpdateMediaApiV1MediaMediaIdPutResponses, UpdateMediaApiV1MediaMediaIdPutErrors, GetMediaContentApiV1MediaByidMediaIdExtGetData, GetMediaContentApiV1MediaByidMediaIdExtGetResponses, GetMediaContentApiV1MediaByidMediaIdExtGetErrors, GetMediaContentApiV1MediaByidMediaIdGetData, GetMediaContentApiV1MediaByidMediaIdGetResponses, GetMediaContentApiV1MediaByidMediaIdGetErrors, GetMediaByNameApiV1MediaByextensionExtensionIdQueryGetData, GetMediaByNameApiV1MediaByextensionExtensionIdQueryGetResponses, GetMediaByNameApiV1MediaByextensionExtensionIdQueryGetErrors, IndexGetData, IndexGetResponses, GetUpdateTelephoningInnovaphoneUpdateGetData, GetUpdateTelephoningInnovaphoneUpdateGetResponses, GetUpdateTelephoningInnovaphoneUpdateGetErrors, GetConfigTelephoningInnovaphoneConfigGetData, GetConfigTelephoningInnovaphoneConfigGetResponses, GetConfigTelephoningInnovaphoneConfigGetErrors, GetServiceDiscoveryTelephoningInnovaphoneServiceDiscoveryGetData, GetServiceDiscoveryTelephoningInnovaphoneServiceDiscoveryGetResponses, GetSipCredentialsTelephoningIotSecretGetData, GetSipCredentialsTelephoningIotSecretGetResponses, GetSipCredentialsTelephoningIotSecretGetErrors, GetConfigTelephoningSnomSnomMacGetData, GetConfigTelephoningSnomSnomMacGetResponses, GetConfigTelephoningSnomSnomMacGetErrors } from './types.gen'; +import type { LoginApiV1UserLoginPostData, LoginApiV1UserLoginPostResponses, LoginApiV1UserLoginPostErrors, LogoutApiV1UserLogoutGetData, LogoutApiV1UserLogoutGetResponses, DeleteApiV1UserDeleteData, DeleteApiV1UserDeleteResponses, DeleteApiV1UserDeleteErrors, InfoApiV1UserGetData, InfoApiV1UserGetResponses, InfoApiV1UserGetErrors, UpdateApiV1UserPatchData, UpdateApiV1UserPatchResponses, UpdateApiV1UserPatchErrors, RegisterApiV1UserRegisterPostData, RegisterApiV1UserRegisterPostResponses, RegisterApiV1UserRegisterPostErrors, ChangePasswordApiV1UserPasswordPatchData, ChangePasswordApiV1UserPasswordPatchResponses, ChangePasswordApiV1UserPasswordPatchErrors, AllUsersApiV1UserAllGetData, AllUsersApiV1UserAllGetResponses, DeleteInviteApiV1UserInviteDeleteData, DeleteInviteApiV1UserInviteDeleteResponses, DeleteInviteApiV1UserInviteDeleteErrors, GetInvitesApiV1UserInviteGetData, GetInvitesApiV1UserInviteGetResponses, CreateInviteApiV1UserInvitePostData, CreateInviteApiV1UserInvitePostResponses, CreateInviteApiV1UserInvitePostErrors, CreateApiV1ExtensionPostData, CreateApiV1ExtensionPostResponses, CreateApiV1ExtensionPostErrors, DeleteApiV1ExtensionExtensionDeleteData, DeleteApiV1ExtensionExtensionDeleteResponses, DeleteApiV1ExtensionExtensionDeleteErrors, UpdateApiV1ExtensionExtensionPatchData, UpdateApiV1ExtensionExtensionPatchResponses, UpdateApiV1ExtensionExtensionPatchErrors, GetApiV1ExtensionInfoExtensionGetData, GetApiV1ExtensionInfoExtensionGetResponses, GetApiV1ExtensionInfoExtensionGetErrors, GetOwnApiV1ExtensionOwnGetData, GetOwnApiV1ExtensionOwnGetResponses, PhonebookApiV1ExtensionPhonebookGetData, PhonebookApiV1ExtensionPhonebookGetResponses, PhonebookApiV1ExtensionPhonebookGetErrors, AdminPhonebookApiV1ExtensionAllGetData, AdminPhonebookApiV1ExtensionAllGetResponses, AdminPhonebookApiV1ExtensionAllGetErrors, GetExtensionsOnlineApiV1ExtensionOnlineGetData, GetExtensionsOnlineApiV1ExtensionOnlineGetResponses, IsExtensionOnlineApiV1ExtensionIsOnlineExtensionGetData, IsExtensionOnlineApiV1ExtensionIsOnlineExtensionGetResponses, IsExtensionOnlineApiV1ExtensionIsOnlineExtensionGetErrors, GetExtensionContactApiV1ExtensionContactExtensionGetData, GetExtensionContactApiV1ExtensionContactExtensionGetResponses, GetExtensionContactApiV1ExtensionContactExtensionGetErrors, GetSettingsApiV1SettingsGetData, GetSettingsApiV1SettingsGetResponses, GetPhoneTypesApiV1TelephoningTypesGetData, GetPhoneTypesApiV1TelephoningTypesGetResponses, DeleteWebsipApiV1TelephoningWebsipDeleteData, DeleteWebsipApiV1TelephoningWebsipDeleteResponses, DeleteWebsipApiV1TelephoningWebsipDeleteErrors, CreateWebsipApiV1TelephoningWebsipGetData, CreateWebsipApiV1TelephoningWebsipGetResponses, PutWebsipApiV1TelephoningWebsipPutData, PutWebsipApiV1TelephoningWebsipPutResponses, PutWebsipApiV1TelephoningWebsipPutErrors, OriginateCallApiV1TelephoningOriginateGetData, OriginateCallApiV1TelephoningOriginateGetResponses, OriginateCallApiV1TelephoningOriginateGetErrors, GetPagesApiV1PagesGetData, GetPagesApiV1PagesGetResponses, RevokeOutgoingPeeringRequestApiV1FederationOutgoingRequestDeleteData, RevokeOutgoingPeeringRequestApiV1FederationOutgoingRequestDeleteResponses, RevokeOutgoingPeeringRequestApiV1FederationOutgoingRequestDeleteErrors, CreateOutgoingPeeringRequestApiV1FederationOutgoingRequestPostData, CreateOutgoingPeeringRequestApiV1FederationOutgoingRequestPostResponses, CreateOutgoingPeeringRequestApiV1FederationOutgoingRequestPostErrors, GetOutgoingPeeringRequestsApiV1FederationOutgoingRequestsGetData, GetOutgoingPeeringRequestsApiV1FederationOutgoingRequestsGetResponses, GetIncomingPeeringRequestsApiV1FederationIncomingRequestsGetData, GetIncomingPeeringRequestsApiV1FederationIncomingRequestsGetResponses, RevokeIncomingPeeringRequestApiV1FederationIncomingRequestRequestIdDeleteData, RevokeIncomingPeeringRequestApiV1FederationIncomingRequestRequestIdDeleteResponses, RevokeIncomingPeeringRequestApiV1FederationIncomingRequestRequestIdDeleteErrors, SetIncomingPeeringRequestStatusApiV1FederationIncomingRequestRequestIdPutData, SetIncomingPeeringRequestStatusApiV1FederationIncomingRequestRequestIdPutResponses, SetIncomingPeeringRequestStatusApiV1FederationIncomingRequestRequestIdPutErrors, CreateIncomingPeeringRequestApiV1FederationIncomingRequestPostData, CreateIncomingPeeringRequestApiV1FederationIncomingRequestPostResponses, CreateIncomingPeeringRequestApiV1FederationIncomingRequestPostErrors, SetOutgoingPeeringRequestStatusApiV1FederationOutgoingRequestRequestIdPutData, SetOutgoingPeeringRequestStatusApiV1FederationOutgoingRequestRequestIdPutResponses, SetOutgoingPeeringRequestStatusApiV1FederationOutgoingRequestRequestIdPutErrors, GetPeersApiV1FederationPeersGetData, GetPeersApiV1FederationPeersGetResponses, DeletePeerApiV1FederationPeerPeerIdDeleteData, DeletePeerApiV1FederationPeerPeerIdDeleteResponses, DeletePeerApiV1FederationPeerPeerIdDeleteErrors, TeardownRequestApiV1FederationPeerTeardownPostData, TeardownRequestApiV1FederationPeerTeardownPostResponses, TeardownRequestApiV1FederationPeerTeardownPostErrors, GetPeerPhonebooksApiV1FederationPhonebookGetData, GetPeerPhonebooksApiV1FederationPhonebookGetResponses, GetMediaApiV1MediaGetData, GetMediaApiV1MediaGetResponses, GetMediaApiV1MediaGetErrors, CreateMediaApiV1MediaPostData, CreateMediaApiV1MediaPostResponses, CreateMediaApiV1MediaPostErrors, DeleteMediaApiV1MediaMediaIdDeleteData, DeleteMediaApiV1MediaMediaIdDeleteResponses, DeleteMediaApiV1MediaMediaIdDeleteErrors, UpdateMediaApiV1MediaMediaIdPutData, UpdateMediaApiV1MediaMediaIdPutResponses, UpdateMediaApiV1MediaMediaIdPutErrors, GetMediaContentApiV1MediaByidMediaIdExtGetData, GetMediaContentApiV1MediaByidMediaIdExtGetResponses, GetMediaContentApiV1MediaByidMediaIdExtGetErrors, GetMediaContentApiV1MediaByidMediaIdGetData, GetMediaContentApiV1MediaByidMediaIdGetResponses, GetMediaContentApiV1MediaByidMediaIdGetErrors, GetMediaByNameApiV1MediaByextensionExtensionIdQueryGetData, GetMediaByNameApiV1MediaByextensionExtensionIdQueryGetResponses, GetMediaByNameApiV1MediaByextensionExtensionIdQueryGetErrors, IndexGetData, IndexGetResponses, GetUpdateTelephoningInnovaphoneUpdateGetData, GetUpdateTelephoningInnovaphoneUpdateGetResponses, GetUpdateTelephoningInnovaphoneUpdateGetErrors, GetConfigTelephoningInnovaphoneConfigGetData, GetConfigTelephoningInnovaphoneConfigGetResponses, GetConfigTelephoningInnovaphoneConfigGetErrors, GetServiceDiscoveryTelephoningInnovaphoneServiceDiscoveryGetData, GetServiceDiscoveryTelephoningInnovaphoneServiceDiscoveryGetResponses, GetSipCredentialsTelephoningIotSecretGetData, GetSipCredentialsTelephoningIotSecretGetResponses, GetSipCredentialsTelephoningIotSecretGetErrors, GetConfigTelephoningSnomSnomMacGetData, GetConfigTelephoningSnomSnomMacGetResponses, GetConfigTelephoningSnomSnomMacGetErrors } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -453,6 +453,23 @@ export const putWebsipApiV1TelephoningWebsipPut = (options: Options) => { + return (options.client ?? _heyApiClient).get({ + security: [ + { + in: 'cookie', + name: 'auth', + type: 'apiKey' + } + ], + url: '/api/v1/telephoning/originate', + ...options + }); +}; + /** * Get Pages */ diff --git a/frontend/src/client/types.gen.ts b/frontend/src/client/types.gen.ts index da2e518..254dfe8 100644 --- a/frontend/src/client/types.gen.ts +++ b/frontend/src/client/types.gen.ts @@ -1567,6 +1567,40 @@ export type PutWebsipApiV1TelephoningWebsipPutResponses = { export type PutWebsipApiV1TelephoningWebsipPutResponse = PutWebsipApiV1TelephoningWebsipPutResponses[keyof PutWebsipApiV1TelephoningWebsipPutResponses]; +export type OriginateCallApiV1TelephoningOriginateGetData = { + body?: never; + path?: never; + query: { + /** + * Source + */ + source: string; + /** + * Dest + */ + dest: string; + }; + url: '/api/v1/telephoning/originate'; +}; + +export type OriginateCallApiV1TelephoningOriginateGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type OriginateCallApiV1TelephoningOriginateGetError = OriginateCallApiV1TelephoningOriginateGetErrors[keyof OriginateCallApiV1TelephoningOriginateGetErrors]; + +export type OriginateCallApiV1TelephoningOriginateGetResponses = { + /** + * Successful Response + */ + 204: void; +}; + +export type OriginateCallApiV1TelephoningOriginateGetResponse = OriginateCallApiV1TelephoningOriginateGetResponses[keyof OriginateCallApiV1TelephoningOriginateGetResponses]; + export type GetPagesApiV1PagesGetData = { body?: never; path?: never; From e834f5cb97fee13bcaaef443f98bc64e4036d7f7 Mon Sep 17 00:00:00 2001 From: olel Date: Sun, 23 Nov 2025 00:04:14 +0100 Subject: [PATCH 08/12] =?UTF-8?q?=E2=9C=A8=20implemented=20orignate=20butt?= =?UTF-8?q?on=20in=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gregor Michels --- frontend/src/components/webphone.svelte | 66 +++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/webphone.svelte b/frontend/src/components/webphone.svelte index a7cef51..9a98ad5 100644 --- a/frontend/src/components/webphone.svelte +++ b/frontend/src/components/webphone.svelte @@ -5,10 +5,16 @@ deleteWebsipApiV1TelephoningWebsipDelete, type WebSipExtension, type ExtensionBase, - putWebsipApiV1TelephoningWebsipPut + putWebsipApiV1TelephoningWebsipPut, + getExtensionsOnlineApiV1ExtensionOnlineGet, + type Extension, + + originateCallApiV1TelephoningOriginateGet + } from '../client'; import { Web } from 'sip.js'; - import { settings } from '../sharedState.svelte'; + import { settings, user_info } from '../sharedState.svelte'; + import { push_api_error } from '../messageService.svelte'; let { isOpen = $bindable(), target = $bindable() } = $props(); @@ -48,9 +54,30 @@ if (isOpen) { statusText = 'Start the call using the button below!'; statusColor = 'info'; + + if (user_info.val !== undefined) { + refreshOnlineExtensions(); + } } }); + let onlineExtensions = $state([]); + let selectedSourceExtension = $state(null); + + async function refreshOnlineExtensions() { + let { data, error } = await getExtensionsOnlineApiV1ExtensionOnlineGet({ + credentials: 'include' + }); + if (error) { + push_api_error(error, 'Failed to load online extensions'); + return; + } + onlineExtensions = data!; + if (data!.length > 0) { + selectedSourceExtension = data![0].extension; + } + } + // SimpleUser delegate const simpleUserDelegate: Web.SimpleUserDelegate = { onCallCreated: (): void => { @@ -89,6 +116,21 @@ let refreshInterval = $state | undefined>(undefined); + const originateCall = async () => { + if (!selectedSourceExtension) return; + + let result = await originateCallApiV1TelephoningOriginateGet({ + "credentials": "include", + query: { + source: selectedSourceExtension, + dest: target.extension + } + }); + if (result.response.status == 204) { + isOpen = false; + } + }; + const startCall = async () => { statusText = 'Creating temporary extension...'; // create websip extension @@ -181,7 +223,7 @@ @@ -209,6 +251,24 @@ > {/if} + {#if user_info.val !== undefined && status == 'prepare'} + +
+
+ Send call to Phone + + +
+ {/if}
From 88a8f9e7c3c462b0dbf6239fc114cf7800771591 Mon Sep 17 00:00:00 2001 From: olel Date: Sun, 23 Nov 2025 00:19:05 +0100 Subject: [PATCH 09/12] =?UTF-8?q?=F0=9F=9A=B8=20show=20status=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/webphone.svelte | 38 +++++++++++++++++++------ 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/webphone.svelte b/frontend/src/components/webphone.svelte index 9a98ad5..6e89bed 100644 --- a/frontend/src/components/webphone.svelte +++ b/frontend/src/components/webphone.svelte @@ -58,11 +58,15 @@ if (user_info.val !== undefined) { refreshOnlineExtensions(); } + originateCallRequested = false; + originateCallFailed = false; } }); let onlineExtensions = $state([]); let selectedSourceExtension = $state(null); + let originateCallRequested = $state(false); + let originateCallFailed = $state(false); async function refreshOnlineExtensions() { let { data, error } = await getExtensionsOnlineApiV1ExtensionOnlineGet({ @@ -118,16 +122,24 @@ const originateCall = async () => { if (!selectedSourceExtension) return; + originateCallRequested = true; - let result = await originateCallApiV1TelephoningOriginateGet({ - "credentials": "include", - query: { - source: selectedSourceExtension, - dest: target.extension + try { + let result = await originateCallApiV1TelephoningOriginateGet({ + "credentials": "include", + query: { + source: selectedSourceExtension, + dest: target.extension + } + }); + if (result.response.status == 204) { + isOpen = false; } - }); - if (result.response.status == 204) { - isOpen = false; + else { + originateCallFailed = true; + } + } catch { + originateCallFailed = true; } }; @@ -268,6 +280,16 @@ > + {#if originateCallRequested && !originateCallFailed} +
+ Requested! The selected phone will ring soon... +
+ {/if} + {#if originateCallFailed} +
+ Failed to send call to the selected phone! +
+ {/if} {/if} From 4c921843149fe653ea93c0c287ba26a385b53636 Mon Sep 17 00:00:00 2001 From: olel Date: Sun, 23 Nov 2025 00:20:13 +0100 Subject: [PATCH 10/12] =?UTF-8?q?=E2=9C=A8=20filter=20target=20extension?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/webphone.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/components/webphone.svelte b/frontend/src/components/webphone.svelte index 6e89bed..22908db 100644 --- a/frontend/src/components/webphone.svelte +++ b/frontend/src/components/webphone.svelte @@ -270,10 +270,12 @@ Send call to Phone