Skip to content
Open
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
3 changes: 3 additions & 0 deletions interface/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,9 @@ export interface CreateMessagingInstanceRequest {
webhook_port?: number;
webhook_bind?: string;
webhook_auth_token?: string;
signal_http_url?: string;
signal_account?: string;
signal_dm_allowed_users?: string;
};
}

Expand Down
58 changes: 57 additions & 1 deletion interface/src/components/ChannelEditModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useState} from "react";
import {useMutation, useQuery, useQueryClient} from "@tanstack/react-query";
import {api, type PlatformStatus, type BindingInfo} from "@/api/client";
import {isValidE164, E164_ERROR_TEXT} from "@/lib/format";
import {
Button,
Input,
Expand All @@ -18,7 +19,7 @@ import {
import {PlatformIcon} from "@/lib/platformIcons";
import {TagInput} from "@/components/TagInput";

type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook";
type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook" | "signal";

interface ChannelEditModalProps {
platform: Platform;
Expand Down Expand Up @@ -168,6 +169,26 @@ export function ChannelEditModal({platform, name, status, open, onOpenChange}: C
twitch_client_secret: credentialInputs.twitch_client_secret?.trim(),
twitch_refresh_token: credentialInputs.twitch_refresh_token?.trim(),
};
} else if (platform === "signal") {
if (!credentialInputs.signal_http_url?.trim()) {
setMessage({text: "HTTP URL is required", type: "error"});
return;
}
if (!credentialInputs.signal_account?.trim()) {
setMessage({text: "Account phone number is required", type: "error"});
return;
}
// Basic E.164 validation
const account = credentialInputs.signal_account.trim();
if (!isValidE164(account)) {
setMessage({text: E164_ERROR_TEXT, type: "error"});
return;
}
request.platform_credentials = {
signal_http_url: credentialInputs.signal_http_url.trim(),
signal_account: account,
signal_dm_allowed_users: credentialInputs.signal_dm_allowed_users?.trim() || undefined,
};
}
saveCreds.mutate(request);
}
Expand Down Expand Up @@ -358,6 +379,41 @@ export function ChannelEditModal({platform, name, status, open, onOpenChange}: C
</>
)}

{platform === "signal" && (
<>
<div className="space-y-3">
<div>
<label className="mb-1.5 block text-sm font-medium text-ink-dull">HTTP URL</label>
<Input
size="lg"
value={credentialInputs.signal_http_url ?? ""}
onChange={(e) => setCredentialInputs({...credentialInputs, signal_http_url: e.target.value})}
placeholder="http://127.0.0.1:8686"
/>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-ink-dull">Account Phone Number</label>
<Input
size="lg"
value={credentialInputs.signal_account ?? ""}
onChange={(e) => setCredentialInputs({...credentialInputs, signal_account: e.target.value})}
placeholder="+1234567890"
onKeyDown={(e) => { if (e.key === "Enter") handleSaveCredentials(); }}
/>
<p className="mt-1 text-xs text-ink-faint">
Your Signal phone number in E.164 format (+ followed by 6-15 digits, first digit 1-9)
</p>
</div>
</div>
<p className="mt-3 text-xs text-ink-faint">
Need help?{" "}
<a href="https://docs.spacebot.sh/signal-setup" target="_blank" rel="noopener noreferrer" className="text-accent hover:underline">
Read the Signal setup docs &rarr;
</a>
</p>
</>
)}

