telemux 1.0.1__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.

Potentially problematic release.


This version of telemux might be problematic. Click here for more details.

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