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
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/bin/
**/obj/
**/.vs/
**/.idea/
74 changes: 74 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copilot instructions for Resgrid Relay

## Build, test, and run

- Full solution build (Windows required for the audio projects and test project):

```powershell
dotnet build "Resgrid Audio.sln"
```

- Full test suite:

```powershell
dotnet test "Resgrid Audio.sln"
```

- Run a single NUnit test method:

```powershell
dotnet test ".\Resgrid.Audio.Tests\Resgrid.Audio.Tests.csproj" --filter "FullyQualifiedName~Resgrid.Audio.Tests.SmtpDispatchAddressParserTests.TryParse_Should_Map_Department_Address_To_Department_Dispatch_Code"
```

- For SMTP-only work on a non-Windows agent, build the cross-platform console target directly:

```powershell
dotnet build ".\Resgrid.Audio.Relay.Console\Resgrid.Audio.Relay.Console.csproj" -f net10.0
```

- Console worker entrypoints from the repo root:

```powershell
dotnet run --project .\Resgrid.Audio.Relay.Console\Resgrid.Audio.Relay.Console.csproj -- run
dotnet run --project .\Resgrid.Audio.Relay.Console\Resgrid.Audio.Relay.Console.csproj -- setup
dotnet run --project .\Resgrid.Audio.Relay.Console\Resgrid.Audio.Relay.Console.csproj -- devices
dotnet run --project .\Resgrid.Audio.Relay.Console\Resgrid.Audio.Relay.Console.csproj -- monitor 0
```

- There is no dedicated lint, analyzer, or `dotnet format` command checked into this repository.

## High-level architecture

- `Resgrid.Audio.Relay.Console\Program.cs` is the live worker entrypoint. It selects `smtp` or `audio` mode, loads `appsettings.json` plus `RELAY_` environment variables into `RelayHostOptions`, and resolves relative paths from `AppContext.BaseDirectory`.

- SMTP mode is the current cross-platform relay path. The main flow is:

`Program.RunAsync` -> `SmtpRelayRunner` -> `RelayMailboxFilter` -> `RelayMessageStore` -> `SmtpDispatchAddressParser` / `DispatchListBuilder` -> `CallsApi`

`RelayMessageStore` parses the MIME message, derives a stable message id, suppresses duplicates through `ProcessedMessageStore`, optionally saves the raw `.eml` under `data\messages\`, creates the call, then uploads attachments as separate call files. `SmtpTelemetry` wraps the path with Serilog logging and optional Sentry/Countly reporting.

- Resgrid authentication and API access live in `Providers\Resgrid.Providers.ApiClient\V4`. `ResgridV4ApiClient` handles OIDC discovery, refresh-token exchange, token-cache persistence, and extraction of `CurrentUserId` from the access token `sub` claim for file uploads. `CallsApi` and `HealthApi` are the thin entrypoints used by the worker and audio pipeline.

- Audio mode is Windows-only and lives in `Resgrid.Audio.Core`. `AudioEvaluator` detects watcher triggers from DTMF/pure tones, `AudioProcessor` manages active watcher lifecycles and captured audio, and `ComService` translates watcher metadata into v4 dispatch lists, creates the call, then uploads the generated MP3 as a separate call file.

- `Resgrid.Audio.Relay` is a lightweight WPF monitor for selecting an input device and watching live audio metrics. It uses `AudioRecorder` directly; the actual relay/dispatch behavior stays in the console app plus `Resgrid.Audio.Core`.

- `Resgrid.Audio.Tests` covers the current seams: SMTP routing and telemetry, v4 dispatch-list formatting, and parts of the audio path. It targets `net10.0-windows`.

## Key conventions

- Prefer compiled code over legacy files that still exist in the tree. `Resgrid.Audio.Relay.Console.csproj` removes `Args\`, `Commands\`, `Data\`, `Models\`, and `ConsoleTable.cs` from compilation. `Providers\Resgrid.Providers.ApiClient.csproj` removes `V3\**\*.cs`. `Resgrid.Audio.Relay.csproj` removes `ViewModel\**\*.cs` and older resource scaffolding. Do not treat those files as the active implementation path unless you are intentionally reviving them.

- OIDC/v4 is the only live Resgrid integration. New API work should go through `ResgridV4ApiClient`, `CallsApi`, and the V4 models instead of the legacy username/password V3 code.

- Configuration is split by mode. Host-level settings live in `Resgrid.Audio.Relay.Console\appsettings.json` plus `RELAY_` environment variables. Audio watcher definitions live in `Resgrid.Audio.Relay.Console\settings.json`. Keep new file-based settings consistent with the existing `AppContext.BaseDirectory` path resolution.

- SMTP routing is domain-driven. Configured department domains become `DispatchCodeType.Department`; configured group domains become `DispatchCodeType.Group`. `DispatchListBuilder` emits pipe-delimited `PREFIX:CODE` tokens, with a configurable department prefix and `G` as the default group prefix.

- SMTP duplicate suppression is persisted in `data\processed-messages.json`. Raw message retention uses `data\messages\`. Preserve the rollback behavior that removes a duplicate-registration entry if call creation or attachment upload fails after registration.

- `Watcher.Type` is semantic: `1` means department dispatch and `2` means group dispatch. `Watcher.AdditionalCodes` is always treated as extra group dispatch codes. When `Config.Multiple` is `false`, simultaneous hits are merged into the first active watcher; when it is `true`, they create separate active watcher flows.

- The Windows audio path still depends on checked-in binary references from `References\` and `packages\NAudio.1.8.4\...`. Avoid cleanup refactors that replace those references without revalidating the audio pipeline.

- `.editorconfig` uses tabs for `.cs` and CRLF line endings. Tests use NUnit with FluentAssertions.
25 changes: 25 additions & 0 deletions .github/workflows/auto-approve.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Auto approve

on:
issue_comment:
types:
- created

jobs:
auto-approve:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/github-script@v9.0.0
name: Approve LGTM Review
if: github.event.issue.pull_request && github.event.comment.user.login == 'ucswift' && contains(github.event.comment.body, 'Approve')
with:
script: |
github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
event: 'APPROVE',
body: 'This PR is approved.'
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
248 changes: 248 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# This workflow runs build and tests on every pull request and every direct push
# to master. It also publishes artifacts and creates a GitHub Release only after
# a pull request is successfully merged to master.
#
# Required Docker Hub secrets:
# - DOCKERHUB_USERNAME
# - DOCKERHUB_TOKEN

name: Build and publish

on:
push:
branches:
- master
- main
pull_request:
types: [opened, synchronize, reopened, closed]
Comment on lines +12 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Align main handling across the trigger and release gates.

This workflow subscribes to main, but the merged-PR path is still hardcoded to master. A PR merged into main will build on the separate push run, but this workflow’s publish/release jobs will never start. Either remove main from the trigger or widen the branch checks consistently.

Also applies to: 28-31, 59-59, 124-124, 197-197

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/dotnet.yml around lines 12 - 17, The workflow mixes 'main'
in the push trigger but still uses 'master' in later branch checks, so align
branch handling consistently: update the push.branches list and any hardcoded
branch checks (e.g., occurrences checking for 'master' in publish/release job
conditions or if-statements) to either remove 'main' from triggers or replace
every 'master' reference with 'main' (and add 'master' as needed) so the same
branch names are used across push, pull_request handling, and release/publish
job conditions.


env:
DOTNET_VERSION: "10.0.x"
RELEASE_VERSION: "2.0.${{ github.run_number }}"
DOCKER_IMAGE_NAME: "resgridrelay"
DOCKER_IMAGE: "resgridllc/resgridrelay"

jobs:
build-and-test:
name: Build and test
if: |
github.event_name == 'push' ||
github.event.action != 'closed' ||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'master')
runs-on: windows-latest

permissions:
contents: read

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.action == 'closed' && github.event.pull_request.merge_commit_sha || github.sha }}

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Restore dependencies
run: dotnet restore "Resgrid Audio.sln"

- name: Build
run: dotnet build "Resgrid Audio.sln" --configuration Release --no-restore

- name: Test
run: dotnet test "Resgrid Audio.sln" --configuration Release --no-build --verbosity normal

publish-apps:
name: Publish app assets
needs: build-and-test
if: github.event.action == 'closed' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'master'
runs-on: windows-latest

permissions:
contents: read

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }}

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}

- name: Restore dependencies
run: dotnet restore "Resgrid Audio.sln"

- name: Publish application outputs
shell: pwsh
run: |
$publishRoot = Join-Path $env:RUNNER_TEMP "publish"
$consoleLinux = Join-Path $publishRoot "relay-console-net10.0"
$consoleWindows = Join-Path $publishRoot "relay-console-net10.0-windows"
$monitorWindows = Join-Path $publishRoot "relay-monitor-net10.0-windows"

dotnet publish ".\Resgrid.Audio.Relay.Console\Resgrid.Audio.Relay.Console.csproj" --configuration Release --framework net10.0 --no-restore --output $consoleLinux /p:UseAppHost=false
dotnet publish ".\Resgrid.Audio.Relay.Console\Resgrid.Audio.Relay.Console.csproj" --configuration Release --framework net10.0-windows --no-restore --output $consoleWindows
dotnet publish ".\Resgrid.Audio.Relay\Resgrid.Audio.Relay.csproj" --configuration Release --framework net10.0-windows --no-restore --output $monitorWindows

- name: Package release assets
shell: pwsh
run: |
$publishRoot = Join-Path $env:RUNNER_TEMP "publish"
$assetRoot = Join-Path $env:RUNNER_TEMP "release-assets"

New-Item -ItemType Directory -Path $assetRoot -Force | Out-Null

$assets = @(
@{ Source = Join-Path $publishRoot "relay-console-net10.0"; File = Join-Path $assetRoot "resgridrelay-console-net10.0-$env:RELEASE_VERSION.zip" },
@{ Source = Join-Path $publishRoot "relay-console-net10.0-windows"; File = Join-Path $assetRoot "resgridrelay-console-net10.0-windows-$env:RELEASE_VERSION.zip" },
@{ Source = Join-Path $publishRoot "relay-monitor-net10.0-windows"; File = Join-Path $assetRoot "resgridrelay-monitor-net10.0-windows-$env:RELEASE_VERSION.zip" }
)

foreach ($asset in $assets)
{
if (Test-Path $asset.File)
{
Remove-Item $asset.File -Force
}

Compress-Archive -Path (Join-Path $asset.Source '*') -DestinationPath $asset.File
}

- name: Upload release assets
uses: actions/upload-artifact@v4
with:
name: release-assets
path: ${{ runner.temp }}\release-assets\*.zip
if-no-files-found: error

docker-build-and-push:
name: Publish Docker image
needs: build-and-test
if: github.event.action == 'closed' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'master'
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha || github.sha }}

- name: Validate Docker Hub configuration
shell: bash
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
run: |
if [ -z "$DOCKERHUB_USERNAME" ]; then
echo "DOCKERHUB_USERNAME secret is required."
exit 1
fi

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_IMAGE }}
tags: |
type=raw,value=${{ env.RELEASE_VERSION }}
type=raw,value=latest
type=sha,prefix=sha-

- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Create Docker release metadata
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/docker-release"
{
echo "Docker image:"
echo "${{ env.DOCKER_IMAGE }}"
echo
echo "Tags:"
printf '%s\n' "${{ steps.meta.outputs.tags }}"
} > "$RUNNER_TEMP/docker-release/docker-image-tags.txt"

- name: Upload Docker release metadata
uses: actions/upload-artifact@v4
with:
name: docker-release-metadata
path: ${{ runner.temp }}/docker-release/docker-image-tags.txt
if-no-files-found: error

github-release:
name: Create GitHub release
needs: [publish-apps, docker-build-and-push]
if: github.event.action == 'closed' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'master'
runs-on: ubuntu-latest

permissions:
contents: write
pull-requests: read

steps:
- name: Download app release assets
uses: actions/download-artifact@v4
with:
name: release-assets
path: ${{ runner.temp }}/release-assets

- name: Download Docker release metadata
uses: actions/download-artifact@v4
with:
name: docker-release-metadata
path: ${{ runner.temp }}/docker-release

- name: Get merged PR info
id: pr-info
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const body = (context.payload.pull_request.body || '')
.replace(/##\s*Summary by CodeRabbit[\s\S]*/i, '')
.trim() || 'No release notes provided.';
const notes = [
body,
'',
'## Docker image',
`- \`${{ env.DOCKER_IMAGE }}:${{ env.RELEASE_VERSION }}\``,
`- \`${{ env.DOCKER_IMAGE }}:latest\``
].join('\n');
fs.writeFileSync('release_notes.md', notes);

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ env.RELEASE_VERSION }}
name: ${{ env.RELEASE_VERSION }}
body_path: release_notes.md
target_commitish: ${{ github.event.pull_request.merge_commit_sha || github.sha }}
files: |
${{ runner.temp }}/release-assets/*.zip
${{ runner.temp }}/docker-release/docker-image-tags.txt
draft: false
prerelease: false
make_latest: true
fail_on_unmatched_files: true
18 changes: 18 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

COPY . .
RUN dotnet restore "Resgrid.Audio.Relay.Console/Resgrid.Audio.Relay.Console.csproj" -p:TargetFramework=net10.0
RUN dotnet publish "Resgrid.Audio.Relay.Console/Resgrid.Audio.Relay.Console.csproj" -c Release -f net10.0 -o /app/publish /p:UseAppHost=false

FROM mcr.microsoft.com/dotnet/runtime:10.0 AS final
WORKDIR /app

COPY --from=build /app/publish .

ENV RELAY_Mode=smtp
ENV RELAY_Smtp__Port=2525

EXPOSE 2525

ENTRYPOINT ["dotnet", "Resgrid.Audio.Relay.Console.dll"]
Loading
Loading