Add basic bot functionality

This commit is contained in:
Jack Kingsman
2026-01-26 21:44:11 -08:00
parent bbf6c63a95
commit ba92aa2342
23 changed files with 1577 additions and 565 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -13,8 +13,8 @@
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<script type="module" crossorigin src="/assets/index-DsqXtGVx.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cxcg9Mr6.css">
<script type="module" crossorigin src="/assets/index-DpuVn1aK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B-EWrA2Q.css">
</head>
<body>
<div id="root"></div>
+30 -2
View File
@@ -1,12 +1,12 @@
{
"name": "remoteterm-meshcore-frontend",
"version": "1.4.1",
"version": "1.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "remoteterm-meshcore-frontend",
"version": "1.4.1",
"version": "1.5.0",
"dependencies": {
"@michaelhart/meshcore-decoder": "^0.2.7",
"@radix-ui/react-checkbox": "^1.3.3",
@@ -15,6 +15,7 @@
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@types/prismjs": "^1.26.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3-force": "^3.0.0",
@@ -22,9 +23,11 @@
"lucide-react": "^0.562.0",
"meshcore-hashtag-cracker": "^1.6.0",
"nosleep.js": "^0.12.0",
"prismjs": "^1.30.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-leaflet": "^4.2.1",
"react-simple-code-editor": "^0.14.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
@@ -1521,6 +1524,12 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@types/prismjs": {
"version": "1.26.5",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"devOptional": true,
@@ -3953,6 +3962,15 @@
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"dev": true,
@@ -4071,6 +4089,16 @@
}
}
},
"node_modules/react-simple-code-editor": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.14.1.tgz",
"integrity": "sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==",
"license": "MIT",
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/react-style-singleton": {
"version": "2.2.3",
"license": "MIT",
+3
View File
@@ -22,6 +22,7 @@
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@types/prismjs": "^1.26.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3-force": "^3.0.0",
@@ -29,9 +30,11 @@
"lucide-react": "^0.562.0",
"meshcore-hashtag-cracker": "^1.6.0",
"nosleep.js": "^0.12.0",
"prismjs": "^1.30.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-leaflet": "^4.2.1",
"react-simple-code-editor": "^0.14.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
+136 -3
View File
@@ -1,4 +1,8 @@
import { useState, useEffect, useMemo } from 'react';
import Editor from 'react-simple-code-editor';
import { highlight, languages } from 'prismjs';
import 'prismjs/components/prism-python';
import 'prismjs/themes/prism-tomorrow.css';
import type {
AppSettings,
AppSettingsUpdate,
@@ -73,7 +77,7 @@ export function SettingsModal({
onRefreshAppSettings,
}: SettingsModalProps) {
// Tab state
type SettingsTab = 'radio' | 'identity' | 'serial' | 'database' | 'advertise';
type SettingsTab = 'radio' | 'identity' | 'serial' | 'database' | 'advertise' | 'bot';
const [activeTab, setActiveTab] = useState<SettingsTab>('radio');
// Radio config state
@@ -103,6 +107,31 @@ export function SettingsModal({
// Advertisement interval state
const [advertInterval, setAdvertInterval] = useState('0');
// Bot state
const DEFAULT_BOT_CODE = `def bot(sender_name, sender_key, message_text, is_dm, channel_key, channel_name, sender_timestamp, path):
"""
Process incoming messages and optionally return a reply.
Args:
sender_name: Display name of sender (may be None)
sender_key: 64-char hex public key (empty for channel msgs)
message_text: The message content
is_dm: True for direct messages, False for channel
channel_key: 32-char hex key for channels, None for DMs
channel_name: Channel name with hash (e.g. "#bot"), None for DMs
sender_timestamp: Sender's timestamp (unix seconds, may be None)
path: Hex-encoded routing path (may be None)
Returns:
None for no reply, or a string to send as reply
"""
# Example: Only respond in #bot channel to "!pling" command
if channel_name == "#bot" and "!pling" in message_text.lower():
return "[BOT] Plong!"
return None`;
const [botEnabled, setBotEnabled] = useState(false);
const [botCode, setBotCode] = useState(DEFAULT_BOT_CODE);
useEffect(() => {
if (config) {
setName(config.name);
@@ -121,6 +150,11 @@ export function SettingsModal({
setMaxRadioContacts(String(appSettings.max_radio_contacts));
setAutoDecryptOnAdvert(appSettings.auto_decrypt_dm_on_advert);
setAdvertInterval(String(appSettings.advert_interval));
setBotEnabled(appSettings.bot_enabled);
// Only overwrite bot code if user has saved custom code
if (appSettings.bot_code) {
setBotCode(appSettings.bot_code);
}
}
}, [appSettings]);
@@ -354,9 +388,25 @@ export function SettingsModal({
}
};
const handleSaveBotSettings = async () => {
setLoading(true);
setError('');
try {
await onSaveAppSettings({ bot_enabled: botEnabled, bot_code: botCode });
toast.success('Bot settings saved');
} catch (err) {
console.error('Failed to save bot settings:', err);
setError(err instanceof Error ? err.message : 'Failed to save');
toast.error('Failed to save settings');
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
<DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
<DialogContent className="sm:max-w-[50vw] sm:min-w-[500px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Radio & Settings</DialogTitle>
<DialogDescription className="sr-only">
@@ -366,6 +416,7 @@ export function SettingsModal({
{activeTab === 'serial' && 'View serial port connection and configure contact sync'}
{activeTab === 'database' && 'View database statistics and clean up old packets'}
{activeTab === 'advertise' && 'Send a flood advertisement to announce your presence'}
{activeTab === 'bot' && 'Configure automatic message bot with Python code'}
</DialogDescription>
</DialogHeader>
@@ -377,12 +428,13 @@ export function SettingsModal({
onValueChange={(v) => setActiveTab(v as SettingsTab)}
className="w-full"
>
<TabsList className="grid w-full grid-cols-5">
<TabsList className="grid w-full grid-cols-6">
<TabsTrigger value="radio">Radio</TabsTrigger>
<TabsTrigger value="identity">Identity</TabsTrigger>
<TabsTrigger value="serial">Serial</TabsTrigger>
<TabsTrigger value="database">Database</TabsTrigger>
<TabsTrigger value="advertise">Advertise</TabsTrigger>
<TabsTrigger value="bot">Bot</TabsTrigger>
</TabsList>
{/* Radio Config Tab */}
@@ -747,6 +799,87 @@ export function SettingsModal({
)}
</div>
</TabsContent>
{/* Bot Tab */}
<TabsContent value="bot" className="space-y-4 mt-4">
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-md">
<p className="text-sm text-yellow-500">
<strong>Security Warning:</strong> This feature executes arbitrary Python code on
the server. Only enable if you understand the security implications.
</p>
</div>
<div className="space-y-3">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={botEnabled}
onChange={(e) => setBotEnabled(e.target.checked)}
className="w-4 h-4 rounded border-input accent-primary"
/>
<span className="text-sm font-medium">Enable Message Bot</span>
</label>
</div>
<Separator />
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="bot-code">Bot Code (Python)</Label>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setBotCode(DEFAULT_BOT_CODE)}
disabled={!botEnabled}
>
Reset to Example
</Button>
</div>
<p className="text-xs text-muted-foreground">
Define a <code className="bg-muted px-1 rounded">bot()</code> function that
receives message data and optionally returns a reply string.
</p>
<div
className={`rounded-md border border-input bg-[#2d2d2d] overflow-auto h-64 ${!botEnabled ? 'opacity-50 pointer-events-none' : ''}`}
>
<Editor
value={botCode}
onValueChange={setBotCode}
highlight={(code) => highlight(code, languages.python, 'python')}
padding={12}
style={{
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
fontSize: 13,
minHeight: '100%',
}}
textareaId="bot-code"
disabled={!botEnabled}
/>
</div>
</div>
<div className="text-xs text-muted-foreground space-y-1">
<p>
<strong>Available:</strong> Standard Python libraries and any modules installed in
the server environment.
</p>
<p>
<strong>Limits:</strong> 10 second timeout, max 3 concurrent executions.
</p>
<p>
<strong>Note:</strong> Bot only responds to incoming messages, not your own. For
channel messages, <code>sender_key</code> is <code>None</code>.
</p>
</div>
{error && <div className="text-sm text-destructive">{error}</div>}
<Button onClick={handleSaveBotSettings} disabled={loading} className="w-full">
{loading ? 'Saving...' : 'Save Bot Settings'}
</Button>
</TabsContent>
</Tabs>
)}
</DialogContent>
+4
View File
@@ -134,6 +134,8 @@ export interface AppSettings {
last_message_times: Record<string, number>;
preferences_migrated: boolean;
advert_interval: number;
bot_enabled: boolean;
bot_code: string;
}
export interface AppSettingsUpdate {
@@ -141,6 +143,8 @@ export interface AppSettingsUpdate {
auto_decrypt_dm_on_advert?: boolean;
sidebar_sort_order?: 'recent' | 'alpha';
advert_interval?: number;
bot_enabled?: boolean;
bot_code?: string;
}
export interface MigratePreferencesRequest {