diff --git a/.forgejo/workflows/build-freeswitch.yaml b/.forgejo/workflows/build-freeswitch.yaml
new file mode 100644
index 0000000..711c705
--- /dev/null
+++ b/.forgejo/workflows/build-freeswitch.yaml
@@ -0,0 +1,22 @@
+on:
+ push:
+ paths:
+ - containers/freeswitch/**
+ - .forgejo/workflows/build-freeswitch.yaml
+jobs:
+ build-freeswitch:
+ runs-on: docker
+ container:
+ image: library/docker:dind
+ steps:
+ - run: apk add --no-cache nodejs git
+ - name: login to container registry
+ run: echo "${{ secrets.DEPLOY_TOKEN }}" | docker login --username ${{ secrets.DEPLOY_USER }} --password-stdin git.janky.solutions
+ - name: build container image
+ uses: docker/build-push-action@v6
+ with:
+ file: Containerfile
+ context: "{{defaultContext}}:containers/freeswitch"
+ tags: git.janky.solutions/jankysolutions/infra/freeswitch:latest
+ platforms: linux/amd64
+ push: true
diff --git a/containers/freeswitch/.gitignore b/containers/freeswitch/.gitignore
new file mode 100644
index 0000000..3921cac
--- /dev/null
+++ b/containers/freeswitch/.gitignore
@@ -0,0 +1 @@
+sources/
diff --git a/containers/freeswitch/Containerfile b/containers/freeswitch/Containerfile
new file mode 100644
index 0000000..aa765de
--- /dev/null
+++ b/containers/freeswitch/Containerfile
@@ -0,0 +1,84 @@
+FROM docker.io/library/debian:latest AS build
+RUN apt-get update && apt-get install -y \
+# build
+ build-essential cmake automake autoconf 'libtool-bin|libtool' pkg-config \
+# general
+ libssl-dev zlib1g-dev libdb-dev unixodbc-dev libncurses5-dev libexpat1-dev libgdbm-dev bison erlang-dev libtpl-dev libtiff5-dev uuid-dev \
+# core
+ libpcre3-dev libedit-dev libsqlite3-dev libcurl4-openssl-dev nasm \
+# core codecs
+ libogg-dev libspeex-dev libspeexdsp-dev \
+# mod_enum
+ libldns-dev \
+# mod_python3
+ python3-dev \
+# mod_av
+ libavformat-dev libswscale-dev \
+# mod_lua
+ liblua5.2-dev \
+# mod_opus
+ libopus-dev \
+# mod_pgsql
+ libpq-dev \
+# mod_sndfile
+ libsndfile1-dev libflac-dev libogg-dev libvorbis-dev \
+# mod_shout
+ libshout3-dev libmpg123-dev libmp3lame-dev \
+# mod_memcached
+ libmemcached-dev
+
+# without this git am will fail
+RUN git config --global user.name nobody && git config --global user.email nobody@localhost
+
+ADD patches /patches
+
+# sofia-sip
+RUN git clone https://github.com/freeswitch/sofia-sip.git /usr/src/sofia-sip && \
+ cd /usr/src/sofia-sip && \
+ git checkout v1.13.17 && \
+ ./bootstrap.sh && \
+ ./configure CFLAGS="-g -ggdb" --with-pic --with-glib=no --without-doxygen && \
+ make && \
+ make install
+
+# libks
+RUN git clone https://github.com/signalwire/libks.git /usr/src/libks && \
+ cd /usr/src/libks && \
+ git checkout v2.0.3 && \
+ cmake . -DWITH_LIBBACKTRACE=1 -DCMAKE_INSTALL_PREFIX:PATH=/usr/local && \
+ make && \
+ make install
+
+# spandsp
+RUN git clone https://github.com/freeswitch/spandsp.git /usr/src/spandsp && \
+ cd /usr/src/spandsp && \
+ ./bootstrap.sh && \
+ ./configure && \
+ make && \
+ make install
+
+# freeswitch
+RUN git clone https://github.com/signalwire/freeswitch.git /usr/src/freeswitch && \
+ cd /usr/src/freeswitch && \
+ git checkout v1.10.11 && \
+ git am /patches/freeswitch/* && \
+ ./bootstrap.sh -j && \
+ ./configure --enable-core-pgsql-support --disable-fhs && \
+ make && \
+ make install
+RUN rm -rf /usr/local/freeswitch/conf
+
+
+FROM library/golang:1.21 AS dynamic-xmlconfig
+ADD dynamic-xmlconfig /go/dynamic-xmlconfig
+WORKDIR /go/dynamic-xmlconfig
+RUN go build -o /dynamic-xmlconfig .
+
+FROM debian:bookworm-slim
+RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y libcurl4 libsqlite3-0 libpcre3 libspeexdsp1 libspeex1 libedit2 libtpl0 libodbc2 libtiff6 liblua5.2-0 libopus0 libavformat59 libopus0 libsndfile1 libswscale6 libldns3 libpq5 && apt-get clean && rm -rf /var/lib/apt/lists/*
+COPY --from=build /usr/local/lib /usr/local/lib
+COPY --from=build /usr/local/freeswitch /usr/local/freeswitch
+COPY --from=dynamic-xmlconfig /dynamic-xmlconfig /usr/local/bin/dynamic-xmlconfig
+ADD freeswitch.xml /usr/local/freeswitch/conf/freeswitch.xml
+RUN ldconfig
+CMD ["/usr/local/freeswitch/bin/freeswitch", "-nf"]
diff --git a/containers/freeswitch/README.md b/containers/freeswitch/README.md
new file mode 100644
index 0000000..3e45918
--- /dev/null
+++ b/containers/freeswitch/README.md
@@ -0,0 +1,7 @@
+# FreeSWITCH Container
+This is an attempt to package a minimal FreeSWITCH container.
+
+* State is stored in an ODBC database. Must set environment variable `DSN`
+* Configuration can be retreived from an HTTP server by setting environment variable `CONFIG_URL`. See [mod_curl_xml](https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Modules/mod_xml_curl_1049001/) for details about the HTTP response format.
+*
+
diff --git a/containers/freeswitch/dynamic-xmlconfig/autoload_configs/modules.conf.xml b/containers/freeswitch/dynamic-xmlconfig/autoload_configs/modules.conf.xml
new file mode 100644
index 0000000..7d577c4
--- /dev/null
+++ b/containers/freeswitch/dynamic-xmlconfig/autoload_configs/modules.conf.xml
@@ -0,0 +1,6 @@
+
+
+
+ {{ $_, $module := range .Modules }}{{ end }}
+
+
diff --git a/containers/freeswitch/dynamic-xmlconfig/autoload_configs/xml_curl.conf.xml b/containers/freeswitch/dynamic-xmlconfig/autoload_configs/xml_curl.conf.xml
new file mode 100644
index 0000000..b2fe3d6
--- /dev/null
+++ b/containers/freeswitch/dynamic-xmlconfig/autoload_configs/xml_curl.conf.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
diff --git a/containers/freeswitch/dynamic-xmlconfig/go.mod b/containers/freeswitch/dynamic-xmlconfig/go.mod
new file mode 100644
index 0000000..8cbe994
--- /dev/null
+++ b/containers/freeswitch/dynamic-xmlconfig/go.mod
@@ -0,0 +1,5 @@
+module codeberg.org/thefinn93/freeswitch-container
+
+go 1.21.5
+
+require github.com/kelseyhightower/envconfig v1.4.0
diff --git a/containers/freeswitch/dynamic-xmlconfig/go.sum b/containers/freeswitch/dynamic-xmlconfig/go.sum
new file mode 100644
index 0000000..8642a1a
--- /dev/null
+++ b/containers/freeswitch/dynamic-xmlconfig/go.sum
@@ -0,0 +1,2 @@
+github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
+github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
diff --git a/containers/freeswitch/dynamic-xmlconfig/main.go b/containers/freeswitch/dynamic-xmlconfig/main.go
new file mode 100644
index 0000000..44efb8f
--- /dev/null
+++ b/containers/freeswitch/dynamic-xmlconfig/main.go
@@ -0,0 +1,165 @@
+package main
+
+import (
+ "embed"
+ "fmt"
+ "net/url"
+ "os"
+ "text/template"
+
+ "github.com/kelseyhightower/envconfig"
+)
+
+type Options struct {
+ DSN string `envconfig:"DSN"`
+ ConfigURL string `envconfig:"CONFIG_URL"` // URL called for configuration, see https://developer.signalwire.com/freeswitch/FreeSWITCH-Explained/Modules/mod_xml_curl_1049001/ (dialplan|directory|phrases will be fetched from to this URL)
+ CaptureServer string `envconfig:"CAPTURE_SERVER"` // if set, this server gets a copy of all SIP messages. Example: udp:homer.domain.com:5060;hep=3;capture_id=100
+ LogLevel string `envconfig:"LOG_LEVEL" default:"info"`
+ Gateway Gateway `envconfig:"GATEWAY"`
+ Modules []string `envconfig:"MODULES" default:"mod_event_socket,mod_sofia,mod_db,mod_dialplan_xml,mod_g723_1,mod_g729,mod_amr,mod_b64,mod_opus,mod_av,mod_sndfile,mod_native_file,mod_png,mod_local_stream,mod_tone_stream,mod_lua,mod_say_en"`
+}
+
+type Gateway struct {
+ Name string
+ Username string
+ Password string
+ Extra map[string]string
+}
+
+var (
+ //go:embed autoload_configs
+ autoloadConfigsFS embed.FS
+ autoloadConfigsTemplates = template.Must(template.New("autoload_configs").ParseFS(autoloadConfigsFS, "autoload_configs/*"))
+ options Options
+ vars = map[string]string{ // this is used to fill in what is normally in vars.xml
+ "sound_prefix": "$${sounds_dir}/en/us/callie",
+ "domain": "$${local_ip_v4}",
+ "domain_name": "$${domain}",
+ "hold_music": "local_stream://moh",
+ "use_profile": "external",
+ "rtp_sdes_suites": "AEAD_AES_256_GCM_8|AEAD_AES_128_GCM_8|AES_CM_256_HMAC_SHA1_80|AES_CM_192_HMAC_SHA1_80|AES_CM_128_HMAC_SHA1_80|AES_CM_256_HMAC_SHA1_32|AES_CM_192_HMAC_SHA1_32|AES_CM_128_HMAC_SHA1_32|AES_CM_128_NULL_AUTH",
+ "global_codec_prefs": "OPUS,G722,PCMU,PCMA,H264,VP8",
+ "outbound_codec_prefs": "OPUS,G722,PCMU,PCMA,H264,VP8",
+ "xmpp_client_profile": "xmppc",
+ "xmpp_server_profile": "xmpps",
+ "bind_server_ip": "auto",
+ "unroll_loops": "true",
+ "outbound_caller_name": "FreeSWITCH",
+ "outbound_caller_id": "0000000000",
+ "call_debug": "false",
+ "console_loglevel": "info",
+ "default_areacode": "918",
+ "default_country": "US",
+ "presence_privacy": "false",
+ "au-ring": "%(400,200,383,417);%(400,2000,383,417)",
+ "be-ring": "%(1000,3000,425)",
+ "ca-ring": "%(2000,4000,440,480)",
+ "cn-ring": "%(1000,4000,450)",
+ "cy-ring": "%(1500,3000,425)",
+ "cz-ring": "%(1000,4000,425)",
+ "de-ring": "%(1000,4000,425)",
+ "dk-ring": "%(1000,4000,425)",
+ "dz-ring": "%(1500,3500,425)",
+ "eg-ring": "%(2000,1000,475,375)",
+ "es-ring": "%(1500,3000,425)",
+ "fi-ring": "%(1000,4000,425)",
+ "fr-ring": "%(1500,3500,440)",
+ "hk-ring": "%(400,200,440,480);%(400,3000,440,480)",
+ "hu-ring": "%(1250,3750,425)",
+ "il-ring": "%(1000,3000,400)",
+ "in-ring": "%(400,200,425,375);%(400,2000,425,375)",
+ "jp-ring": "%(1000,2000,420,380)",
+ "ko-ring": "%(1000,2000,440,480)",
+ "pk-ring": "%(1000,2000,400)",
+ "pl-ring": "%(1000,4000,425)",
+ "ro-ring": "%(1850,4150,475,425)",
+ "rs-ring": "%(1000,4000,425)",
+ "ru-ring": "%(800,3200,425)",
+ "sa-ring": "%(1200,4600,425)",
+ "tr-ring": "%(2000,4000,450)",
+ "uk-ring": "%(400,200,400,450);%(400,2000,400,450)",
+ "us-ring": "%(2000,4000,440,480)",
+ "bong-ring": "v",
+ "beep": "%(1000,0,640)",
+ "sit": "%(274,0,913.8);%(274,0,1370.6);%(380,0,1776.7)",
+ "df_us_ssn": "(?!219099999|078051120)(?!666|000|9d{2})d{3}(?!00)d{2}(?!0{4})d{4}",
+ "df_luhn": "?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35d{3})d{11}",
+ "default_provider": "example.com",
+ "default_provider_username": "joeuser",
+ "default_provider_password": "password",
+ "default_provider_from_domain": "example.com",
+ "default_provider_register": "false",
+ "default_provider_contact": "5000",
+ "sip_tls_version": "tlsv1,tlsv1.1,tlsv1.2",
+ "sip_tls_ciphers": "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
+ "internal_auth_calls": "true",
+ "internal_sip_port": "5060",
+ "internal_tls_port": "5061",
+ "internal_ssl_enable": "false",
+ "external_auth_calls": "false",
+ "external_sip_port": "5080",
+ "external_tls_port": "5081",
+ "external_ssl_enable": "false",
+ "rtp_video_max_bandwidth_in": "3mb",
+ "rtp_video_max_bandwidth_out": "3mb",
+ "suppress_cng": "true",
+ "rtp_liberal_dtmf": "true",
+ "video_mute_png": "$${images_dir}/default-mute.png",
+ "video_no_avatar_png": "$${images_dir}/default-avatar.png",
+ }
+)
+
+func main() {
+ if err := envconfig.Process("", &options); err != nil {
+ panic(err)
+ }
+
+ if options.ConfigURL != "" {
+ options.Modules = append(options.Modules, "mod_xml_curl")
+ }
+
+ // autoload_configs
+ for k, v := range vars {
+ fmt.Printf("\n", k, v)
+ }
+
+ fmt.Println("")
+
+ configs, err := autoloadConfigsFS.ReadDir("autoload_configs")
+ if err != nil {
+ panic(err)
+ }
+
+ for _, file := range configs {
+ if file.IsDir() {
+ fmt.Println("skipping dir: ", file.Name())
+ continue
+ }
+ if err := autoloadConfigsTemplates.ExecuteTemplate(os.Stdout, file.Name(), options); err != nil {
+ panic(err)
+ }
+ }
+
+ fmt.Println("")
+}
+
+func (g *Gateway) Decode(value string) error {
+ u, err := url.Parse(value)
+ if err != nil {
+ return err
+ }
+
+ g.Name = u.Hostname()
+ g.Username = u.User.Username()
+ if password, ok := u.User.Password(); ok {
+ g.Password = password
+ }
+
+ g.Extra = make(map[string]string)
+ for k, v := range u.Query() {
+ if len(v) > 0 {
+ g.Extra[k] = v[0]
+ }
+ }
+ return nil
+}
diff --git a/containers/freeswitch/freeswitch.xml b/containers/freeswitch/freeswitch.xml
new file mode 100644
index 0000000..a743143
--- /dev/null
+++ b/containers/freeswitch/freeswitch.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/containers/freeswitch/patches/freeswitch/0001-Fix-mod_spandsp-build.patch b/containers/freeswitch/patches/freeswitch/0001-Fix-mod_spandsp-build.patch
new file mode 100644
index 0000000..3e8439c
--- /dev/null
+++ b/containers/freeswitch/patches/freeswitch/0001-Fix-mod_spandsp-build.patch
@@ -0,0 +1,61 @@
+From 9d91452d5a0a4a44884ff74fa2e49c51aaaa32dd Mon Sep 17 00:00:00 2001
+From: nobody
+Date: Wed, 24 Jan 2024 21:19:50 -0800
+Subject: [PATCH 1/2] Fix mod_spandsp build
+
+based on https://github.com/xrobau/freeswitch-ubuntubuilder/blob/master/patches/freeswitch/005-spandsp-updates.patch
+---
+ src/mod/applications/mod_spandsp/mod_spandsp_dsp.c | 12 ++++++------
+ 1 file changed, 6 insertions(+), 6 deletions(-)
+
+diff --git a/src/mod/applications/mod_spandsp/mod_spandsp_dsp.c b/src/mod/applications/mod_spandsp/mod_spandsp_dsp.c
+index 836808a48d..9558f42169 100644
+--- a/src/mod/applications/mod_spandsp/mod_spandsp_dsp.c
++++ b/src/mod/applications/mod_spandsp/mod_spandsp_dsp.c
+@@ -156,13 +156,13 @@ static int get_v18_mode(switch_core_session_t *session)
+ {
+ switch_channel_t *channel = switch_core_session_get_channel(session);
+ const char *var;
+- int r = V18_MODE_5BIT_4545;
++ int r = V18_MODE_WEITBRECHT_5BIT_4545;
+
+ if ((var = switch_channel_get_variable(channel, "v18_mode"))) {
+ if (!strcasecmp(var, "5BIT_45") || !strcasecmp(var, "baudot")) {
+- r = V18_MODE_5BIT_4545;
++ r = V18_MODE_WEITBRECHT_5BIT_4545;
+ } else if (!strcasecmp(var, "5BIT_50")) {
+- r = V18_MODE_5BIT_50;
++ r = V18_MODE_WEITBRECHT_5BIT_50;
+ } else if (!strcasecmp(var, "DTMF")) {
+ r = V18_MODE_DTMF;
+ } else if (!strcasecmp(var, "EDT")) {
+@@ -213,7 +213,7 @@ switch_status_t spandsp_tdd_send_session(switch_core_session_t *session, const c
+ return SWITCH_STATUS_FALSE;
+ }
+
+- tdd_state = v18_init(NULL, TRUE, get_v18_mode(session), V18_AUTOMODING_GLOBAL, put_text_msg, NULL);
++ tdd_state = v18_init(NULL, TRUE, get_v18_mode(session), V18_AUTOMODING_GLOBAL, put_text_msg, NULL, NULL, NULL);
+
+
+ v18_put(tdd_state, text, -1);
+@@ -260,7 +260,7 @@ switch_status_t spandsp_tdd_encode_session(switch_core_session_t *session, const
+ }
+
+ pvt->session = session;
+- pvt->tdd_state = v18_init(NULL, TRUE, get_v18_mode(session), V18_AUTOMODING_GLOBAL, put_text_msg, NULL);
++ pvt->tdd_state = v18_init(NULL, TRUE, get_v18_mode(session), V18_AUTOMODING_GLOBAL, put_text_msg, NULL, NULL, NULL);
+ pvt->head_lead = TDD_LEAD;
+
+ v18_put(pvt->tdd_state, text, -1);
+@@ -338,7 +338,7 @@ switch_status_t spandsp_tdd_decode_session(switch_core_session_t *session)
+ }
+
+ pvt->session = session;
+- pvt->tdd_state = v18_init(NULL, FALSE, get_v18_mode(session), V18_AUTOMODING_GLOBAL, put_text_msg, pvt);
++ pvt->tdd_state = v18_init(NULL, FALSE, get_v18_mode(session), V18_AUTOMODING_GLOBAL, put_text_msg, pvt, NULL, NULL);
+
+ if ((status = switch_core_media_bug_add(session, "spandsp_tdd_decode", NULL,
+ tdd_decode_callback, pvt, 0, SMBF_READ_REPLACE | SMBF_NO_PAUSE, &bug)) != SWITCH_STATUS_SUCCESS) {
+--
+2.39.2
+
diff --git a/containers/freeswitch/patches/freeswitch/0002-disable-some-cloud-bullshit-enable-some-other-bullsh.patch b/containers/freeswitch/patches/freeswitch/0002-disable-some-cloud-bullshit-enable-some-other-bullsh.patch
new file mode 100644
index 0000000..b977cfc
--- /dev/null
+++ b/containers/freeswitch/patches/freeswitch/0002-disable-some-cloud-bullshit-enable-some-other-bullsh.patch
@@ -0,0 +1,87 @@
+From 28b7f07ae806a975074e8d0087263f6965f24ae3 Mon Sep 17 00:00:00 2001
+From: nobody
+Date: Wed, 24 Jan 2024 21:20:07 -0800
+Subject: [PATCH] disable some cloud bullshit, enable some other bullshit
+
+---
+ build/modules.conf.in | 19 +++++++++----------
+ 1 file changed, 9 insertions(+), 10 deletions(-)
+
+diff --git a/build/modules.conf.in b/build/modules.conf.in
+index 7bf59e2acc..22ddc03845 100644
+--- a/build/modules.conf.in
++++ b/build/modules.conf.in
+@@ -3,12 +3,12 @@ applications/mod_av
+ #applications/mod_avmd
+ #applications/mod_bert
+ #applications/mod_blacklist
+-#applications/mod_callcenter
+-#applications/mod_cidlookup
++applications/mod_callcenter
++applications/mod_cidlookup
+ #applications/mod_cluechoo
+ applications/mod_commands
+ applications/mod_conference
+-#applications/mod_curl
++applications/mod_curl
+ #applications/mod_cv
+ applications/mod_db
+ #applications/mod_directory
+@@ -17,7 +17,7 @@ applications/mod_dptools
+ #applications/mod_easyroute
+ applications/mod_enum
+ applications/mod_esf
+-#applications/mod_esl
++applications/mod_esl
+ applications/mod_expr
+ applications/mod_fifo
+ #applications/mod_fsk
+@@ -28,7 +28,7 @@ applications/mod_httapi
+ #applications/mod_http_cache
+ #applications/mod_ladspa
+ #applications/mod_lcr
+-#applications/mod_memcache
++applications/mod_memcache
+ #applications/mod_mongo
+ #applications/mod_mp4
+ #applications/mod_mp4v2
+@@ -39,7 +39,6 @@ applications/mod_httapi
+ #applications/mod_rad_auth
+ #applications/mod_redis
+ #applications/mod_rss
+-applications/mod_signalwire
+ applications/mod_sms
+ #applications/mod_sms_flowroute
+ #applications/mod_snapshot
+@@ -121,7 +120,7 @@ formats/mod_native_file
+ formats/mod_png
+ #formats/mod_portaudio_stream
+ #formats/mod_shell_stream
+-#formats/mod_shout
++formats/mod_shout
+ formats/mod_sndfile
+ #formats/mod_ssml
+ formats/mod_tone_stream
+@@ -138,8 +137,8 @@ languages/mod_lua
+ #languages/mod_yaml
+ loggers/mod_console
+ #loggers/mod_graylog2
+-loggers/mod_logfile
+-loggers/mod_syslog
++#loggers/mod_logfile
++#loggers/mod_syslog
+ #loggers/mod_raven
+ #say/mod_say_de
+ say/mod_say_en
+@@ -162,7 +161,7 @@ say/mod_say_en
+ #timers/mod_posix_timer
+ #timers/mod_timerfd
+ xml_int/mod_xml_cdr
+-#xml_int/mod_xml_curl
++xml_int/mod_xml_curl
+ #xml_int/mod_xml_ldap
+ #xml_int/mod_xml_radius
+ xml_int/mod_xml_rpc
+--
+2.39.2
+