Upgrade Slack CLI to two-way interactive chat with real-time messaging
- Add interactive chat mode with persistent configuration storage - Implement real-time message receiving with polling mechanism - Add threading support for simultaneous send/receive operations - Create MessageReceiver class for handling incoming messages - Add commands: /listen, /stop, /history for two-way communication - Display sent messages in terminal with timestamps - Support for environment variables and .env file loading - Enhanced error handling and user-friendly prompts - Update README with comprehensive two-way communication documentation - Maintain backward compatibility with original CLI functionality
This commit is contained in:
parent
3af4b831e7
commit
a971d9366c
@ -1,6 +1,6 @@
|
|||||||
# Slack Message Poster CLI
|
# Slack Two-Way Chat CLI
|
||||||
|
|
||||||
A fast and simple command-line tool for posting messages to Slack channels.
|
A powerful command-line tool for two-way communication with Slack channels. Send and receive messages in real-time with both CLI and interactive chat modes.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -11,13 +11,31 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Command Line Arguments
|
### Interactive Two-Way Chat Mode (Recommended)
|
||||||
|
|
||||||
|
Start an interactive chat session where you can send and receive messages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python slack_poster.py --chat
|
||||||
|
python slack_poster.py -i
|
||||||
|
```
|
||||||
|
|
||||||
|
In chat mode, you can:
|
||||||
|
- Type messages directly to send them to Slack
|
||||||
|
- Use `/listen` to start receiving messages from others in real-time
|
||||||
|
- View message history with `/history`
|
||||||
|
- Use commands to manage your configuration
|
||||||
|
- Have your token and channel ID automatically saved for future sessions
|
||||||
|
|
||||||
|
### Command Line Mode
|
||||||
|
|
||||||
|
Send a single message using command line arguments:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python slack_poster.py --token <SLACK_TOKEN> --channel <CHANNEL_ID> --message "<MESSAGE>"
|
python slack_poster.py --token <SLACK_TOKEN> --channel <CHANNEL_ID> --message "<MESSAGE>"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Interactive Mode
|
### Interactive CLI Mode
|
||||||
|
|
||||||
If any parameter is missing, the tool will prompt you interactively:
|
If any parameter is missing, the tool will prompt you interactively:
|
||||||
|
|
||||||
@ -28,10 +46,13 @@ python slack_poster.py
|
|||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Full command line
|
# Interactive chat mode
|
||||||
|
python slack_poster.py --chat
|
||||||
|
|
||||||
|
# Single message via CLI
|
||||||
python slack_poster.py -t xoxb-1234567890-1234567890-abcdefghijklmnopqrstuvwx -c C04ABC123 -m "Hello World!"
|
python slack_poster.py -t xoxb-1234567890-1234567890-abcdefghijklmnopqrstuvwx -c C04ABC123 -m "Hello World!"
|
||||||
|
|
||||||
# Interactive mode
|
# CLI with prompts for missing parameters
|
||||||
python slack_poster.py
|
python slack_poster.py
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -40,6 +61,51 @@ python slack_poster.py
|
|||||||
- `--token` or `-t`: Slack Bot/User OAuth token (e.g., xoxb-...)
|
- `--token` or `-t`: Slack Bot/User OAuth token (e.g., xoxb-...)
|
||||||
- `--channel` or `-c`: Slack channel ID (e.g., C04ABC123)
|
- `--channel` or `-c`: Slack channel ID (e.g., C04ABC123)
|
||||||
- `--message` or `-m`: Message text to post
|
- `--message` or `-m`: Message text to post
|
||||||
|
- `--chat` or `-i` or `--interactive`: Start interactive chat mode
|
||||||
|
|
||||||
|
## Interactive Chat Commands
|
||||||
|
|
||||||
|
When in chat mode, you can use the following commands:
|
||||||
|
|
||||||
|
- `/help` or `/h` - Show help message with all available commands
|
||||||
|
- `/quit` or `/q` or `/exit` - Exit the chat
|
||||||
|
- `/token <token>` - Set or update your Slack token
|
||||||
|
- `/channel <id>` - Set or update the channel ID
|
||||||
|
- `/status` - Show current configuration (token and channel)
|
||||||
|
- `/clear` - Clear all saved configuration
|
||||||
|
- `/test` - Send a test message to verify your configuration
|
||||||
|
- `/listen` - Start listening for incoming messages in real-time
|
||||||
|
- `/stop` - Stop listening for messages
|
||||||
|
- `/history [n]` - Show recent message history (default: 10 messages)
|
||||||
|
- `<message>` - Send any text as a message to the current channel
|
||||||
|
|
||||||
|
## Two-Way Communication Features
|
||||||
|
|
||||||
|
The tool now supports full two-way communication with Slack channels:
|
||||||
|
|
||||||
|
### Real-Time Message Receiving
|
||||||
|
- Use `/listen` to start receiving messages from others in the channel
|
||||||
|
- Messages appear automatically with timestamps and sender names
|
||||||
|
- Polls for new messages every 2 seconds for near real-time experience
|
||||||
|
- Use `/stop` to stop listening when needed
|
||||||
|
|
||||||
|
### Message History
|
||||||
|
- Use `/history` to view recent messages in the channel
|
||||||
|
- Specify number of messages: `/history 20` (default: 10, max: 50)
|
||||||
|
- Shows messages with timestamps and sender information
|
||||||
|
|
||||||
|
### Threading Support
|
||||||
|
- Send and receive messages simultaneously
|
||||||
|
- Background thread handles incoming messages
|
||||||
|
- Clean shutdown when exiting the application
|
||||||
|
|
||||||
|
## Configuration Storage
|
||||||
|
|
||||||
|
The tool automatically saves your Slack token and channel ID to `slack_config.json` in the current directory. This allows you to:
|
||||||
|
|
||||||
|
- Skip entering credentials on subsequent runs
|
||||||
|
- Switch between different tokens/channels using commands
|
||||||
|
- Maintain persistent configuration across sessions
|
||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
@ -56,7 +122,13 @@ The tool handles various error scenarios:
|
|||||||
1. Go to https://api.slack.com/apps
|
1. Go to https://api.slack.com/apps
|
||||||
2. Create a new app or select existing one
|
2. Create a new app or select existing one
|
||||||
3. Go to "OAuth & Permissions"
|
3. Go to "OAuth & Permissions"
|
||||||
4. Add the `chat:write` scope
|
4. Add the following scopes:
|
||||||
|
- `chat:write` - Send messages
|
||||||
|
- `channels:history` - Read channel messages
|
||||||
|
- `groups:history` - Read private channel messages
|
||||||
|
- `im:history` - Read direct messages
|
||||||
|
- `mpim:history` - Read group direct messages
|
||||||
|
- `users:read` - Get user information
|
||||||
5. Install the app to your workspace
|
5. Install the app to your workspace
|
||||||
6. Copy the Bot User OAuth Token (starts with `xoxb-`)
|
6. Copy the Bot User OAuth Token (starts with `xoxb-`)
|
||||||
|
|
||||||
|
|||||||
@ -3,13 +3,18 @@
|
|||||||
Slack Message Poster CLI Tool
|
Slack Message Poster CLI Tool
|
||||||
|
|
||||||
A command-line tool that posts messages to Slack channels using the Slack Web API.
|
A command-line tool that posts messages to Slack channels using the Slack Web API.
|
||||||
|
Supports both CLI mode and interactive chat mode with persistent configuration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from slack_sdk import WebClient
|
from slack_sdk import WebClient
|
||||||
@ -27,6 +32,58 @@ except ImportError:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigManager:
|
||||||
|
"""Manages persistent configuration storage."""
|
||||||
|
|
||||||
|
def __init__(self, config_file: str = "slack_config.json"):
|
||||||
|
"""Initialize the configuration manager."""
|
||||||
|
self.config_file = Path(config_file)
|
||||||
|
self.config = self._load_config()
|
||||||
|
|
||||||
|
def _load_config(self) -> Dict[str, Any]:
|
||||||
|
"""Load configuration from file."""
|
||||||
|
if self.config_file.exists():
|
||||||
|
try:
|
||||||
|
with open(self.config_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError):
|
||||||
|
return {}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_config(self) -> bool:
|
||||||
|
"""Save configuration to file."""
|
||||||
|
try:
|
||||||
|
with open(self.config_file, 'w') as f:
|
||||||
|
json.dump(self.config, f, indent=2)
|
||||||
|
return True
|
||||||
|
except IOError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_token(self) -> Optional[str]:
|
||||||
|
"""Get stored Slack token."""
|
||||||
|
return self.config.get('token')
|
||||||
|
|
||||||
|
def get_channel(self) -> Optional[str]:
|
||||||
|
"""Get stored channel ID."""
|
||||||
|
return self.config.get('channel')
|
||||||
|
|
||||||
|
def set_token(self, token: str) -> None:
|
||||||
|
"""Set and save Slack token."""
|
||||||
|
self.config['token'] = token
|
||||||
|
self.save_config()
|
||||||
|
|
||||||
|
def set_channel(self, channel: str) -> None:
|
||||||
|
"""Set and save channel ID."""
|
||||||
|
self.config['channel'] = channel
|
||||||
|
self.save_config()
|
||||||
|
|
||||||
|
def clear_config(self) -> None:
|
||||||
|
"""Clear all stored configuration."""
|
||||||
|
self.config = {}
|
||||||
|
if self.config_file.exists():
|
||||||
|
self.config_file.unlink()
|
||||||
|
|
||||||
|
|
||||||
class SlackPoster:
|
class SlackPoster:
|
||||||
"""Main class for posting messages to Slack."""
|
"""Main class for posting messages to Slack."""
|
||||||
|
|
||||||
@ -60,6 +117,154 @@ class SlackPoster:
|
|||||||
except SlackClientError as e:
|
except SlackClientError as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
def get_channel_history(self, channel: str, limit: int = 10) -> List[dict]:
|
||||||
|
"""
|
||||||
|
Get recent messages from a Slack channel.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel: Slack channel ID (e.g., C04ABC123)
|
||||||
|
limit: Number of messages to retrieve (max 1000)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[dict]: List of message objects
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SlackApiError: If the API call fails
|
||||||
|
SlackClientError: If there's a client error
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.client.conversations_history(
|
||||||
|
channel=channel,
|
||||||
|
limit=limit
|
||||||
|
)
|
||||||
|
return response.get('messages', [])
|
||||||
|
except SlackApiError as e:
|
||||||
|
raise e
|
||||||
|
except SlackClientError as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def get_user_info(self, user_id: str) -> dict:
|
||||||
|
"""
|
||||||
|
Get user information by user ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: Slack user ID (e.g., U123ABC456)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: User information
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
SlackApiError: If the API call fails
|
||||||
|
SlackClientError: If there's a client error
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = self.client.users_info(user=user_id)
|
||||||
|
return response.get('user', {})
|
||||||
|
except SlackApiError as e:
|
||||||
|
raise e
|
||||||
|
except SlackClientError as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
class MessageReceiver:
|
||||||
|
"""Handles receiving and displaying messages from Slack channels."""
|
||||||
|
|
||||||
|
def __init__(self, slack_poster: SlackPoster, channel: str):
|
||||||
|
"""Initialize the message receiver."""
|
||||||
|
self.slack_poster = slack_poster
|
||||||
|
self.channel = channel
|
||||||
|
self.last_message_timestamp = None
|
||||||
|
self.running = False
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def start_listening(self):
|
||||||
|
"""Start listening for new messages in a separate thread."""
|
||||||
|
if self.running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
self.thread = threading.Thread(target=self._listen_loop, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def stop_listening(self):
|
||||||
|
"""Stop listening for messages."""
|
||||||
|
self.running = False
|
||||||
|
if self.thread:
|
||||||
|
self.thread.join(timeout=1)
|
||||||
|
|
||||||
|
def _listen_loop(self):
|
||||||
|
"""Main listening loop that polls for new messages."""
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
messages = self.slack_poster.get_channel_history(self.channel, limit=5)
|
||||||
|
|
||||||
|
# Filter out messages we've already seen
|
||||||
|
new_messages = []
|
||||||
|
for msg in reversed(messages): # Process oldest first
|
||||||
|
msg_timestamp = msg.get('ts')
|
||||||
|
if self.last_message_timestamp is None or float(msg_timestamp) > float(self.last_message_timestamp):
|
||||||
|
new_messages.append(msg)
|
||||||
|
|
||||||
|
# Display new messages
|
||||||
|
for msg in new_messages:
|
||||||
|
self._display_message(msg)
|
||||||
|
if self.last_message_timestamp is None or float(msg.get('ts')) > float(self.last_message_timestamp):
|
||||||
|
self.last_message_timestamp = msg.get('ts')
|
||||||
|
|
||||||
|
time.sleep(2) # Poll every 2 seconds
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to receive messages: {e}")
|
||||||
|
time.sleep(5) # Wait longer on error
|
||||||
|
|
||||||
|
def _display_message(self, msg: dict):
|
||||||
|
"""Display a received message."""
|
||||||
|
try:
|
||||||
|
user_id = msg.get('user', '')
|
||||||
|
text = msg.get('text', '')
|
||||||
|
timestamp = msg.get('ts', '')
|
||||||
|
|
||||||
|
# Get user info
|
||||||
|
user_name = 'Unknown'
|
||||||
|
if user_id:
|
||||||
|
try:
|
||||||
|
user_info = self.slack_poster.get_user_info(user_id)
|
||||||
|
user_name = user_info.get('real_name', user_info.get('name', user_id))
|
||||||
|
except:
|
||||||
|
user_name = user_id
|
||||||
|
|
||||||
|
# Format timestamp
|
||||||
|
try:
|
||||||
|
dt = datetime.fromtimestamp(float(timestamp))
|
||||||
|
formatted_time = dt.strftime('%H:%M:%S')
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
formatted_time = timestamp
|
||||||
|
|
||||||
|
# Display the message
|
||||||
|
print(f"\n[{formatted_time}] {user_name}: {text}")
|
||||||
|
print("> ", end="", flush=True) # Restore prompt
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to display message: {e}")
|
||||||
|
|
||||||
|
def get_recent_messages(self, limit: int = 10):
|
||||||
|
"""Get and display recent messages from the channel."""
|
||||||
|
try:
|
||||||
|
messages = self.slack_poster.get_channel_history(self.channel, limit=limit)
|
||||||
|
|
||||||
|
print(f"\n[RECENT MESSAGES] Last {len(messages)} messages in #{self.channel}:")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
for msg in reversed(messages): # Show newest first
|
||||||
|
self._display_message(msg)
|
||||||
|
|
||||||
|
print("-" * 60)
|
||||||
|
print("> ", end="", flush=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to get recent messages: {e}")
|
||||||
|
print("> ", end="", flush=True)
|
||||||
|
|
||||||
|
|
||||||
def get_user_input(prompt: str, required: bool = True) -> str:
|
def get_user_input(prompt: str, required: bool = True) -> str:
|
||||||
"""
|
"""
|
||||||
@ -114,16 +319,279 @@ def validate_channel(channel: str) -> bool:
|
|||||||
return channel.startswith(('C', 'D', 'G')) and len(channel) > 1
|
return channel.startswith(('C', 'D', 'G')) and len(channel) > 1
|
||||||
|
|
||||||
|
|
||||||
|
def print_help():
|
||||||
|
"""Print help information for interactive mode."""
|
||||||
|
print("\n" + "="*70)
|
||||||
|
print("SLACK TWO-WAY CHAT MODE - Available Commands:")
|
||||||
|
print("="*70)
|
||||||
|
print(" /help, /h - Show this help message")
|
||||||
|
print(" /quit, /q, /exit - Exit the chat")
|
||||||
|
print(" /token <token> - Set/update Slack token")
|
||||||
|
print(" /channel <id> - Set/update channel ID")
|
||||||
|
print(" /status - Show current configuration")
|
||||||
|
print(" /clear - Clear saved configuration")
|
||||||
|
print(" /test - Test current configuration")
|
||||||
|
print(" /listen - Start listening for incoming messages")
|
||||||
|
print(" /stop - Stop listening for messages")
|
||||||
|
print(" /history [n] - Show recent message history (default: 10)")
|
||||||
|
print(" <message> - Send message to current channel")
|
||||||
|
print("="*70)
|
||||||
|
print("TWO-WAY FEATURES:")
|
||||||
|
print(" - Messages sent by others will appear automatically when listening")
|
||||||
|
print(" - Use /listen to start receiving messages in real-time")
|
||||||
|
print(" - Use /history to see recent conversation")
|
||||||
|
print("="*70)
|
||||||
|
|
||||||
|
|
||||||
|
def interactive_chat_mode(config_manager: ConfigManager):
|
||||||
|
"""Run interactive chat mode."""
|
||||||
|
print("\n[CHAT] Welcome to Slack Two-Way Interactive Chat Mode!")
|
||||||
|
print("Type your messages to send them to Slack.")
|
||||||
|
print("Use /help for available commands.\n")
|
||||||
|
|
||||||
|
# Check if we have stored configuration (try environment variables first, then config file)
|
||||||
|
token = os.getenv('SLACK_BOT_TOKEN') or config_manager.get_token()
|
||||||
|
channel = os.getenv('SLACK_CHANNEL_ID') or config_manager.get_channel()
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
print("No Slack token found. Please set one using /token <your-token>")
|
||||||
|
elif not channel:
|
||||||
|
print("No channel ID found. Please set one using /channel <channel-id>")
|
||||||
|
else:
|
||||||
|
print(f"[OK] Using configuration:")
|
||||||
|
print(f" Channel: {channel}")
|
||||||
|
print(f" Token: {token[:10]}...")
|
||||||
|
|
||||||
|
print("\nReady to chat! Type your message or use /help for commands.")
|
||||||
|
print("Use /listen to start receiving messages from others.\n")
|
||||||
|
|
||||||
|
poster = None
|
||||||
|
message_receiver = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user_input = input("> ").strip()
|
||||||
|
|
||||||
|
if not user_input:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle commands
|
||||||
|
if user_input.startswith('/'):
|
||||||
|
command = user_input.lower().split()[0]
|
||||||
|
|
||||||
|
if command in ['/help', '/h']:
|
||||||
|
print_help()
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif command in ['/quit', '/q', '/exit']:
|
||||||
|
print("\n[BYE] Goodbye!")
|
||||||
|
break
|
||||||
|
|
||||||
|
elif command == '/token':
|
||||||
|
parts = user_input.split(' ', 1)
|
||||||
|
if len(parts) < 2:
|
||||||
|
print("[ERROR] Usage: /token <your-slack-token>")
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_token = parts[1].strip()
|
||||||
|
if validate_token(new_token):
|
||||||
|
config_manager.set_token(new_token)
|
||||||
|
token = new_token
|
||||||
|
poster = None # Reset poster to use new token
|
||||||
|
print("[OK] Token updated successfully!")
|
||||||
|
else:
|
||||||
|
print("[ERROR] Invalid token format. Tokens should start with 'xoxb-', 'xoxp-', 'xoxa-', or 'xoxr-'")
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif command == '/channel':
|
||||||
|
parts = user_input.split(' ', 1)
|
||||||
|
if len(parts) < 2:
|
||||||
|
print("[ERROR] Usage: /channel <channel-id>")
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_channel = parts[1].strip()
|
||||||
|
if validate_channel(new_channel):
|
||||||
|
config_manager.set_channel(new_channel)
|
||||||
|
channel = new_channel
|
||||||
|
print(f"[OK] Channel updated to: {channel}")
|
||||||
|
else:
|
||||||
|
print("[ERROR] Invalid channel format. Channel IDs should start with 'C', 'D', or 'G'")
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif command == '/status':
|
||||||
|
print(f"\n[STATUS] Current Configuration:")
|
||||||
|
print(f" Token: {token[:10] + '...' if token else 'Not set'}")
|
||||||
|
print(f" Channel: {channel if channel else 'Not set'}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif command == '/clear':
|
||||||
|
confirm = input("[WARNING] Are you sure you want to clear all saved configuration? (y/N): ").strip().lower()
|
||||||
|
if confirm in ['y', 'yes']:
|
||||||
|
config_manager.clear_config()
|
||||||
|
token = None
|
||||||
|
channel = None
|
||||||
|
poster = None
|
||||||
|
print("[OK] Configuration cleared!")
|
||||||
|
else:
|
||||||
|
print("[CANCELLED] Configuration not cleared.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif command == '/test':
|
||||||
|
if not token or not channel:
|
||||||
|
print("[ERROR] Please set both token and channel before testing.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not poster:
|
||||||
|
poster = SlackPoster(token)
|
||||||
|
test_message = "[TEST] Test message from Slack Chat CLI"
|
||||||
|
response = poster.post_message(channel, test_message)
|
||||||
|
timestamp = response.get('ts', 'unknown')
|
||||||
|
|
||||||
|
# Convert timestamp to readable format
|
||||||
|
try:
|
||||||
|
dt = datetime.fromtimestamp(float(timestamp))
|
||||||
|
formatted_time = dt.strftime('%H:%M:%S')
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
formatted_time = timestamp
|
||||||
|
|
||||||
|
# Display the test message
|
||||||
|
print(f"[{formatted_time}] You: {test_message}")
|
||||||
|
print("[OK] Test message sent successfully!")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Test failed: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif command == '/listen':
|
||||||
|
if not token or not channel:
|
||||||
|
print("[ERROR] Please set both token and channel before listening.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not poster:
|
||||||
|
poster = SlackPoster(token)
|
||||||
|
|
||||||
|
if message_receiver and message_receiver.running:
|
||||||
|
print("[INFO] Already listening for messages.")
|
||||||
|
else:
|
||||||
|
message_receiver = MessageReceiver(poster, channel)
|
||||||
|
message_receiver.start_listening()
|
||||||
|
print("[OK] Started listening for incoming messages!")
|
||||||
|
print(" Messages from others will appear automatically.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to start listening: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif command == '/stop':
|
||||||
|
if message_receiver and message_receiver.running:
|
||||||
|
message_receiver.stop_listening()
|
||||||
|
print("[OK] Stopped listening for messages.")
|
||||||
|
else:
|
||||||
|
print("[INFO] Not currently listening for messages.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif command == '/history':
|
||||||
|
if not token or not channel:
|
||||||
|
print("[ERROR] Please set both token and channel before viewing history.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not poster:
|
||||||
|
poster = SlackPoster(token)
|
||||||
|
|
||||||
|
# Parse limit if provided
|
||||||
|
parts = user_input.split()
|
||||||
|
limit = 10
|
||||||
|
if len(parts) > 1:
|
||||||
|
try:
|
||||||
|
limit = int(parts[1])
|
||||||
|
limit = min(limit, 50) # Cap at 50 for performance
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not message_receiver:
|
||||||
|
message_receiver = MessageReceiver(poster, channel)
|
||||||
|
message_receiver.get_recent_messages(limit)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to get message history: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"[ERROR] Unknown command: {command}. Use /help for available commands.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Send message
|
||||||
|
if not token or not channel:
|
||||||
|
print("[ERROR] Please set both token and channel before sending messages.")
|
||||||
|
print(" Use /token <your-token> and /channel <channel-id>")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not poster:
|
||||||
|
poster = SlackPoster(token)
|
||||||
|
|
||||||
|
response = poster.post_message(channel, user_input)
|
||||||
|
timestamp = response.get('ts', 'unknown')
|
||||||
|
|
||||||
|
# Convert timestamp to readable format
|
||||||
|
try:
|
||||||
|
dt = datetime.fromtimestamp(float(timestamp))
|
||||||
|
formatted_time = dt.strftime('%H:%M:%S')
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
formatted_time = timestamp
|
||||||
|
|
||||||
|
# Display the sent message in the terminal
|
||||||
|
print(f"[{formatted_time}] You: {user_input}")
|
||||||
|
print(f"[OK] Message sent to #{channel}")
|
||||||
|
|
||||||
|
except SlackApiError as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
if "invalid_auth" in error_msg.lower():
|
||||||
|
print("[ERROR] Invalid token. Please update your token using /token <new-token>")
|
||||||
|
elif "channel_not_found" in error_msg.lower():
|
||||||
|
print(f"[ERROR] Channel '{channel}' not found. Please check the channel ID.")
|
||||||
|
elif "not_in_channel" in error_msg.lower():
|
||||||
|
print(f"[ERROR] Bot is not a member of channel '{channel}'. Please add the bot to the channel.")
|
||||||
|
elif "rate_limited" in error_msg.lower():
|
||||||
|
print("[ERROR] Rate limited. Please wait before trying again.")
|
||||||
|
else:
|
||||||
|
print(f"[ERROR] Slack API Error: {error_msg}")
|
||||||
|
|
||||||
|
except SlackClientError as e:
|
||||||
|
print(f"[ERROR] Slack Client Error: {e}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Unexpected error: {e}")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\n[BYE] Goodbye!")
|
||||||
|
if message_receiver:
|
||||||
|
message_receiver.stop_listening()
|
||||||
|
break
|
||||||
|
except EOFError:
|
||||||
|
print("\n\n[BYE] Goodbye!")
|
||||||
|
if message_receiver:
|
||||||
|
message_receiver.stop_listening()
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main function to handle CLI arguments and execute the message posting."""
|
"""Main function to handle CLI arguments and execute the message posting."""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Post a message to a Slack channel",
|
description="Post a message to a Slack channel or start interactive chat mode",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog="""
|
epilog="""
|
||||||
Examples:
|
Examples:
|
||||||
|
# CLI Mode - Send a single message
|
||||||
python slack_poster.py -t xoxb-1234567890-1234567890-abcdefghijklmnopqrstuvwx -c C04ABC123 -m "Hello World!"
|
python slack_poster.py -t xoxb-1234567890-1234567890-abcdefghijklmnopqrstuvwx -c C04ABC123 -m "Hello World!"
|
||||||
python slack_poster.py --token xoxb-1234567890-1234567890-abcdefghijklmnopqrstuvwx --channel C04ABC123 --message "Hello World!"
|
python slack_poster.py --token xoxb-1234567890-1234567890-abcdefghijklmnopqrstuvwx --channel C04ABC123 --message "Hello World!"
|
||||||
python slack_poster.py # Interactive mode - will prompt for missing parameters
|
|
||||||
|
# Interactive Chat Mode
|
||||||
|
python slack_poster.py --chat
|
||||||
|
python slack_poster.py -i
|
||||||
|
|
||||||
|
# CLI Mode with prompts for missing parameters
|
||||||
|
python slack_poster.py
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -145,24 +613,43 @@ Examples:
|
|||||||
help='Message text to post'
|
help='Message text to post'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--chat', '-i', '--interactive',
|
||||||
|
action='store_true',
|
||||||
|
help='Start interactive chat mode'
|
||||||
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Get token (try environment variable first, then command line, then interactive)
|
# Initialize configuration manager
|
||||||
token = args.token or os.getenv('SLACK_BOT_TOKEN')
|
config_manager = ConfigManager()
|
||||||
|
|
||||||
|
# If interactive chat mode is requested
|
||||||
|
if args.chat:
|
||||||
|
interactive_chat_mode(config_manager)
|
||||||
|
return
|
||||||
|
|
||||||
|
# CLI Mode - Send a single message
|
||||||
|
# Get token (try environment variable first, then command line, then config, then interactive)
|
||||||
|
token = args.token or os.getenv('SLACK_BOT_TOKEN') or config_manager.get_token()
|
||||||
if not token:
|
if not token:
|
||||||
print("Slack Token not provided. Please enter it interactively:")
|
print("Slack Token not provided. Please enter it interactively:")
|
||||||
token = get_user_input("Enter your Slack token: ")
|
token = get_user_input("Enter your Slack token: ")
|
||||||
|
# Save token for future use
|
||||||
|
config_manager.set_token(token)
|
||||||
|
|
||||||
# Validate token format
|
# Validate token format
|
||||||
if not validate_token(token):
|
if not validate_token(token):
|
||||||
print("Error: Invalid token format. Slack tokens typically start with 'xoxb-', 'xoxp-', 'xoxa-', or 'xoxr-'")
|
print("Error: Invalid token format. Slack tokens typically start with 'xoxb-', 'xoxp-', 'xoxa-', or 'xoxr-'")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Get channel (try environment variable first, then command line, then interactive)
|
# Get channel (try environment variable first, then command line, then config, then interactive)
|
||||||
channel = args.channel or os.getenv('SLACK_CHANNEL_ID')
|
channel = args.channel or os.getenv('SLACK_CHANNEL_ID') or config_manager.get_channel()
|
||||||
if not channel:
|
if not channel:
|
||||||
print("Channel ID not provided. Please enter it interactively:")
|
print("Channel ID not provided. Please enter it interactively:")
|
||||||
channel = get_user_input("Enter Slack channel ID (e.g., C04ABC123): ")
|
channel = get_user_input("Enter Slack channel ID (e.g., C04ABC123): ")
|
||||||
|
# Save channel for future use
|
||||||
|
config_manager.set_channel(channel)
|
||||||
|
|
||||||
# Validate channel format
|
# Validate channel format
|
||||||
if not validate_channel(channel):
|
if not validate_channel(channel):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user