telemux 1.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
telemux/__init__.py ADDED
@@ -0,0 +1,24 @@
1
+ """
2
+ TeleMux - Bidirectional Telegram integration for tmux sessions
3
+ """
4
+
5
+ __version__ = "1.0.5"
6
+ __author__ = "Marco Almazan"
7
+
8
+ from pathlib import Path
9
+
10
+ # Package-level constants
11
+ TELEMUX_DIR = Path.home() / ".telemux"
12
+ MESSAGE_QUEUE_DIR = TELEMUX_DIR / "message_queue"
13
+ CONFIG_FILE = TELEMUX_DIR / "telegram_config"
14
+ LOG_FILE = TELEMUX_DIR / "telegram_listener.log"
15
+ TMUX_SESSION = "telegram-listener"
16
+
17
+ __all__ = [
18
+ "__version__",
19
+ "TELEMUX_DIR",
20
+ "MESSAGE_QUEUE_DIR",
21
+ "CONFIG_FILE",
22
+ "LOG_FILE",
23
+ "TMUX_SESSION",
24
+ ]
telemux/cleanup.py ADDED
@@ -0,0 +1,185 @@
1
+ """
2
+ TeleMux Log Rotation and Cleanup
3
+ Automatically rotates large log files and archives old data
4
+ """
5
+
6
+ import sys
7
+ import gzip
8
+ import shutil
9
+ import subprocess
10
+ from pathlib import Path
11
+ from datetime import datetime, timedelta
12
+
13
+ from . import MESSAGE_QUEUE_DIR, LOG_FILE
14
+
15
+
16
+ # Configuration
17
+ MAX_SIZE_MB = 10
18
+ MAX_SIZE_BYTES = MAX_SIZE_MB * 1024 * 1024
19
+
20
+
21
+ def log_info(message: str):
22
+ """Print info message."""
23
+ print(f"✓ {message}")
24
+
25
+
26
+ def log_warning(message: str):
27
+ """Print warning message."""
28
+ print(f"⚠ {message}")
29
+
30
+
31
+ def rotate_log(log_file: Path):
32
+ """Rotate a log file if it exceeds size limit."""
33
+ if not log_file.exists():
34
+ return
35
+
36
+ file_size = log_file.stat().st_size
37
+
38
+ if file_size > MAX_SIZE_BYTES:
39
+ # Create archive directory
40
+ archive_month = datetime.now().strftime("%Y-%m")
41
+ archive_dir = MESSAGE_QUEUE_DIR / "archive" / archive_month
42
+ archive_dir.mkdir(parents=True, exist_ok=True)
43
+
44
+ # Create archive filename
45
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
46
+ archive_file = archive_dir / f"{log_file.name}.{timestamp}"
47
+
48
+ size_mb = file_size / (1024 * 1024)
49
+ log_warning(f"Rotating {log_file.name} ({size_mb:.2f}MB > {MAX_SIZE_MB}MB)")
50
+
51
+ # Move to archive
52
+ shutil.move(str(log_file), str(archive_file))
53
+
54
+ # Compress archive
55
+ with open(archive_file, 'rb') as f_in:
56
+ with gzip.open(str(archive_file) + '.gz', 'wb') as f_out:
57
+ shutil.copyfileobj(f_in, f_out)
58
+
59
+ # Remove uncompressed archive
60
+ archive_file.unlink()
61
+
62
+ # Create new empty log file
63
+ log_file.touch()
64
+
65
+ log_info(f"Archived to {archive_file}.gz")
66
+ else:
67
+ size_mb = file_size / (1024 * 1024)
68
+ log_info(f"{log_file.name} is {size_mb:.2f}MB (under {MAX_SIZE_MB}MB limit)")
69
+
70
+
71
+ def cleanup_old_archives():
72
+ """Remove archives older than 6 months."""
73
+ archive_base = MESSAGE_QUEUE_DIR / "archive"
74
+ if not archive_base.exists():
75
+ return
76
+
77
+ # Calculate cutoff date (6 months ago)
78
+ cutoff_date = datetime.now() - timedelta(days=180)
79
+ cutoff_str = cutoff_date.strftime("%Y-%m")
80
+
81
+ # Find and remove old archives
82
+ removed_count = 0
83
+ for archive_dir in archive_base.iterdir():
84
+ if archive_dir.is_dir() and archive_dir.name < cutoff_str:
85
+ log_warning(f"Removing {archive_dir.name}")
86
+ shutil.rmtree(archive_dir)
87
+ removed_count += 1
88
+
89
+ if removed_count > 0:
90
+ print("")
91
+
92
+
93
+ def install_cron():
94
+ """Install cron job for monthly log rotation."""
95
+ print("")
96
+ print("Installing cron job for monthly log rotation...")
97
+
98
+ cron_cmd = "0 0 1 * * python3 -m telemux.cleanup"
99
+
100
+ try:
101
+ # Get current crontab
102
+ result = subprocess.run(
103
+ ['crontab', '-l'],
104
+ capture_output=True,
105
+ text=True,
106
+ check=False
107
+ )
108
+
109
+ # Filter out existing cleanup entries
110
+ existing_lines = []
111
+ if result.returncode == 0:
112
+ existing_lines = [
113
+ line for line in result.stdout.splitlines()
114
+ if 'telemux' not in line.lower() and 'cleanup' not in line.lower()
115
+ ]
116
+
117
+ # Add new cron job
118
+ new_crontab = '\n'.join(existing_lines + [cron_cmd]) + '\n'
119
+
120
+ # Install new crontab
121
+ subprocess.run(
122
+ ['crontab', '-'],
123
+ input=new_crontab,
124
+ text=True,
125
+ check=True
126
+ )
127
+
128
+ log_info("Cron job installed (runs 1st of each month at midnight)")
129
+ print("To remove: crontab -e")
130
+
131
+ except subprocess.CalledProcessError as e:
132
+ print(f"Failed to install cron job: {e}")
133
+ except FileNotFoundError:
134
+ print("crontab command not found (cron may not be available)")
135
+
136
+
137
+ def main():
138
+ """Main cleanup entry point."""
139
+ print("TeleMux Log Rotation")
140
+ print("=" * 60)
141
+ print("")
142
+
143
+ # Log files to rotate
144
+ outgoing_log = MESSAGE_QUEUE_DIR / "outgoing.log"
145
+ incoming_log = MESSAGE_QUEUE_DIR / "incoming.log"
146
+
147
+ # Rotate logs if they exceed size limit
148
+ rotate_log(outgoing_log)
149
+ rotate_log(incoming_log)
150
+ rotate_log(LOG_FILE)
151
+
152
+ print("")
153
+
154
+ # Clean up old archives
155
+ cleanup_old_archives()
156
+
157
+ # Summary
158
+ print("Summary")
159
+ print("-" * 60)
160
+
161
+ archive_dir = MESSAGE_QUEUE_DIR / "archive"
162
+ if archive_dir.exists():
163
+ # Count archived files
164
+ archive_count = sum(1 for _ in archive_dir.rglob("*.gz"))
165
+
166
+ # Calculate total size
167
+ total_size = sum(f.stat().st_size for f in archive_dir.rglob("*.gz"))
168
+ size_mb = total_size / (1024 * 1024)
169
+
170
+ print(f"Archive directory: {archive_dir}")
171
+ print(f"Archived files: {archive_count}")
172
+ print(f"Total archive size: {size_mb:.2f} MB")
173
+ else:
174
+ print("No archives yet")
175
+
176
+ print("")
177
+ log_info("Log rotation complete!")
178
+
179
+ # Optional: Install cron job
180
+ if len(sys.argv) > 1 and sys.argv[1] == "--install-cron":
181
+ install_cron()
182
+
183
+
184
+ if __name__ == "__main__":
185
+ main()
telemux/cli.py ADDED
@@ -0,0 +1,82 @@
1
+ """
2
+ Main CLI entry point for TeleMux
3
+ """
4
+
5
+ import sys
6
+ from . import control, __version__
7
+
8
+
9
+ def main():
10
+ """Main CLI dispatcher."""
11
+ if len(sys.argv) < 2:
12
+ print("TeleMux - Bidirectional Telegram Integration for tmux")
13
+ print("")
14
+ print("Usage: telemux <command>")
15
+ print("")
16
+ print("Commands:")
17
+ print(" install - Run interactive installer")
18
+ print(" start - Start the listener daemon")
19
+ print(" stop - Stop the listener daemon")
20
+ print(" restart - Restart the listener daemon")
21
+ print(" status - Check if listener is running")
22
+ print(" logs - Tail the log file")
23
+ print(" attach - Attach to the listener tmux session")
24
+ print(" cleanup - Rotate and clean up log files")
25
+ print(" doctor - Run health check and diagnose issues")
26
+ print(" version - Show version information")
27
+ print("")
28
+ print("Shell Functions (available after installation):")
29
+ print(" tg_alert \"message\" - Send notification to Telegram")
30
+ print(" tg_agent \"name\" \"message\" - Send message and receive replies")
31
+ print(" tg_done - Alert when previous command completes")
32
+ print("")
33
+ print("Shortcuts:")
34
+ print(" tg-start, tg-stop, tg-status, tg-logs")
35
+ print("")
36
+ print("Examples:")
37
+ print(" telemux install # Run installer")
38
+ print(" telemux start # Start listener")
39
+ print(" telemux --version # Show version")
40
+ print(" tg_alert \"Build complete\" # Send notification")
41
+ print("")
42
+ print("Documentation: https://github.com/malmazan/telemux")
43
+ sys.exit(0)
44
+
45
+ command = sys.argv[1]
46
+
47
+ # Handle version flags
48
+ if command in ["--version", "-v", "version"]:
49
+ print(f"telemux {__version__}")
50
+ sys.exit(0)
51
+ elif command == "install":
52
+ from .installer import main as installer_main
53
+ installer_main()
54
+ elif command == "start":
55
+ control.start()
56
+ elif command == "stop":
57
+ control.stop()
58
+ elif command == "restart":
59
+ control.restart()
60
+ elif command == "status":
61
+ control.status()
62
+ elif command == "logs":
63
+ control.logs()
64
+ elif command == "attach":
65
+ control.attach()
66
+ elif command == "cleanup":
67
+ from .cleanup import main as cleanup_main
68
+ cleanup_main()
69
+ elif command == "doctor":
70
+ control.doctor()
71
+ elif command in ["-h", "--help", "help"]:
72
+ # Remove command argument and show help
73
+ sys.argv.pop(1)
74
+ main()
75
+ else:
76
+ print(f"Unknown command: {command}")
77
+ print("Run 'telemux' with no arguments for usage information")
78
+ sys.exit(1)
79
+
80
+
81
+ if __name__ == "__main__":
82
+ main()
telemux/config.py ADDED
@@ -0,0 +1,77 @@
1
+ """
2
+ Configuration management for TeleMux
3
+ """
4
+
5
+ import os
6
+ from typing import Optional, Tuple
7
+
8
+ from . import TELEMUX_DIR, MESSAGE_QUEUE_DIR, CONFIG_FILE
9
+
10
+
11
+ def ensure_directories() -> None:
12
+ """Create TeleMux directories if they don't exist."""
13
+ TELEMUX_DIR.mkdir(parents=True, exist_ok=True)
14
+ MESSAGE_QUEUE_DIR.mkdir(parents=True, exist_ok=True)
15
+
16
+
17
+ def load_config() -> Tuple[Optional[str], Optional[str]]:
18
+ """
19
+ Load Telegram configuration from config file.
20
+
21
+ Returns:
22
+ Tuple of (bot_token, chat_id) or (None, None) if not configured
23
+ """
24
+ if not CONFIG_FILE.exists():
25
+ return None, None
26
+
27
+ # Source the bash config file to extract env vars
28
+ bot_token = None
29
+ chat_id = None
30
+
31
+ try:
32
+ with open(CONFIG_FILE, 'r') as f:
33
+ for line in f:
34
+ line = line.strip()
35
+ if line.startswith('export TELEMUX_TG_BOT_TOKEN='):
36
+ bot_token = line.split('=', 1)[1].strip('"').strip("'")
37
+ elif line.startswith('export TELEMUX_TG_CHAT_ID='):
38
+ chat_id = line.split('=', 1)[1].strip('"').strip("'")
39
+ except Exception:
40
+ pass
41
+
42
+ # Also check environment variables (they take precedence)
43
+ bot_token = os.environ.get('TELEMUX_TG_BOT_TOKEN', bot_token)
44
+ chat_id = os.environ.get('TELEMUX_TG_CHAT_ID', chat_id)
45
+
46
+ return bot_token, chat_id
47
+
48
+
49
+ def save_config(bot_token: str, chat_id: str) -> None:
50
+ """
51
+ Save Telegram configuration to config file.
52
+
53
+ Args:
54
+ bot_token: Telegram bot token
55
+ chat_id: Telegram chat ID
56
+ """
57
+ ensure_directories()
58
+
59
+ config_content = f"""#!/bin/bash
60
+ # TeleMux Telegram Bot Configuration
61
+ # Keep this file secure! (chmod 600)
62
+
63
+ export TELEMUX_TG_BOT_TOKEN="{bot_token}"
64
+ export TELEMUX_TG_CHAT_ID="{chat_id}"
65
+ """
66
+
67
+ with open(CONFIG_FILE, 'w') as f:
68
+ f.write(config_content)
69
+
70
+ # Secure the config file
71
+ os.chmod(CONFIG_FILE, 0o600)
72
+
73
+
74
+ def is_configured() -> bool:
75
+ """Check if TeleMux is configured."""
76
+ bot_token, chat_id = load_config()
77
+ return bot_token is not None and chat_id is not None
telemux/control.py ADDED
@@ -0,0 +1,302 @@
1
+ """
2
+ Control commands for TeleMux listener daemon
3
+ """
4
+
5
+ import sys
6
+ import time
7
+ import subprocess
8
+
9
+ from . import LOG_FILE, TMUX_SESSION
10
+ from .config import load_config
11
+
12
+
13
+ def is_listener_running() -> bool:
14
+ """Check if the listener tmux session is running."""
15
+ try:
16
+ result = subprocess.run(
17
+ ['tmux', 'has-session', '-t', TMUX_SESSION],
18
+ capture_output=True,
19
+ check=False
20
+ )
21
+ return result.returncode == 0
22
+ except FileNotFoundError:
23
+ print("Error: tmux is not installed")
24
+ sys.exit(1)
25
+
26
+
27
+ def start():
28
+ """Start the Telegram listener daemon."""
29
+ if is_listener_running():
30
+ print("Telegram listener is already running")
31
+ print(" Use: telemux-status")
32
+ sys.exit(1)
33
+
34
+ print("Starting Telegram listener...")
35
+
36
+ # Start tmux session with the listener module
37
+ # Use -m flag to run as module, which handles imports correctly
38
+ subprocess.run(
39
+ ['tmux', 'new-session', '-d', '-s', TMUX_SESSION, 'python3', '-m', 'telemux.listener'],
40
+ check=False
41
+ )
42
+ time.sleep(1)
43
+
44
+ if is_listener_running():
45
+ print("Telegram listener started successfully")
46
+ print(f" Session: {TMUX_SESSION}")
47
+ print(f" Log: {LOG_FILE}")
48
+ print("")
49
+ print("Commands:")
50
+ print(" telemux-status - Check status")
51
+ print(" telemux-logs - View logs")
52
+ print(" telemux-attach - Attach to session")
53
+ print(" telemux-stop - Stop listener")
54
+ else:
55
+ print("Failed to start listener")
56
+ sys.exit(1)
57
+
58
+
59
+ def stop():
60
+ """Stop the Telegram listener daemon."""
61
+ if not is_listener_running():
62
+ print("Telegram listener is not running")
63
+ sys.exit(0)
64
+
65
+ print("Stopping Telegram listener...")
66
+ subprocess.run(['tmux', 'kill-session', '-t', TMUX_SESSION], check=False)
67
+ print("Telegram listener stopped")
68
+
69
+
70
+ def restart():
71
+ """Restart the Telegram listener daemon."""
72
+ stop()
73
+ time.sleep(2)
74
+ start()
75
+
76
+
77
+ def status():
78
+ """Check the status of the listener daemon."""
79
+ if is_listener_running():
80
+ print("Telegram listener is RUNNING")
81
+ print(f" Session: {TMUX_SESSION}")
82
+ print(f" Log: {LOG_FILE}")
83
+ print("")
84
+ print("Recent activity:")
85
+
86
+ if LOG_FILE.exists():
87
+ try:
88
+ with open(LOG_FILE, 'r') as f:
89
+ lines = f.readlines()
90
+ recent = lines[-10:] if len(lines) >= 10 else lines
91
+ for line in recent:
92
+ print(line.rstrip())
93
+ except Exception as e:
94
+ print(f"Error reading log: {e}")
95
+ else:
96
+ print("No logs yet")
97
+ else:
98
+ print("Telegram listener is NOT running")
99
+ print(" Start with: telemux-start")
100
+
101
+
102
+ def logs():
103
+ """Tail the log file."""
104
+ if LOG_FILE.exists():
105
+ try:
106
+ subprocess.run(['tail', '-f', str(LOG_FILE)])
107
+ except KeyboardInterrupt:
108
+ print("\nLog streaming stopped")
109
+ else:
110
+ print(f"No log file found at {LOG_FILE}")
111
+
112
+
113
+ def attach():
114
+ """Attach to the listener tmux session."""
115
+ if is_listener_running():
116
+ subprocess.run(['tmux', 'attach-session', '-t', TMUX_SESSION])
117
+ else:
118
+ print("Telegram listener is not running")
119
+ sys.exit(1)
120
+
121
+
122
+ def doctor():
123
+ """Run health check and diagnose issues."""
124
+ print("TeleMux Health Check")
125
+ print("=" * 60)
126
+ print("")
127
+
128
+ # Check tmux
129
+ print("Checking tmux...")
130
+ try:
131
+ result = subprocess.run(['tmux', '-V'], capture_output=True, text=True, check=False)
132
+ if result.returncode == 0:
133
+ print(f" tmux is installed ({result.stdout.strip()})")
134
+ else:
135
+ print(" tmux is NOT installed")
136
+ except FileNotFoundError:
137
+ print(" tmux is NOT installed")
138
+ print("")
139
+
140
+ # Check Python
141
+ print("Checking Python...")
142
+ result = subprocess.run(['python3', '--version'], capture_output=True, text=True, check=False)
143
+ if result.returncode == 0:
144
+ print(f" Python is installed ({result.stdout.strip()})")
145
+ else:
146
+ print(" Python3 is NOT installed")
147
+ print("")
148
+
149
+ # Check dependencies
150
+ print("Checking Python dependencies...")
151
+ try:
152
+ import requests
153
+ print(f" requests library is installed (v{requests.__version__})")
154
+ except ImportError:
155
+ print(" requests library is NOT installed")
156
+ print(" Install with: pip install telemux")
157
+ print("")
158
+
159
+ # Check config file
160
+ print("Checking configuration...")
161
+ from . import CONFIG_FILE
162
+ if CONFIG_FILE.exists():
163
+ print(f" Config file exists: {CONFIG_FILE}")
164
+
165
+ # Check permissions
166
+ perms = oct(CONFIG_FILE.stat().st_mode)[-3:]
167
+ if perms == "600":
168
+ print(" Config file permissions are secure (600)")
169
+ else:
170
+ print(f" Config file permissions: {perms} (should be 600)")
171
+ print(f" Fix with: chmod 600 {CONFIG_FILE}")
172
+
173
+ # Check if credentials are set
174
+ bot_token, chat_id = load_config()
175
+ if bot_token:
176
+ print(" Bot token is set")
177
+ else:
178
+ print(" Bot token is NOT set")
179
+
180
+ if chat_id:
181
+ print(" Chat ID is set")
182
+ # Validate format
183
+ if chat_id.lstrip('-').isdigit():
184
+ if chat_id.startswith('-'):
185
+ print(f" (Group chat: {chat_id})")
186
+ else:
187
+ print(f" (Personal chat: {chat_id})")
188
+ else:
189
+ print(" Chat ID format may be invalid")
190
+ else:
191
+ print(" Chat ID is NOT set")
192
+ else:
193
+ print(f" Config file NOT found: {CONFIG_FILE}")
194
+ print(" Run: telemux-install")
195
+ print("")
196
+
197
+ # Test bot connection
198
+ print("Testing Telegram bot connection...")
199
+ bot_token, chat_id = load_config()
200
+ if bot_token:
201
+ try:
202
+ response = requests.get(f"https://api.telegram.org/bot{bot_token}/getMe", timeout=10)
203
+ data = response.json()
204
+ if data.get("ok"):
205
+ bot_name = data["result"].get("first_name", "")
206
+ bot_username = data["result"].get("username", "")
207
+ print(" Bot connection successful!")
208
+ print(f" Bot name: {bot_name}")
209
+ print(f" Username: @{bot_username}")
210
+ else:
211
+ print(" Bot connection failed")
212
+ print(f" Response: {data}")
213
+ print(" Check your bot token")
214
+ except Exception as e:
215
+ print(f" Connection failed: {e}")
216
+ else:
217
+ print(" Skipping (no bot token configured)")
218
+ print("")
219
+
220
+ # Check listener process
221
+ print("Checking listener daemon...")
222
+ if is_listener_running():
223
+ print(f" Listener is RUNNING (session: {TMUX_SESSION})")
224
+ else:
225
+ print(" Listener is NOT running")
226
+ print(" Start with: telemux-start")
227
+ print("")
228
+
229
+ # Check log files
230
+ print("Checking log files...")
231
+ if LOG_FILE.exists():
232
+ size = LOG_FILE.stat().st_size
233
+ size_mb = size / (1024 * 1024)
234
+ with open(LOG_FILE, 'r') as f:
235
+ line_count = sum(1 for _ in f)
236
+ print(f" Listener log exists: {LOG_FILE}")
237
+ print(f" Size: {size_mb:.2f} MB ({line_count} lines)")
238
+ else:
239
+ print(" No listener log file yet")
240
+
241
+ from . import MESSAGE_QUEUE_DIR
242
+ outgoing_log = MESSAGE_QUEUE_DIR / "outgoing.log"
243
+ if outgoing_log.exists():
244
+ with open(outgoing_log, 'r') as f:
245
+ count = sum(1 for _ in f)
246
+ print(f" Outgoing message log exists ({count} messages)")
247
+ else:
248
+ print(" No outgoing messages yet")
249
+
250
+ incoming_log = MESSAGE_QUEUE_DIR / "incoming.log"
251
+ if incoming_log.exists():
252
+ with open(incoming_log, 'r') as f:
253
+ count = sum(1 for _ in f)
254
+ print(f" Incoming message log exists ({count} messages)")
255
+ else:
256
+ print(" No incoming messages yet")
257
+ print("")
258
+
259
+ # Summary
260
+ print("=" * 60)
261
+ print("Health Check Complete")
262
+ print("=" * 60)
263
+
264
+
265
+ def main():
266
+ """Main control CLI entry point."""
267
+ if len(sys.argv) < 2:
268
+ print("TeleMux Control")
269
+ print("")
270
+ print("Usage: telemux <command>")
271
+ print("")
272
+ print("Commands:")
273
+ print(" start - Start the listener daemon")
274
+ print(" stop - Stop the listener daemon")
275
+ print(" restart - Restart the listener daemon")
276
+ print(" status - Check if listener is running")
277
+ print(" logs - Tail the log file")
278
+ print(" attach - Attach to the tmux session")
279
+ print(" doctor - Run health check and diagnose issues")
280
+ print("")
281
+ print("You can also use:")
282
+ print(" telemux-start, telemux-stop, telemux-status, telemux-logs, etc.")
283
+ sys.exit(1)
284
+
285
+ command = sys.argv[1]
286
+ if command == "start":
287
+ start()
288
+ elif command == "stop":
289
+ stop()
290
+ elif command == "restart":
291
+ restart()
292
+ elif command == "status":
293
+ status()
294
+ elif command == "logs":
295
+ logs()
296
+ elif command == "attach":
297
+ attach()
298
+ elif command == "doctor":
299
+ doctor()
300
+ else:
301
+ print(f"Unknown command: {command}")
302
+ sys.exit(1)