Add comprehensive CLI tool with interactive shell mode

- Created miku-cli.py: Full-featured CLI for Miku bot API
- Added interactive shell mode for continuous command execution
- Implemented all API endpoints: status, mood, autonomous, DM, blocking, profile pictures
- Consistent hyphenated command naming across shell and regular modes
- Created API_REFERENCE.md: Complete API endpoint documentation
- Created CLI_README.md: User guide with examples and usage instructions
- Error handling and user-friendly output formatting
This commit is contained in:
2025-12-10 10:02:34 +02:00
parent 711101816a
commit a7f3a0a0ee
3 changed files with 1549 additions and 0 deletions

460
API_REFERENCE.md Normal file
View File

@@ -0,0 +1,460 @@
# Miku Discord Bot API Reference
The Miku bot exposes a FastAPI REST API on port 3939 for controlling and monitoring the bot.
## Base URL
```
http://localhost:3939
```
## API Endpoints
### 📊 Status & Information
#### `GET /status`
Get current bot status and overview.
**Response:**
```json
{
"status": "online",
"mood": "neutral",
"servers": 2,
"active_schedulers": 2,
"server_moods": {
"123456789": "bubbly",
"987654321": "excited"
}
}
```
#### `GET /logs`
Get the last 100 lines of bot logs.
**Response:** Plain text log output
#### `GET /prompt`
Get the last full prompt sent to the LLM.
**Response:**
```json
{
"prompt": "Last prompt text..."
}
```
---
### 😊 Mood Management
#### `GET /mood`
Get current DM mood.
**Response:**
```json
{
"mood": "neutral",
"description": "Mood description text..."
}
```
#### `POST /mood`
Set DM mood.
**Request Body:**
```json
{
"mood": "bubbly"
}
```
**Response:**
```json
{
"status": "ok",
"new_mood": "bubbly"
}
```
#### `POST /mood/reset`
Reset DM mood to neutral.
#### `POST /mood/calm`
Calm Miku down (set to neutral).
#### `GET /servers/{guild_id}/mood`
Get mood for specific server.
#### `POST /servers/{guild_id}/mood`
Set mood for specific server.
**Request Body:**
```json
{
"mood": "excited"
}
```
#### `POST /servers/{guild_id}/mood/reset`
Reset server mood to neutral.
#### `GET /servers/{guild_id}/mood/state`
Get complete mood state for server.
#### `GET /moods/available`
List all available moods.
**Response:**
```json
{
"moods": {
"neutral": "😊",
"bubbly": "🥰",
"excited": "🤩",
"sleepy": "😴",
...
}
}
```
---
### 😴 Sleep Management
#### `POST /sleep`
Force Miku to sleep.
#### `POST /wake`
Wake Miku up.
#### `POST /bedtime?guild_id={guild_id}`
Send bedtime reminder. If `guild_id` is provided, sends only to that server.
---
### 🤖 Autonomous Actions
#### `POST /autonomous/general?guild_id={guild_id}`
Trigger autonomous general message.
#### `POST /autonomous/engage?guild_id={guild_id}`
Trigger autonomous user engagement.
#### `POST /autonomous/tweet?guild_id={guild_id}`
Trigger autonomous tweet sharing.
#### `POST /autonomous/reaction?guild_id={guild_id}`
Trigger autonomous reaction to a message.
#### `POST /autonomous/custom?guild_id={guild_id}`
Send custom autonomous message.
**Request Body:**
```json
{
"prompt": "Say something funny about cats"
}
```
#### `GET /autonomous/stats`
Get autonomous engine statistics for all servers.
**Response:** Detailed stats including message counts, activity, mood profiles, etc.
#### `GET /autonomous/v2/stats/{guild_id}`
Get autonomous V2 stats for specific server.
#### `GET /autonomous/v2/check/{guild_id}`
Check if autonomous action should happen for server.
#### `GET /autonomous/v2/status`
Get autonomous V2 status across all servers.
---
### 🌐 Server Management
#### `GET /servers`
List all configured servers.
**Response:**
```json
{
"servers": [
{
"guild_id": 123456789,
"guild_name": "My Server",
"autonomous_channel_id": 987654321,
"autonomous_channel_name": "general",
"bedtime_channel_ids": [111111111],
"enabled_features": ["autonomous", "bedtime"]
}
]
}
```
#### `POST /servers`
Add a new server configuration.
**Request Body:**
```json
{
"guild_id": 123456789,
"guild_name": "My Server",
"autonomous_channel_id": 987654321,
"autonomous_channel_name": "general",
"bedtime_channel_ids": [111111111],
"enabled_features": ["autonomous", "bedtime"]
}
```
#### `DELETE /servers/{guild_id}`
Remove server configuration.
#### `PUT /servers/{guild_id}`
Update server configuration.
#### `POST /servers/{guild_id}/bedtime-range`
Set bedtime range for server.
#### `POST /servers/{guild_id}/memory`
Update server memory/context.
#### `GET /servers/{guild_id}/memory`
Get server memory/context.
#### `POST /servers/repair`
Repair server configurations.
---
### 💬 DM Management
#### `GET /dms/users`
List all users with DM history.
**Response:**
```json
{
"users": [
{
"user_id": "123456789",
"username": "User#1234",
"total_messages": 42,
"last_message_date": "2025-12-10T12:34:56",
"is_blocked": false
}
]
}
```
#### `GET /dms/users/{user_id}`
Get details for specific user.
#### `GET /dms/users/{user_id}/conversations`
Get conversation history for user.
#### `GET /dms/users/{user_id}/search?query={query}`
Search user's DM history.
#### `GET /dms/users/{user_id}/export`
Export user's DM history.
#### `DELETE /dms/users/{user_id}`
Delete user's DM data.
#### `POST /dm/{user_id}/custom`
Send custom DM (LLM-generated).
**Request Body:**
```json
{
"prompt": "Ask about their day"
}
```
#### `POST /dm/{user_id}/manual`
Send manual DM (direct message).
**Form Data:**
- `message`: Message text
#### `GET /dms/blocked-users`
List blocked users.
#### `POST /dms/users/{user_id}/block`
Block a user.
#### `POST /dms/users/{user_id}/unblock`
Unblock a user.
#### `POST /dms/users/{user_id}/conversations/{conversation_id}/delete`
Delete specific conversation.
#### `POST /dms/users/{user_id}/conversations/delete-all`
Delete all conversations for user.
#### `POST /dms/users/{user_id}/delete-completely`
Completely delete user data.
---
### 📊 DM Analysis
#### `POST /dms/analysis/run`
Run analysis on all DM conversations.
#### `POST /dms/users/{user_id}/analyze`
Analyze specific user's DMs.
#### `GET /dms/analysis/reports`
Get all analysis reports.
#### `GET /dms/analysis/reports/{user_id}`
Get analysis report for specific user.
---
### 🖼️ Profile Picture Management
#### `POST /profile-picture/change?guild_id={guild_id}`
Change profile picture. Optionally upload custom image.
**Form Data:**
- `file`: Image file (optional)
**Response:**
```json
{
"status": "ok",
"message": "Profile picture changed successfully",
"source": "danbooru",
"metadata": {
"url": "https://...",
"tags": ["hatsune_miku", "...]
}
}
```
#### `GET /profile-picture/metadata`
Get current profile picture metadata.
#### `POST /profile-picture/restore-fallback`
Restore original fallback profile picture.
---
### 🎨 Role Color Management
#### `POST /role-color/custom`
Set custom role color.
**Form Data:**
- `hex_color`: Hex color code (e.g., "#FF0000")
#### `POST /role-color/reset-fallback`
Reset role color to fallback (#86cecb).
---
### 💬 Conversation Management
#### `GET /conversation/{user_id}`
Get conversation history for user.
#### `POST /conversation/reset`
Reset conversation history.
**Request Body:**
```json
{
"user_id": "123456789"
}
```
---
### 📨 Manual Messaging
#### `POST /manual/send`
Send manual message to channel.
**Form Data:**
- `message`: Message text
- `channel_id`: Channel ID
- `files`: Files to attach (optional, multiple)
---
### 🎁 Figurine Notifications
#### `GET /figurines/subscribers`
List figurine subscribers.
#### `POST /figurines/subscribers`
Add figurine subscriber.
#### `DELETE /figurines/subscribers/{user_id}`
Remove figurine subscriber.
#### `POST /figurines/send_now`
Send figurine notification to all subscribers.
#### `POST /figurines/send_to_user`
Send figurine notification to specific user.
---
### 🖼️ Image Generation
#### `POST /image/generate`
Generate image using image generation service.
#### `GET /image/status`
Get image generation service status.
#### `POST /image/test-detection`
Test face detection on uploaded image.
---
### 😀 Message Reactions
#### `POST /messages/react`
Add reaction to a message.
**Request Body:**
```json
{
"channel_id": "123456789",
"message_id": "987654321",
"emoji": "😊"
}
```
---
## Error Responses
All endpoints return errors in the following format:
```json
{
"status": "error",
"message": "Error description"
}
```
HTTP status codes:
- `200` - Success
- `400` - Bad request
- `404` - Not found
- `500` - Internal server error
## Authentication
Currently, the API does not require authentication. It's designed to run on localhost within a Docker network.
## Rate Limiting
No rate limiting is currently implemented.

347
CLI_README.md Normal file
View File

@@ -0,0 +1,347 @@
# Miku CLI - Command Line Interface
A powerful command-line interface for controlling and monitoring the Miku Discord bot.
## Installation
1. Make the script executable:
```bash
chmod +x miku-cli.py
```
2. Install dependencies:
```bash
pip install requests
```
3. (Optional) Create a symlink for easier access:
```bash
sudo ln -s $(pwd)/miku-cli.py /usr/local/bin/miku
```
## Quick Start
```bash
# Check bot status
./miku-cli.py status
# Get current mood
./miku-cli.py mood --get
# Set mood to bubbly
./miku-cli.py mood --set bubbly
# List available moods
./miku-cli.py mood --list
# Trigger autonomous message
./miku-cli.py autonomous general
# List servers
./miku-cli.py servers
# View logs
./miku-cli.py logs
```
## Configuration
By default, the CLI connects to `http://localhost:3939`. To use a different URL:
```bash
./miku-cli.py --url http://your-server:3939 status
```
## Commands
### Status & Information
```bash
# Get bot status
./miku-cli.py status
# View recent logs
./miku-cli.py logs
# Get last LLM prompt
./miku-cli.py prompt
```
### Mood Management
```bash
# Get current DM mood
./miku-cli.py mood --get
# Get server mood
./miku-cli.py mood --get --server 123456789
# Set mood
./miku-cli.py mood --set bubbly
./miku-cli.py mood --set excited --server 123456789
# Reset mood to neutral
./miku-cli.py mood --reset
./miku-cli.py mood --reset --server 123456789
# List available moods
./miku-cli.py mood --list
```
### Sleep Management
```bash
# Put Miku to sleep
./miku-cli.py sleep
# Wake Miku up
./miku-cli.py wake
# Send bedtime reminder
./miku-cli.py bedtime
./miku-cli.py bedtime --server 123456789
```
### Autonomous Actions
```bash
# Trigger general autonomous message
./miku-cli.py autonomous general
./miku-cli.py autonomous general --server 123456789
# Trigger user engagement
./miku-cli.py autonomous engage
./miku-cli.py autonomous engage --server 123456789
# Share a tweet
./miku-cli.py autonomous tweet
./miku-cli.py autonomous tweet --server 123456789
# Trigger reaction
./miku-cli.py autonomous reaction
./miku-cli.py autonomous reaction --server 123456789
# Send custom autonomous message
./miku-cli.py autonomous custom --prompt "Tell a joke about programming"
./miku-cli.py autonomous custom --prompt "Say hello" --server 123456789
# Get autonomous stats
./miku-cli.py autonomous stats
```
### Server Management
```bash
# List all configured servers
./miku-cli.py servers
```
### DM Management
```bash
# List users with DM history
./miku-cli.py dm-users
# Send custom DM (LLM-generated)
./miku-cli.py dm-custom 123456789 "Ask them how their day was"
# Send manual DM (direct message)
./miku-cli.py dm-manual 123456789 "Hello! How are you?"
# Block a user
./miku-cli.py block 123456789
# Unblock a user
./miku-cli.py unblock 123456789
# List blocked users
./miku-cli.py blocked-users
```
### Profile Picture
```bash
# Change profile picture (search Danbooru based on mood)
./miku-cli.py change-pfp
# Change to custom image
./miku-cli.py change-pfp --image /path/to/image.png
# Change for specific server mood
./miku-cli.py change-pfp --server 123456789
# Get current profile picture metadata
./miku-cli.py pfp-metadata
```
### Conversation Management
```bash
# Reset conversation history for a user
./miku-cli.py reset-conversation 123456789
```
### Manual Messaging
```bash
# Send message to channel
./miku-cli.py send 987654321 "Hello everyone!"
# Send message with file attachments
./miku-cli.py send 987654321 "Check this out!" --files image.png document.pdf
```
## Available Moods
- 😊 neutral
- 🥰 bubbly
- 🤩 excited
- 😴 sleepy
- 😡 angry
- 🙄 irritated
- 😏 flirty
- 💕 romantic
- 🤔 curious
- 😳 shy
- 🤪 silly
- 😢 melancholy
- 😤 serious
- 💤 asleep
## Examples
### Morning Routine
```bash
# Wake up Miku
./miku-cli.py wake
# Set a bubbly mood
./miku-cli.py mood --set bubbly
# Send a general message to all servers
./miku-cli.py autonomous general
# Change profile picture to match mood
./miku-cli.py change-pfp
```
### Server-Specific Control
```bash
# Get server list
./miku-cli.py servers
# Set mood for specific server
./miku-cli.py mood --set excited --server 123456789
# Trigger engagement on that server
./miku-cli.py autonomous engage --server 123456789
```
### DM Interaction
```bash
# List users
./miku-cli.py dm-users
# Send custom message
./miku-cli.py dm-custom 123456789 "Ask them about their favorite anime"
# If user is spamming, block them
./miku-cli.py block 123456789
```
### Monitoring
```bash
# Check status
./miku-cli.py status
# View logs
./miku-cli.py logs
# Get autonomous stats
./miku-cli.py autonomous stats
# Check last prompt
./miku-cli.py prompt
```
## Output Format
The CLI uses emoji and colored output for better readability:
- ✅ Success messages
- ❌ Error messages
- 😊 Mood indicators
- 🌐 Server information
- 💬 DM information
- 📊 Statistics
- 🖼️ Media information
## Scripting
The CLI is designed to be script-friendly:
```bash
#!/bin/bash
# Morning routine script
./miku-cli.py wake
./miku-cli.py mood --set bubbly
./miku-cli.py autonomous general
# Wait 5 minutes
sleep 300
# Engage users
./miku-cli.py autonomous engage
```
## Error Handling
The CLI exits with status code 1 on errors and 0 on success, making it suitable for use in scripts:
```bash
if ./miku-cli.py mood --set bubbly; then
echo "Mood set successfully"
else
echo "Failed to set mood"
fi
```
## API Reference
For complete API documentation, see [API_REFERENCE.md](./API_REFERENCE.md).
## Troubleshooting
### Connection Refused
If you get "Connection refused" errors:
1. Check that the bot API is running on port 3939
2. Verify the URL with `--url` parameter
3. Check Docker container status: `docker-compose ps`
### Permission Denied
Make the script executable:
```bash
chmod +x miku-cli.py
```
### Import Errors
Install required dependencies:
```bash
pip install requests
```
## Future Enhancements
Planned features:
- Configuration file support (~/.miku-cli.conf)
- Interactive mode
- Tab completion
- Color output control
- JSON output mode for scripting
- Batch operations
- Watch mode for real-time monitoring
## Contributing
Feel free to extend the CLI with additional commands and features!

742
miku-cli.py Normal file
View File

@@ -0,0 +1,742 @@
#!/usr/bin/env python3
"""
Miku Discord Bot CLI
A command-line interface for interacting with the Miku Discord bot API.
"""
import requests
import argparse
import sys
import json
import shlex
from typing import Optional, Dict, Any
from pathlib import Path
# Default API base URL
DEFAULT_API_URL = "http://192.168.1.2:3939"
class MikuCLI:
def __init__(self, base_url: str = DEFAULT_API_URL):
self.base_url = base_url.rstrip('/')
def _get(self, endpoint: str) -> Dict[str, Any]:
"""Make a GET request to the API."""
try:
response = requests.get(f"{self.base_url}{endpoint}")
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"❌ Error: {e}")
sys.exit(1)
def _post(self, endpoint: str, data: Optional[Dict] = None, json_data: Optional[Dict] = None, files: Optional[Dict] = None) -> Dict[str, Any]:
"""Make a POST request to the API."""
try:
response = requests.post(f"{self.base_url}{endpoint}", data=data, json=json_data, files=files)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"❌ Error: {e}")
if hasattr(e.response, 'text'):
print(f"Response: {e.response.text}")
sys.exit(1)
def _delete(self, endpoint: str) -> Dict[str, Any]:
"""Make a DELETE request to the API."""
try:
response = requests.delete(f"{self.base_url}{endpoint}")
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"❌ Error: {e}")
sys.exit(1)
def _put(self, endpoint: str, json_data: Dict) -> Dict[str, Any]:
"""Make a PUT request to the API."""
try:
response = requests.put(f"{self.base_url}{endpoint}", json=json_data)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"❌ Error: {e}")
sys.exit(1)
# ========== Status & Info ==========
def get_status(self):
"""Get bot status."""
result = self._get("/status")
print(f"✅ Bot Status: {result['status']}")
print(f"😊 DM Mood: {result['mood']}")
print(f"🌐 Servers: {result['servers']}")
print(f"📊 Active Schedulers: {result['active_schedulers']}")
if result.get('server_moods'):
print("\n📋 Server Moods:")
for guild_id, mood in result['server_moods'].items():
print(f" Server {guild_id}: {mood}")
def get_logs(self):
"""Get recent bot logs."""
result = self._get("/logs")
print(result)
def get_prompt(self):
"""Get last prompt sent to LLM."""
result = self._get("/prompt")
print(f"Last prompt:\n{result['prompt']}")
# ========== Mood Management ==========
def get_mood(self, guild_id: Optional[int] = None):
"""Get current mood (global or per-server)."""
if guild_id:
result = self._get(f"/servers/{guild_id}/mood")
print(f"😊 Server {guild_id} Mood: {result['mood']}")
print(f"📝 Description: {result['description']}")
else:
result = self._get("/mood")
print(f"😊 DM Mood: {result['mood']}")
print(f"📝 Description: {result['description']}")
def set_mood(self, mood: str, guild_id: Optional[int] = None):
"""Set mood (global or per-server)."""
if guild_id:
result = self._post(f"/servers/{guild_id}/mood", json_data={"mood": mood})
else:
result = self._post("/mood", json_data={"mood": mood})
if result['status'] == 'ok':
print(f"✅ Mood set to: {result['new_mood']}")
else:
print(f"❌ Error: {result.get('message', 'Unknown error')}")
def reset_mood(self, guild_id: Optional[int] = None):
"""Reset mood to neutral."""
if guild_id:
result = self._post(f"/servers/{guild_id}/mood/reset")
else:
result = self._post("/mood/reset")
if result['status'] == 'ok':
print(f"✅ Mood reset to neutral")
else:
print(f"❌ Error: {result.get('message', 'Unknown error')}")
def list_moods(self):
"""List available moods."""
result = self._get("/moods/available")
print("Available moods:")
for mood, emoji in result['moods'].items():
print(f" {emoji} {mood}")
# ========== Sleep Management ==========
def sleep(self):
"""Put Miku to sleep."""
result = self._post("/sleep")
print(f"{result['message']}")
def wake(self):
"""Wake Miku up."""
result = self._post("/wake")
print(f"{result['message']}")
# ========== Autonomous Actions ==========
def autonomous_general(self, guild_id: Optional[int] = None):
"""Trigger general autonomous message."""
if guild_id:
result = self._post(f"/autonomous/general?guild_id={guild_id}")
else:
result = self._post("/autonomous/general")
print(f"{result['message']}")
def autonomous_engage(self, guild_id: Optional[int] = None):
"""Trigger autonomous user engagement."""
if guild_id:
result = self._post(f"/autonomous/engage?guild_id={guild_id}")
else:
result = self._post("/autonomous/engage")
print(f"{result['message']}")
def autonomous_tweet(self, guild_id: Optional[int] = None):
"""Trigger autonomous tweet sharing."""
if guild_id:
result = self._post(f"/autonomous/tweet?guild_id={guild_id}")
else:
result = self._post("/autonomous/tweet")
print(f"{result['message']}")
def autonomous_reaction(self, guild_id: Optional[int] = None):
"""Trigger autonomous reaction."""
if guild_id:
result = self._post(f"/autonomous/reaction?guild_id={guild_id}")
else:
result = self._post("/autonomous/reaction")
print(f"{result['message']}")
def autonomous_custom(self, prompt: str, guild_id: Optional[int] = None):
"""Send custom autonomous message."""
if guild_id:
result = self._post(f"/autonomous/custom?guild_id={guild_id}", json_data={"prompt": prompt})
else:
result = self._post("/autonomous/custom", json_data={"prompt": prompt})
print(f"{result['message']}")
def autonomous_stats(self):
"""Get autonomous engine stats."""
result = self._get("/autonomous/stats")
print(json.dumps(result, indent=2))
# ========== Server Management ==========
def list_servers(self):
"""List all configured servers."""
result = self._get("/servers")
if not result.get('servers'):
print("No servers configured")
return
print(f"\n📋 Configured Servers ({len(result['servers'])}):\n")
for server in result['servers']:
print(f"🌐 {server['guild_name']} (ID: {server['guild_id']})")
print(f" Channel: #{server['autonomous_channel_name']} (ID: {server['autonomous_channel_id']})")
if server.get('bedtime_channel_ids'):
print(f" Bedtime Channels: {server['bedtime_channel_ids']}")
if server.get('enabled_features'):
print(f" Features: {', '.join(server['enabled_features'])}")
print()
def bedtime(self, guild_id: Optional[int] = None):
"""Send bedtime reminder."""
if guild_id:
result = self._post(f"/bedtime?guild_id={guild_id}")
else:
result = self._post("/bedtime")
print(f"{result['message']}")
# ========== DM Management ==========
def list_dm_users(self):
"""List all users with DM history."""
result = self._get("/dms/users")
if not result.get('users'):
print("No DM users found")
return
print(f"\n💬 Users with DM History ({len(result['users'])}):\n")
for user in result['users']:
print(f"\n👤 {user.get('username', 'Unknown')} (ID: {user.get('user_id', 'N/A')})")
if 'message_count' in user:
print(f" Messages: {user['message_count']}")
if 'last_message_date' in user:
print(f" Last seen: {user['last_message_date']}")
elif 'last_seen' in user:
print(f" Last seen: {user['last_seen']}")
elif 'timestamp' in user:
print(f" Last seen: {user['timestamp']}")
if user.get('is_blocked'):
print(f" ⛔ BLOCKED")
print()
def dm_custom(self, user_id: str, prompt: str):
"""Send custom DM to user."""
result = self._post(f"/dm/{user_id}/custom", json_data={"prompt": prompt})
if result['status'] == 'ok':
print(f"✅ Custom DM queued for user {user_id}")
else:
print(f"❌ Error: {result.get('message', 'Unknown error')}")
def dm_manual(self, user_id: str, message: str):
"""Send manual DM to user."""
result = self._post(f"/dm/{user_id}/manual", data={"message": message})
if result['status'] == 'ok':
print(f"✅ Manual DM sent to user {user_id}")
else:
print(f"❌ Error: {result.get('message', 'Unknown error')}")
def block_user(self, user_id: str):
"""Block a user from DMing."""
result = self._post(f"/dms/users/{user_id}/block")
if result['status'] == 'ok':
print(f"✅ User {user_id} blocked")
else:
print(f"❌ Error: {result.get('message', 'Unknown error')}")
def unblock_user(self, user_id: str):
"""Unblock a user."""
result = self._post(f"/dms/users/{user_id}/unblock")
if result['status'] == 'ok':
print(f"✅ User {user_id} unblocked")
else:
print(f"❌ Error: {result.get('message', 'Unknown error')}")
def list_blocked_users(self):
"""List blocked users."""
result = self._get("/dms/blocked-users")
if not result.get('blocked_users'):
print("No blocked users")
return
print(f"\n⛔ Blocked Users ({len(result['blocked_users'])}):\n")
for user_id, username in result['blocked_users'].items():
print(f" {username} (ID: {user_id})")
# ========== Profile Picture ==========
def change_profile_picture(self, image_path: Optional[str] = None, guild_id: Optional[int] = None):
"""Change profile picture."""
files = None
if image_path:
files = {'file': open(image_path, 'rb')}
params = f"?guild_id={guild_id}" if guild_id else ""
result = self._post(f"/profile-picture/change{params}", files=files)
if result['status'] == 'ok':
print(f"{result['message']}")
print(f"📸 Source: {result['source']}")
if result.get('metadata'):
print(f"📝 Metadata: {json.dumps(result['metadata'], indent=2)}")
else:
print(f"❌ Error: {result.get('message', 'Unknown error')}")
def get_profile_metadata(self):
"""Get profile picture metadata."""
result = self._get("/profile-picture/metadata")
if result.get('metadata'):
print(json.dumps(result['metadata'], indent=2))
else:
print("No metadata found")
# ========== Conversation Management ==========
def reset_conversation(self, user_id: str):
"""Reset conversation history for a user."""
result = self._post("/conversation/reset", json_data={"user_id": user_id})
print(f"{result['message']}")
# ========== Manual Send ==========
def manual_send(self, channel_id: str, message: str, files: Optional[list] = None):
"""Send manual message to a channel."""
data = {"channel_id": channel_id, "message": message}
file_data = {}
if files:
for i, file_path in enumerate(files):
file_data[f'files'] = open(file_path, 'rb')
result = self._post("/manual/send", data=data, files=file_data if file_data else None)
if result['status'] == 'ok':
print(f"{result['message']}")
else:
print(f"❌ Error: {result.get('message', 'Unknown error')}")
def interactive_shell(base_url: str = DEFAULT_API_URL):
"""Run an interactive shell for Miku CLI commands."""
cli = MikuCLI(base_url)
print("╔══════════════════════════════════════════╗")
print("║ Miku Discord Bot - Interactive ║")
print("║ Shell Mode v1.0 ║")
print("╚══════════════════════════════════════════╝")
print(f"\n🌐 Connected to: {base_url}")
print("💡 Type 'help' for available commands")
print("💡 Type 'exit' or 'quit' to leave\n")
while True:
try:
# Get user input
user_input = input("miku> ").strip()
if not user_input:
continue
# Parse the command
try:
parts = shlex.split(user_input)
except ValueError as e:
print(f"❌ Invalid command syntax: {e}")
continue
command = parts[0].lower()
args = parts[1:] if len(parts) > 1 else []
# Handle built-in commands
if command in ['exit', 'quit', 'q']:
print("\n👋 Goodbye!")
break
elif command == 'help':
print_help()
continue
elif command == 'clear':
print("\033[H\033[J", end="")
continue
# Execute Miku commands
try:
execute_command(cli, command, args)
except Exception as e:
print(f"❌ Error executing command: {e}")
except KeyboardInterrupt:
print("\n\n👋 Goodbye!")
break
except EOFError:
print("\n\n👋 Goodbye!")
break
def print_help():
"""Print help for interactive mode."""
help_text = """
📖 Available Commands:
Status & Info:
status - Get bot status
logs - Get recent logs
prompt - Get current prompt
servers - List all servers
Mood Management:
mood-list - List available moods
mood-get [server_id] - Get current mood
mood-set <mood> [server_id] - Set mood
mood-reset [server_id] - Reset mood to neutral
Sleep/Wake:
sleep - Put bot to sleep
wake - Wake the bot up
bedtime [server_id] - Set bedtime for server
Autonomous Actions:
autonomous-general [server_id] - Generate general message
autonomous-engage [server_id] - Engage with conversation
autonomous-tweet [server_id] - Post tweet-style message
autonomous-reaction [server_id] - Generate reaction
autonomous-custom <prompt> [server_id] - Custom autonomous action
autonomous-stats - Show autonomous statistics
DM Management:
dm-users - List users with DM history
dm-custom <user_id> <prompt> - Send custom DM
dm-manual <user_id> <msg> - Send manual DM
reset-conversation <user_id> - Reset conversation history
User Blocking:
block <user_id> - Block a user
unblock <user_id> - Unblock a user
blocked-users - List blocked users
Profile Pictures:
change-pfp [image_path] [server_id] - Change profile picture
pfp-metadata - Get profile picture metadata
Manual Actions:
send <channel_id> <message> - Send message to channel
Shell Commands:
help - Show this help
clear - Clear screen
exit, quit, q - Exit interactive mode
"""
print(help_text)
def execute_command(cli: MikuCLI, command: str, args: list):
"""Execute a command in interactive mode."""
# Status & Info
if command == 'status':
cli.get_status()
elif command == 'logs':
cli.get_logs()
elif command == 'prompt':
cli.get_prompt()
elif command == 'servers':
cli.list_servers()
# Mood Management
elif command == 'mood-list':
cli.list_moods()
elif command == 'mood-get':
server_id = int(args[0]) if args and args[0].isdigit() else None
cli.get_mood(server_id)
elif command == 'mood-set':
if not args:
print("❌ Usage: mood-set <mood> [server_id]")
return
mood = args[0]
server_id = int(args[1]) if len(args) > 1 and args[1].isdigit() else None
cli.set_mood(mood, server_id)
elif command == 'mood-reset':
server_id = int(args[0]) if args and args[0].isdigit() else None
cli.reset_mood(server_id)
# Sleep/Wake
elif command == 'sleep':
cli.sleep()
elif command == 'wake':
cli.wake()
elif command == 'bedtime':
server_id = int(args[0]) if args and args[0].isdigit() else None
cli.bedtime(server_id)
# Autonomous Actions
elif command == 'autonomous-general':
server_id = int(args[0]) if args and args[0].isdigit() else None
cli.autonomous_general(server_id)
elif command == 'autonomous-engage':
server_id = int(args[0]) if args and args[0].isdigit() else None
cli.autonomous_engage(server_id)
elif command == 'autonomous-tweet':
server_id = int(args[0]) if args and args[0].isdigit() else None
cli.autonomous_tweet(server_id)
elif command == 'autonomous-reaction':
server_id = int(args[0]) if args and args[0].isdigit() else None
cli.autonomous_reaction(server_id)
elif command == 'autonomous-custom':
if not args:
print("❌ Usage: autonomous-custom <prompt> [server_id]")
return
# Find server_id if provided
server_id = None
prompt_parts = []
for arg in args:
if arg.isdigit():
server_id = int(arg)
else:
prompt_parts.append(arg)
prompt = ' '.join(prompt_parts)
cli.autonomous_custom(prompt, server_id)
elif command == 'autonomous-stats':
cli.autonomous_stats()
# DM Management
elif command == 'dm-users':
cli.list_dm_users()
elif command == 'dm-custom':
if len(args) < 2:
print("❌ Usage: dm-custom <user_id> <prompt>")
return
user_id = args[0]
prompt = ' '.join(args[1:])
cli.dm_custom(user_id, prompt)
elif command == 'dm-manual':
if len(args) < 2:
print("❌ Usage: dm-manual <user_id> <message>")
return
user_id = args[0]
message = ' '.join(args[1:])
cli.dm_manual(user_id, message)
elif command == 'reset-conversation':
if not args:
print("❌ Usage: reset-conversation <user_id>")
return
cli.reset_conversation(args[0])
# User Blocking
elif command == 'block':
if not args:
print("❌ Usage: block <user_id>")
return
cli.block_user(args[0])
elif command == 'unblock':
if not args:
print("❌ Usage: unblock <user_id>")
return
cli.unblock_user(args[0])
elif command == 'blocked-users':
cli.list_blocked_users()
# Profile Pictures
elif command == 'change-pfp':
image_path = args[0] if args else None
server_id = int(args[1]) if len(args) > 1 and args[1].isdigit() else None
cli.change_profile_picture(image_path, server_id)
elif command == 'pfp-metadata':
cli.get_profile_metadata()
# Manual Actions
elif command == 'send':
if len(args) < 2:
print("❌ Usage: send <channel_id> <message>")
return
channel_id = args[0]
message = ' '.join(args[1:])
cli.manual_send(channel_id, message)
else:
print(f"❌ Unknown command: {command}")
print("💡 Type 'help' for available commands")
def main():
parser = argparse.ArgumentParser(
description="Miku Discord Bot CLI",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s status # Get bot status
%(prog)s mood --get # Get current DM mood
%(prog)s mood --set bubbly # Set DM mood to bubbly
%(prog)s mood --set excited --server 123 # Set server mood
%(prog)s mood --list # List available moods
%(prog)s autonomous general --server 123 # Trigger autonomous message
%(prog)s servers # List all servers
%(prog)s dm-users # List DM users
%(prog)s sleep # Put Miku to sleep
%(prog)s wake # Wake Miku up
"""
)
parser.add_argument('--url', default=DEFAULT_API_URL, help=f'API base URL (default: {DEFAULT_API_URL})')
subparsers = parser.add_subparsers(dest='command', help='Command to execute')
# Shell mode
subparsers.add_parser('shell', help='Start interactive shell mode')
# Status commands
subparsers.add_parser('status', help='Get bot status')
subparsers.add_parser('logs', help='Get recent logs')
subparsers.add_parser('prompt', help='Get last LLM prompt')
# Mood commands
mood_parser = subparsers.add_parser('mood', help='Mood management')
mood_parser.add_argument('--get', action='store_true', help='Get current mood')
mood_parser.add_argument('--set', metavar='MOOD', help='Set mood')
mood_parser.add_argument('--reset', action='store_true', help='Reset mood to neutral')
mood_parser.add_argument('--list', action='store_true', help='List available moods')
mood_parser.add_argument('--server', type=int, help='Server/guild ID')
# Sleep commands
subparsers.add_parser('sleep', help='Put Miku to sleep')
subparsers.add_parser('wake', help='Wake Miku up')
# Autonomous commands
auto_parser = subparsers.add_parser('autonomous', help='Autonomous actions')
auto_parser.add_argument('action', choices=['general', 'engage', 'tweet', 'reaction', 'custom', 'stats'])
auto_parser.add_argument('--prompt', help='Custom prompt (for custom action)')
auto_parser.add_argument('--server', type=int, help='Server/guild ID')
# Server commands
subparsers.add_parser('servers', help='List configured servers')
bedtime_parser = subparsers.add_parser('bedtime', help='Send bedtime reminder')
bedtime_parser.add_argument('--server', type=int, help='Server/guild ID')
# DM commands
subparsers.add_parser('dm-users', help='List users with DM history')
dm_custom_parser = subparsers.add_parser('dm-custom', help='Send custom DM')
dm_custom_parser.add_argument('user_id', help='User ID')
dm_custom_parser.add_argument('prompt', help='Custom prompt')
dm_manual_parser = subparsers.add_parser('dm-manual', help='Send manual DM')
dm_manual_parser.add_argument('user_id', help='User ID')
dm_manual_parser.add_argument('message', help='Message to send')
block_parser = subparsers.add_parser('block', help='Block a user')
block_parser.add_argument('user_id', help='User ID')
unblock_parser = subparsers.add_parser('unblock', help='Unblock a user')
unblock_parser.add_argument('user_id', help='User ID')
subparsers.add_parser('blocked-users', help='List blocked users')
# Profile picture commands
pfp_parser = subparsers.add_parser('change-pfp', help='Change profile picture')
pfp_parser.add_argument('--image', help='Path to image file')
pfp_parser.add_argument('--server', type=int, help='Server/guild ID')
subparsers.add_parser('pfp-metadata', help='Get profile picture metadata')
# Conversation commands
conv_parser = subparsers.add_parser('reset-conversation', help='Reset conversation history')
conv_parser.add_argument('user_id', help='User ID')
# Manual send command
send_parser = subparsers.add_parser('send', help='Send manual message to channel')
send_parser.add_argument('channel_id', help='Channel ID')
send_parser.add_argument('message', help='Message to send')
send_parser.add_argument('--files', nargs='+', help='File paths to attach')
args = parser.parse_args()
# If no command provided, start shell mode
if not args.command:
interactive_shell(args.url)
sys.exit(0)
# If shell command, start interactive mode
if args.command == 'shell':
interactive_shell(args.url)
sys.exit(0)
cli = MikuCLI(args.url)
# Execute commands
try:
if args.command == 'status':
cli.get_status()
elif args.command == 'logs':
cli.get_logs()
elif args.command == 'prompt':
cli.get_prompt()
elif args.command == 'mood':
if args.list:
cli.list_moods()
elif args.get:
cli.get_mood(args.server)
elif args.set:
cli.set_mood(args.set, args.server)
elif args.reset:
cli.reset_mood(args.server)
else:
mood_parser.print_help()
elif args.command == 'sleep':
cli.sleep()
elif args.command == 'wake':
cli.wake()
elif args.command == 'autonomous':
if args.action == 'general':
cli.autonomous_general(args.server)
elif args.action == 'engage':
cli.autonomous_engage(args.server)
elif args.action == 'tweet':
cli.autonomous_tweet(args.server)
elif args.action == 'reaction':
cli.autonomous_reaction(args.server)
elif args.action == 'custom':
if not args.prompt:
print("❌ --prompt is required for custom action")
sys.exit(1)
cli.autonomous_custom(args.prompt, args.server)
elif args.action == 'stats':
cli.autonomous_stats()
elif args.command == 'servers':
cli.list_servers()
elif args.command == 'bedtime':
cli.bedtime(args.server)
elif args.command == 'dm-users':
cli.list_dm_users()
elif args.command == 'dm-custom':
cli.dm_custom(args.user_id, args.prompt)
elif args.command == 'dm-manual':
cli.dm_manual(args.user_id, args.message)
elif args.command == 'block':
cli.block_user(args.user_id)
elif args.command == 'unblock':
cli.unblock_user(args.user_id)
elif args.command == 'blocked-users':
cli.list_blocked_users()
elif args.command == 'change-pfp':
cli.change_profile_picture(args.image, args.server)
elif args.command == 'pfp-metadata':
cli.get_profile_metadata()
elif args.command == 'reset-conversation':
cli.reset_conversation(args.user_id)
elif args.command == 'send':
cli.manual_send(args.channel_id, args.message, args.files)
except KeyboardInterrupt:
print("\n\n👋 Bye!")
sys.exit(0)
if __name__ == '__main__':
main()