{platform === "webhook" && (
<p className="text-sm text-ink-dull">
Webhook receiver requires no additional credentials.
Expand Down
103 changes: 101 additions & 2 deletions interface/src/components/ChannelSettingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import {
Toggle,
} from "@/ui";
import {PlatformIcon} from "@/lib/platformIcons";
import {isValidE164, E164_ERROR_TEXT} from "@/lib/format";
import {TagInput} from "@/components/TagInput";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faChevronDown, faPlus} from "@fortawesome/free-solid-svg-icons";

type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook";
type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook" | "signal";

const PLATFORM_LABELS: Record<Platform, string> = {
discord: "Discord",
Expand All @@ -36,13 +37,15 @@ const PLATFORM_LABELS: Record<Platform, string> = {
twitch: "Twitch",
email: "Email",
webhook: "Webhook",
signal: "Signal",
};

const DOC_LINKS: Partial<Record<Platform, string>> = {
discord: "https://docs.spacebot.sh/discord-setup",
slack: "https://docs.spacebot.sh/slack-setup",
telegram: "https://docs.spacebot.sh/telegram-setup",
twitch: "https://docs.spacebot.sh/twitch-setup",
signal: "https://docs.spacebot.sh/signal-setup",
};

// --- Platform Catalog (Left Column) ---
Expand All @@ -59,6 +62,7 @@ export function PlatformCatalog({onAddInstance}: PlatformCatalogProps) {
"twitch",
"email",
"webhook",
"signal",
];

const COMING_SOON = [
Expand Down Expand Up @@ -636,6 +640,57 @@ export function AddInstanceCard({platform, isDefault, onCancel, onCreated}: AddI
credentials.webhook_bind = credentialInputs.webhook_bind.trim();
if (credentialInputs.webhook_auth_token?.trim())
credentials.webhook_auth_token = credentialInputs.webhook_auth_token.trim();
} else if (platform === "signal") {
if (!credentialInputs.signal_http_url?.trim()) {
setMessage({text: "HTTP URL is required", type: "error"});
return;
}
if (!credentialInputs.signal_account?.trim()) {
setMessage({text: "Account phone number is required", type: "error"});
return;
}
// Basic E.164 validation (frontend) - match backend rules
const account = credentialInputs.signal_account.trim();
if (!isValidE164(account)) {
setMessage({
text: E164_ERROR_TEXT,
type: "error"
});
return;
}
credentials.signal_http_url = credentialInputs.signal_http_url.trim();
credentials.signal_account = account;
if (credentialInputs.signal_dm_allowed_users?.trim()) {
// Split, trim, and validate each phone number
const dm_users = credentialInputs.signal_dm_allowed_users
.split(',')
.map((s: string) => s.trim())
.filter((s: string) => s.length > 0);

// Validate each entry is E.164 format
const invalid_users: string[] = [];
const valid_users: string[] = [];

for (const user of dm_users) {
if (!isValidE164(user)) {
invalid_users.push(user);
} else {
valid_users.push(user);
}
}

if (invalid_users.length > 0) {
setMessage({
text: `Invalid phone number(s): ${invalid_users.join(', ')}. ${E164_ERROR_TEXT}`,
type: "error"
});
return;
}

if (valid_users.length > 0) {
credentials.signal_dm_allowed_users = valid_users.join(',');
}
}
}

if (!isDefault && !instanceName.trim()) {
Expand Down Expand Up @@ -925,7 +980,51 @@ export function AddInstanceCard({platform, isDefault, onCancel, onCreated}: AddI
</>
)}

{docLink && (
{platform === "signal" && (
<>
<div>
<label className="mb-1.5 block text-sm font-medium text-ink-dull">HTTP URL</label>
<Input
size="lg"
value={credentialInputs.signal_http_url ?? ""}
onChange={(e) => setCredentialInputs({...credentialInputs, signal_http_url: e.target.value})}
placeholder="http://127.0.0.1:8686"
onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }}
/>
<p className="mt-1 text-xs text-ink-faint">
URL of your signal-cli daemon (e.g., http://127.0.0.1:8686)
</p>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-ink-dull">Account Phone Number</label>
<Input
size="lg"
value={credentialInputs.signal_account ?? ""}
onChange={(e) => setCredentialInputs({...credentialInputs, signal_account: e.target.value})}
placeholder="+1234567890"
onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }}
/>
<p className="mt-1 text-xs text-ink-faint">
Your Signal phone number in E.164 format (+ followed by 6-15 digits, first digit 1-9)
</p>
</div>
<div>
<label className="mb-1.5 block text-sm font-medium text-ink-dull">DM Allowed Users (Optional)</label>
<Input
size="lg"
value={credentialInputs.signal_dm_allowed_users ?? ""}
onChange={(e) => setCredentialInputs({...credentialInputs, signal_dm_allowed_users: e.target.value})}
placeholder="+1234567890, +1987654321"
onKeyDown={(e) => { if (e.key === "Enter") handleSave(); }}
/>
<p className="mt-1 text-xs text-ink-faint">
Comma-separated list of phone numbers allowed to DM this bot (if empty, only the bot's own account can DM)
</p>
</div>
</>
)}

{docLink && (
<p className="text-xs text-ink-faint">
Need help?{" "}
<a href={docLink} target="_blank" rel="noopener noreferrer" className="text-accent hover:underline">
Expand Down
22 changes: 22 additions & 0 deletions interface/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,25 @@ export function platformColor(platform: string): string {
default: return "bg-gray-500/20 text-gray-400";
}
}

// E.164 Phone Number Validation
// Validates international phone numbers in format: + followed by country code and 6-15 digits
export const E164_REGEX = /^\+[1-9]\d{5,14}$/;

export const E164_ERROR_TEXT =
"Phone number must be in E.164 format: + followed by country code and 6-15 digits (e.g., +1234567890)";

export function isValidE164(phoneNumber: string): boolean {
return E164_REGEX.test(phoneNumber.trim());
}

export function validateE164(phoneNumber: string): { valid: boolean; error?: string } {
const trimmed = phoneNumber.trim();
if (!trimmed) {
return { valid: false, error: "Phone number is required" };
}
if (!E164_REGEX.test(trimmed)) {
return { valid: false, error: E164_ERROR_TEXT };
}
return { valid: true };
}
1 change: 1 addition & 0 deletions interface/src/lib/platformIcons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export function PlatformIcon({ platform, className = "text-ink-faint", size = "1
webhook: faLink,
email: faEnvelope,
whatsapp: faWhatsapp,
signal: faComment,
matrix: faComments,
imessage: faComment,
irc: faComments,
Expand Down
2 changes: 1 addition & 1 deletion interface/src/routes/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ function ThemePreview({ themeId }: { themeId: ThemeId }) {
);
}

type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook";
type Platform = "discord" | "slack" | "telegram" | "twitch" | "email" | "webhook" | "signal";

function ChannelsSection() {
const [expandedKey, setExpandedKey] = useState<string | null>(null);
Expand Down
2 changes: 2 additions & 0 deletions prompts/en/adapters/cron.md.j2
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ This is an automated scheduled task, not a live conversation. There is no human
- Be concise and data-driven in the final output. Lead with findings, not preamble. Include specifics — numbers, dates, names, links.
- Your entire text output will be delivered as-is to the configured channel. Write it like a finished report.
- If a worker fails or data is unavailable, say so clearly and include what you were able to gather.
- Use the `reply` tool for your primary output — it will be delivered to the configured destination automatically.
- Only use `send_message_to_another_channel` if the task explicitly requires sending to additional channels beyond the primary delivery target.
2 changes: 1 addition & 1 deletion prompts/en/fragments/conversation_context.md.j2
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ Platform: {{ platform }}
Server: {{ server_name }}
{%- endif %}
{%- if channel_name %}
Channel: #{{ channel_name }}
Channel: {{ channel_name }} ({{ platform }}{% if conversation_id %}, id: `{{ conversation_id }}`{% endif %})
{%- endif %}
Multiple users may be present. Each message is prefixed with [username].
2 changes: 2 additions & 0 deletions src/agent/channel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,7 @@ impl Channel {
&first.source,
server_name,
channel_name,
self.conversation_id.as_deref(),
)?);
}

Expand Down Expand Up @@ -1690,6 +1691,7 @@ impl Channel {
&message.source,
server_name,
channel_name,
self.conversation_id.as_deref(),
)?);
}

Expand Down
1 change: 1 addition & 0 deletions src/api/channels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,7 @@ pub(super) async fn inspect_prompt(
&info.platform,
server_name,
info.display_name.as_deref(),
Some(&info.id),
)
.ok()
}
Expand Down
Loading
Loading