Skip to content

feat: Add Telnyx Voice alerts#7215

Open
LuvForAirplanes wants to merge 2 commits intolouislam:masterfrom
LuvForAirplanes:master
Open

feat: Add Telnyx Voice alerts#7215
LuvForAirplanes wants to merge 2 commits intolouislam:masterfrom
LuvForAirplanes:master

Conversation

@LuvForAirplanes
Copy link
Copy Markdown
Contributor

Summary

In this pull request, the following changes are made:

  • I added an Telnyx Voice API integration so that Telnyx can also call users with alerts
  • I used Claude Opus to assist me, especially with the Vue, as I am not super familiar. I have written quite a few Telnyx integrations before in private products.
Please follow this checklist to avoid unnecessary back and forth (click to expand)
  • ⚠️ If there are Breaking change (a fix or feature that alters existing functionality in a way that could cause issues) I have called them out
  • 🧠 I have disclosed any use of LLMs/AI in this contribution and reviewed all generated content.
    I understand that I am responsible for and able to explain every line of code I submit.
  • 🔍 Any UI changes adhere to visual style of this project.
  • 🛠️ I have self-reviewed and self-tested my code to ensure it works as expected.
  • 📝 I have commented my code, especially in hard-to-understand areas (e.g., using JSDoc for methods).
  • 🤖 I added or updated automated tests where appropriate.
  • 📄 Documentation updates are included (if applicable).
  • 🧰 Dependency updates are listed and explained.
  • ⚠️ CI passes and is green.

Screenshots for Visual Changes

Event Before After
UP N/A, Telnyx didn't exist image
DOWN N/A, Telnyx didn't exist image
Certificate-expiry N/A, Telnyx didn't exist image
Testing N/A, Telnyx didn't exist image

Copilot AI review requested due to automatic review settings March 27, 2026 15:30
Comment on lines +106 to +115
await axios.post(
`https://api.telnyx.com/v2/calls/${callControlId}/actions/speak`,
{
payload: pending.speechText,
payload_type: "text",
voice: "female",
language: "en-US",
},
{ headers }
);

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.
);
} else if (eventType === "call.speak.ended") {
// Hang up once the message has finished.
await axios.post(`https://api.telnyx.com/v2/calls/${callControlId}/actions/hangup`, {}, { headers });

Check failure

Code scanning / CodeQL

Server-side request forgery Critical

The
URL
of this request depends on a
user-provided value
.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Telnyx Voice notification channel that places outbound calls via Telnyx Call Control and speaks the alert message when the call is answered, including UI configuration and a public webhook for call events.

Changes:

  • Added UI form + i18n strings for configuring Telnyx Voice (App ID, base URL, optional speech template).
  • Registered a new telnyxVoice notification type in the frontend and backend provider list.
  • Added a public webhook endpoint and a new server-side Telnyx Voice provider to manage call lifecycle (speak/hangup).

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/lang/en.json Adds translation strings for Telnyx Voice configuration fields and help text.
src/components/notifications/index.js Registers the new TelnyxVoice.vue notification form component.
src/components/notifications/TelnyxVoice.vue New Vue form for Telnyx Voice provider configuration.
src/components/NotificationDialog.vue Adds “Telnyx Voice” and renames existing Telnyx label to “Telnyx SMS”.
server/routers/api-router.js Adds a public Telnyx call-control webhook callback route.
server/notification.js Registers the new Telnyx Voice notification provider.
server/notification-providers/telnyx-voice.js Implements Telnyx Voice call placement + webhook handling (speak/hangup) with in-memory pending call state.

notification.telnyxVoiceText && notification.telnyxVoiceText.trim()
? notification.telnyxVoiceText
: "{kumaMessage}";
const speechText = templateText.replace("{kumaMessage}", msg);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The helptext says {kumaMessage} can be used anywhere in the text, but String.prototype.replace will only replace the first occurrence. Use a global replacement (e.g., replaceAll or a global regex) so multiple placeholders are substituted correctly.

Suggested change
const speechText = templateText.replace("{kumaMessage}", msg);
const speechText = templateText.replace(/{kumaMessage}/g, msg);

Copilot uses AI. Check for mistakes.
Comment on lines +640 to +656
/**
* Public webhook endpoint for Telnyx Voice Call Control events.
* Telnyx posts the events here (call.answered, call.speak.ended) so that
* Uptime Kuma can send the speak and hangup commands.
* No authentication is applied because Telnyx servers must reach this
* endpoint from the public internet
*/
router.post("/api/telnyx-voice-callback", async (request, response) => {
// Acknowledge immediately so Telnyx does not retry the delivery
response.sendStatus(200);

try {
await TelnyxVoice.handleWebhook(request.body);
} catch (e) {
log.error("telnyx-voice", "Webhook handling error: " + e.message);
}
});
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

This endpoint accepts unauthenticated requests from the public internet and directly triggers outbound Telnyx API actions (speak/hangup). Add webhook authenticity verification (e.g., validate Telnyx’s signature headers + timestamp/nonce per their webhook security docs) before calling handleWebhook, and consider passing request.headers into the handler so verification can occur there as well.

Copilot uses AI. Check for mistakes.
Comment on lines +648 to +654
// Acknowledge immediately so Telnyx does not retry the delivery
response.sendStatus(200);

try {
await TelnyxVoice.handleWebhook(request.body);
} catch (e) {
log.error("telnyx-voice", "Webhook handling error: " + e.message);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Responding 200 before processing means Telnyx will not retry delivery even if handleWebhook fails, reducing reliability (missed speak/hangup actions). Prefer returning success only after successful processing, or queue/persist the event for guaranteed processing (and return non-2xx on validation/processing failures so Telnyx can retry when appropriate).

Suggested change
// Acknowledge immediately so Telnyx does not retry the delivery
response.sendStatus(200);
try {
await TelnyxVoice.handleWebhook(request.body);
} catch (e) {
log.error("telnyx-voice", "Webhook handling error: " + e.message);
try {
await TelnyxVoice.handleWebhook(request.body);
// Acknowledge only after successful processing so Telnyx can retry on failures
response.sendStatus(200);
} catch (e) {
log.error("telnyx-voice", "Webhook handling error: " + e.message);
// Indicate failure so Telnyx may retry delivery
response.sendStatus(500);

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +116
const headers = {
"Content-Type": "application/json",
Authorization: "Bearer " + pending.apiKey,
};

if (eventType === "call.answered") {
// Speak the alert message via text-to-speech.
await axios.post(
`https://api.telnyx.com/v2/calls/${callControlId}/actions/speak`,
{
payload: pending.speechText,
payload_type: "text",
voice: "female",
language: "en-US",
},
{ headers }
);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Outbound Telnyx API calls in send() apply getAxiosConfigWithProxy(...), but webhook-triggered calls here do not. In environments that require an HTTP(S) proxy, speak/hangup will fail. Refactor to reuse the same proxy-aware axios config in handleWebhook (e.g., share a helper that builds axios config from Settings / the provider base implementation).

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +72
TelnyxVoice.pendingCalls.set(callControlId, {
speechText,
apiKey: notification.telnyxApiKey,
});

// Safety clean-up: remove stale entry after 5 minutes in case
// the call never reaches a terminal state.
setTimeout(() => {
TelnyxVoice.pendingCalls.delete(callControlId);
}, 300000);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

Each call schedules a setTimeout that cannot be canceled when the call completes early, so high volume can accumulate many pending timers even after pendingCalls entries are removed. Store the timeout handle in the map entry and clearTimeout it when deleting the call state (e.g., on call.speak.ended), or centralize cleanup with a periodic sweeper.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants