botslt 1.0.0__tar.gz

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.
botslt-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: botslt
3
+ Version: 1.0.0
4
+ Summary: Official CLI for Bots.LT — sync your bot commands like Git
5
+ Author-email: "Bots.LT Team" <support@bots.lt>
6
+ License: MIT
7
+ Project-URL: Homepage, https://bots.lt
8
+ Project-URL: Documentation, https://bots.lt/docs
9
+ Keywords: telegram,bot,botslt,cli,sync
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
14
+ Classifier: Topic :: Communications :: Chat
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: requests>=2.28.0
18
+ Requires-Dist: click>=8.1.0
19
+
20
+ # blp — Official Bots.LT CLI
21
+
22
+ > Sync your Telegram bot commands like Git.
23
+
24
+ ```bash
25
+ pip install botslt
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ # 1. Save your API key (from Settings → Security on bots.lt)
34
+ blp login
35
+
36
+ # 2. Create a project in your bot's folder
37
+ mkdir my-bot && cd my-bot
38
+ blp init
39
+
40
+ # 3. Map your commands to .py files
41
+ blp add /start start.py
42
+ blp add /help help.py
43
+ blp add @ at_handler.py
44
+
45
+ # 4. Write your code, then push!
46
+ blp push
47
+ ```
48
+
49
+ ---
50
+
51
+ ## How it Works
52
+
53
+ Your project folder will look like this:
54
+
55
+ ```
56
+ my-telegram-bot/
57
+ ├── botslt.json ← Bot ID + command mappings (safe to commit to Git)
58
+ ├── botslt.lock ← Hash cache for change detection (Git-ignored)
59
+ ├── start.py ← Code for /start command
60
+ ├── help.py ← Code for /help command
61
+ └── at_handler.py ← Code for @ (global) handler
62
+ ```
63
+
64
+ Your **API key is stored globally** in `~/.botslt/credentials.json` — never in the project folder, so it's never accidentally committed to Git.
65
+
66
+ ---
67
+
68
+ ## Commands
69
+
70
+ | Command | Description |
71
+ |---------|-------------|
72
+ | `blp login` | Save your API key |
73
+ | `blp logout` | Remove saved credentials |
74
+ | `blp init` | Create `botslt.json` in current folder |
75
+ | `blp add /cmd file.py` | Map a command to a local file |
76
+ | `blp push` | Push changed commands to server |
77
+ | `blp push file.py` | Push a specific file |
78
+ | `blp push --all` | Push all commands (ignore change detection) |
79
+ | `blp pull` | Pull all server commands to local files |
80
+ | `blp status` | Show which files have changed |
81
+ | `blp logs` | Show recent bot error logs |
82
+ | `blp bots` | List all your bots |
83
+
84
+ ---
85
+
86
+ ## Example `botslt.json`
87
+
88
+ ```json
89
+ {
90
+ "bot_id": "12345678",
91
+ "commands": {
92
+ "/start": "start.py",
93
+ "/help": "help.py",
94
+ "/balance": "balance.py",
95
+ "@": "at_handler.py",
96
+ "*": "fallback.py"
97
+ }
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Example Bot Command (start.py)
104
+
105
+ ```python
106
+ bot.sendMessage(
107
+ text=f"<b>Welcome, {user.first_name}!</b>\n\nHow can I help you?",
108
+ parse_mode="html",
109
+ reply_markup={
110
+ "keyboard": [
111
+ [{"text": "💰 Balance"}],
112
+ [{"text": "❓ Help"}]
113
+ ],
114
+ "resize_keyboard": True
115
+ }
116
+ )
117
+ ```
118
+
119
+ ---
120
+
121
+ ## .gitignore
122
+
123
+ `blp init` automatically adds `botslt.lock` to your `.gitignore`:
124
+
125
+ ```gitignore
126
+ # BotsLT
127
+ botslt.lock
128
+ ```
129
+
130
+ > **Never commit `~/.botslt/credentials.json`** — it contains your API key.
131
+
132
+ ---
133
+
134
+ ## Links
135
+
136
+ - Platform: [https://bots.lt](https://bots.lt)
137
+ - Documentation: [https://bots.lt/docs](https://bots.lt/docs)
138
+ - Support: [https://t.me/JSOrganization](https://t.me/JSOrganization)
botslt-1.0.0/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # blp — Official Bots.LT CLI
2
+
3
+ > Sync your Telegram bot commands like Git.
4
+
5
+ ```bash
6
+ pip install botslt
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # 1. Save your API key (from Settings → Security on bots.lt)
15
+ blp login
16
+
17
+ # 2. Create a project in your bot's folder
18
+ mkdir my-bot && cd my-bot
19
+ blp init
20
+
21
+ # 3. Map your commands to .py files
22
+ blp add /start start.py
23
+ blp add /help help.py
24
+ blp add @ at_handler.py
25
+
26
+ # 4. Write your code, then push!
27
+ blp push
28
+ ```
29
+
30
+ ---
31
+
32
+ ## How it Works
33
+
34
+ Your project folder will look like this:
35
+
36
+ ```
37
+ my-telegram-bot/
38
+ ├── botslt.json ← Bot ID + command mappings (safe to commit to Git)
39
+ ├── botslt.lock ← Hash cache for change detection (Git-ignored)
40
+ ├── start.py ← Code for /start command
41
+ ├── help.py ← Code for /help command
42
+ └── at_handler.py ← Code for @ (global) handler
43
+ ```
44
+
45
+ Your **API key is stored globally** in `~/.botslt/credentials.json` — never in the project folder, so it's never accidentally committed to Git.
46
+
47
+ ---
48
+
49
+ ## Commands
50
+
51
+ | Command | Description |
52
+ |---------|-------------|
53
+ | `blp login` | Save your API key |
54
+ | `blp logout` | Remove saved credentials |
55
+ | `blp init` | Create `botslt.json` in current folder |
56
+ | `blp add /cmd file.py` | Map a command to a local file |
57
+ | `blp push` | Push changed commands to server |
58
+ | `blp push file.py` | Push a specific file |
59
+ | `blp push --all` | Push all commands (ignore change detection) |
60
+ | `blp pull` | Pull all server commands to local files |
61
+ | `blp status` | Show which files have changed |
62
+ | `blp logs` | Show recent bot error logs |
63
+ | `blp bots` | List all your bots |
64
+
65
+ ---
66
+
67
+ ## Example `botslt.json`
68
+
69
+ ```json
70
+ {
71
+ "bot_id": "12345678",
72
+ "commands": {
73
+ "/start": "start.py",
74
+ "/help": "help.py",
75
+ "/balance": "balance.py",
76
+ "@": "at_handler.py",
77
+ "*": "fallback.py"
78
+ }
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Example Bot Command (start.py)
85
+
86
+ ```python
87
+ bot.sendMessage(
88
+ text=f"<b>Welcome, {user.first_name}!</b>\n\nHow can I help you?",
89
+ parse_mode="html",
90
+ reply_markup={
91
+ "keyboard": [
92
+ [{"text": "💰 Balance"}],
93
+ [{"text": "❓ Help"}]
94
+ ],
95
+ "resize_keyboard": True
96
+ }
97
+ )
98
+ ```
99
+
100
+ ---
101
+
102
+ ## .gitignore
103
+
104
+ `blp init` automatically adds `botslt.lock` to your `.gitignore`:
105
+
106
+ ```gitignore
107
+ # BotsLT
108
+ botslt.lock
109
+ ```
110
+
111
+ > **Never commit `~/.botslt/credentials.json`** — it contains your API key.
112
+
113
+ ---
114
+
115
+ ## Links
116
+
117
+ - Platform: [https://bots.lt](https://bots.lt)
118
+ - Documentation: [https://bots.lt/docs](https://bots.lt/docs)
119
+ - Support: [https://t.me/JSOrganization](https://t.me/JSOrganization)
@@ -0,0 +1,4 @@
1
+ """
2
+ botslt — Official CLI for Bots.LT
3
+ Sync your Telegram bot commands like Git.
4
+ """
@@ -0,0 +1,77 @@
1
+ """
2
+ api.py — All API calls to the Bots.LT backend.
3
+ Uses X-API-Key header for authentication.
4
+ """
5
+ import requests
6
+ from .auth import get_api_key, get_base_url
7
+
8
+
9
+ def _headers() -> dict:
10
+ return {"X-API-Key": get_api_key()}
11
+
12
+
13
+ def _url(path: str) -> str:
14
+ return f"{get_base_url()}{path}"
15
+
16
+
17
+ def verify_api_key() -> dict:
18
+ """Verify that the stored API key is valid by calling /api/account."""
19
+ try:
20
+ r = requests.get(_url("/account"), headers=_headers(), timeout=10)
21
+ if r.status_code == 200:
22
+ return r.json()
23
+ return {"ok": False, "error": f"HTTP {r.status_code}"}
24
+ except Exception as e:
25
+ return {"ok": False, "error": str(e)}
26
+
27
+
28
+ def list_bots() -> dict:
29
+ """List all bots for the authenticated user."""
30
+ r = requests.get(_url("/bots-list"), headers=_headers(), timeout=10)
31
+ return r.json()
32
+
33
+
34
+ def get_all_commands(bot_id: str) -> dict:
35
+ """Get all commands for a bot."""
36
+ r = requests.get(_url(f"/bots/{bot_id}/commands"), headers=_headers(), timeout=10)
37
+ return r.json()
38
+
39
+
40
+ def get_command_code(bot_id: str, command_name: str) -> str | None:
41
+ """Get the code for a single command."""
42
+ import base64
43
+ encoded = base64.b64encode(command_name.encode("utf-8")).decode("utf-8")
44
+ r = requests.get(_url(f"/bots/{bot_id}/commands/{encoded}"), headers=_headers(), timeout=10)
45
+ if r.status_code == 200:
46
+ data = r.json()
47
+ return data.get("code") or data.get("result", {}).get("code")
48
+ return None
49
+
50
+
51
+ def push_command(bot_id: str, command_name: str, code: str) -> dict:
52
+ """Create or update a command on the server."""
53
+ import base64
54
+ encoded = base64.b64encode(command_name.encode("utf-8")).decode("utf-8")
55
+ # Try update first
56
+ r = requests.put(
57
+ _url(f"/bots/{bot_id}/commands/{encoded}"),
58
+ headers=_headers(),
59
+ json={"code": code},
60
+ timeout=15
61
+ )
62
+ if r.status_code == 200:
63
+ return r.json()
64
+ # If not found, create it
65
+ r2 = requests.post(
66
+ _url(f"/bots/{bot_id}/commands"),
67
+ headers=_headers(),
68
+ json={"command": command_name, "code": code},
69
+ timeout=15
70
+ )
71
+ return r2.json()
72
+
73
+
74
+ def get_bot_errors(bot_id: str) -> dict:
75
+ """Get recent error logs for a bot."""
76
+ r = requests.get(_url(f"/bots/{bot_id}/errors"), headers=_headers(), timeout=10)
77
+ return r.json()
@@ -0,0 +1,45 @@
1
+ """
2
+ auth.py — Handles login and credential storage.
3
+ Credentials are stored in ~/.botslt/credentials.json (never in the project folder).
4
+ """
5
+ import os
6
+ import json
7
+ from pathlib import Path
8
+
9
+ CRED_DIR = Path.home() / ".botslt"
10
+ CRED_FILE = CRED_DIR / "credentials.json"
11
+
12
+
13
+ def get_credentials() -> dict:
14
+ """Load credentials from ~/.botslt/credentials.json"""
15
+ if not CRED_FILE.exists():
16
+ return {}
17
+ try:
18
+ with open(CRED_FILE, "r", encoding="utf-8") as f:
19
+ return json.load(f)
20
+ except Exception:
21
+ return {}
22
+
23
+
24
+ def save_credentials(api_key: str, base_url: str = "https://bots.lt/api"):
25
+ """Save credentials to ~/.botslt/credentials.json"""
26
+ CRED_DIR.mkdir(parents=True, exist_ok=True)
27
+ data = {"api_key": api_key, "base_url": base_url}
28
+ with open(CRED_FILE, "w", encoding="utf-8") as f:
29
+ json.dump(data, f, indent=4)
30
+
31
+
32
+ def get_api_key() -> str | None:
33
+ """Return the stored API key, or None if not logged in."""
34
+ return get_credentials().get("api_key")
35
+
36
+
37
+ def get_base_url() -> str:
38
+ """Return the stored base URL (defaults to bots.lt)."""
39
+ return get_credentials().get("base_url", "https://bots.lt/api")
40
+
41
+
42
+ def clear_credentials():
43
+ """Remove stored credentials (logout)."""
44
+ if CRED_FILE.exists():
45
+ CRED_FILE.unlink()
@@ -0,0 +1,397 @@
1
+ """
2
+ cli.py — Main entry point for the blp CLI.
3
+ Command: blp <subcommand>
4
+ """
5
+ import click
6
+ import sys
7
+ from pathlib import Path
8
+ from . import auth, config, api
9
+
10
+
11
+ # ─── Helpers ────────────────────────────────────────────────────────────────
12
+
13
+ def require_login():
14
+ """Abort with a helpful message if not logged in."""
15
+ key = auth.get_api_key()
16
+ if not key:
17
+ click.echo(click.style("[-] Not logged in.", fg="red"))
18
+ click.echo(" Run: blp login")
19
+ sys.exit(1)
20
+
21
+ def require_config():
22
+ """Abort with a helpful message if botslt.json is missing."""
23
+ if not config.config_exists():
24
+ click.echo(click.style("[-] No botslt.json found in current directory.", fg="red"))
25
+ click.echo(" Run: blp init")
26
+ sys.exit(1)
27
+
28
+
29
+ # ─── CLI Group ───────────────────────────────────────────────────────────────
30
+
31
+ @click.group()
32
+ @click.version_option("1.0.0", prog_name="blp")
33
+ def cli():
34
+ """
35
+ blp — Official CLI for Bots.LT
36
+
37
+ Sync your Telegram bot commands like Git.
38
+
39
+ \b
40
+ Quick start:
41
+ blp login Save your API key
42
+ blp init Setup a project in the current folder
43
+ blp push Push all changed commands to server
44
+ blp pull Pull all commands from server
45
+ blp status Show which files have changed
46
+
47
+ Get your API Key from: https://bots.lt/dashboard -> Settings -> Security
48
+ """
49
+ pass
50
+
51
+
52
+ # ─── login ───────────────────────────────────────────────────────────────────
53
+
54
+ @cli.command()
55
+ @click.option("--key", "-k", prompt=False, default=None, help="API Key (from Settings -> Security)")
56
+ def login(key):
57
+ """Save your Bots.LT API key to ~/.botslt/credentials.json"""
58
+ if not key:
59
+ click.echo("Get your API Key from: https://bots.lt/dashboard -> Settings -> Security")
60
+ key = click.prompt("API Key", hide_input=True)
61
+
62
+ key = key.strip()
63
+ if not key:
64
+ click.echo(click.style("[-] API Key cannot be empty.", fg="red"))
65
+ sys.exit(1)
66
+
67
+ # Validate key against server
68
+ click.echo("Verifying API key...")
69
+ auth.save_credentials(key)
70
+ result = api.verify_api_key()
71
+
72
+ if result.get("ok"):
73
+ acc = result.get("result", {}).get("account", {})
74
+ email = acc.get("email", "unknown")
75
+ click.echo(click.style(f"[+] Logged in as: {email}", fg="green"))
76
+ click.echo(click.style(f" Credentials saved to: {auth.CRED_FILE}", fg="bright_black"))
77
+ else:
78
+ auth.clear_credentials()
79
+ click.echo(click.style(f"[-] Invalid API key: {result.get('error', 'Unknown error')}", fg="red"))
80
+ sys.exit(1)
81
+
82
+
83
+ # ─── logout ──────────────────────────────────────────────────────────────────
84
+
85
+ @cli.command()
86
+ def logout():
87
+ """Remove stored credentials from ~/.botslt/credentials.json"""
88
+ auth.clear_credentials()
89
+ click.echo(click.style("[+] Logged out successfully.", fg="green"))
90
+
91
+
92
+ # ─── init ────────────────────────────────────────────────────────────────────
93
+
94
+ @cli.command()
95
+ @click.option("--bot-id", "-b", prompt="Bot ID", help="Your Bots.LT Bot ID")
96
+ def init(bot_id):
97
+ """Create a botslt.json config file in the current directory."""
98
+ require_login()
99
+
100
+ if config.config_exists():
101
+ if not click.confirm("botslt.json already exists. Overwrite?"):
102
+ click.echo("Aborted.")
103
+ sys.exit(0)
104
+
105
+ bot_id = bot_id.strip()
106
+ data = {
107
+ "bot_id": bot_id,
108
+ "commands": {}
109
+ }
110
+ config.save_config(data)
111
+ config.ensure_gitignore()
112
+
113
+ click.echo(click.style(f"[+] Initialized project for bot ID: {bot_id}", fg="green"))
114
+ click.echo(click.style(" botslt.json created. Add commands with: blp add /start start.py", fg="bright_black"))
115
+
116
+
117
+ # ─── add ─────────────────────────────────────────────────────────────────────
118
+
119
+ @cli.command()
120
+ @click.argument("command_name")
121
+ @click.argument("filename")
122
+ def add(command_name, filename):
123
+ """Map a command name to a local .py file.
124
+
125
+ \b
126
+ Example:
127
+ blp add /start start.py
128
+ blp add @ at_handler.py
129
+ """
130
+ require_config()
131
+
132
+ cfg = config.load_config()
133
+ commands = cfg.setdefault("commands", {})
134
+ commands[command_name] = filename
135
+ config.save_config(cfg)
136
+
137
+ click.echo(click.style(f"[+] Mapped '{command_name}' -> '{filename}'", fg="green"))
138
+ click.echo(" Remember to create the file, then run: blp push")
139
+
140
+
141
+ # ─── status ──────────────────────────────────────────────────────────────────
142
+
143
+ @cli.command()
144
+ def status():
145
+ """Show which local files have changed since last push."""
146
+ require_login()
147
+ require_config()
148
+
149
+ cfg = config.load_config()
150
+ commands = cfg.get("commands", {})
151
+
152
+ if not commands:
153
+ click.echo("No commands mapped. Use: blp add /start start.py")
154
+ return
155
+
156
+ changed = config.get_changed_files()
157
+ changed_files = {c["file"] for c in changed}
158
+
159
+ click.echo(f"\n Bot ID: {click.style(cfg.get('bot_id', '?'), fg='cyan')}\n")
160
+
161
+ modified = []
162
+ ok = []
163
+ missing = []
164
+
165
+ for cmd_name, filename in commands.items():
166
+ if not Path(filename).exists():
167
+ missing.append((cmd_name, filename))
168
+ elif filename in changed_files:
169
+ modified.append((cmd_name, filename))
170
+ else:
171
+ ok.append((cmd_name, filename))
172
+
173
+ if modified:
174
+ click.echo(click.style(" Modified (not pushed):", fg="yellow"))
175
+ for cmd, f in modified:
176
+ click.echo(f" {click.style('M', fg='yellow')} {f:<25} -> {cmd}")
177
+
178
+ if missing:
179
+ click.echo(click.style("\n File not found:", fg="red"))
180
+ for cmd, f in missing:
181
+ click.echo(f" {click.style('!', fg='red')} {f:<25} -> {cmd}")
182
+
183
+ if ok:
184
+ click.echo(click.style("\n Up to date:", fg="green"))
185
+ for cmd, f in ok:
186
+ click.echo(f" {click.style('[+]', fg='green')} {f:<25} -> {cmd}")
187
+
188
+ if not modified and not missing:
189
+ click.echo(click.style("\n Everything is up to date!", fg="green"))
190
+ else:
191
+ click.echo(f"\n Run {click.style('blp push', fg='cyan')} to sync changes.")
192
+
193
+
194
+ # ─── push ────────────────────────────────────────────────────────────────────
195
+
196
+ @cli.command()
197
+ @click.argument("files", nargs=-1)
198
+ @click.option("--all", "-a", "push_all", is_flag=True, help="Push all commands, even if unchanged")
199
+ @click.option("--force", "-f", is_flag=True, help="Same as --all")
200
+ def push(files, push_all, force):
201
+ """Push changed commands to Bots.LT server.
202
+
203
+ \b
204
+ Examples:
205
+ blp push Push only changed files
206
+ blp push start.py Push a specific file
207
+ blp push --all Push all commands regardless of changes
208
+ """
209
+ require_login()
210
+ require_config()
211
+
212
+ cfg = config.load_config()
213
+ bot_id = cfg.get("bot_id")
214
+ commands = cfg.get("commands", {})
215
+
216
+ if not commands:
217
+ click.echo("No commands configured. Run: blp add /start start.py")
218
+ return
219
+
220
+ push_all = push_all or force
221
+
222
+ # If specific files given, only push those
223
+ if files:
224
+ to_push = [(cmd, f) for cmd, f in commands.items() if f in files]
225
+ if not to_push:
226
+ click.echo(click.style("[-] No matching commands found for the given files.", fg="red"))
227
+ click.echo(" Check your botslt.json mappings.")
228
+ sys.exit(1)
229
+ elif push_all:
230
+ to_push = list(commands.items())
231
+ else:
232
+ changed = config.get_changed_files()
233
+ changed_files = {c["file"] for c in changed}
234
+ to_push = [(cmd, f) for cmd, f in commands.items() if f in changed_files]
235
+
236
+ if not to_push:
237
+ click.echo(click.style("[+] Nothing to push. Everything is up to date.", fg="green"))
238
+ return
239
+
240
+ click.echo(f"\n Syncing bot {click.style(bot_id, fg='cyan')}...\n")
241
+
242
+ updated = 0
243
+ failed = 0
244
+
245
+ for cmd_name, filename in to_push:
246
+ if not Path(filename).exists():
247
+ click.echo(f" {click.style('!', fg='red')} {filename:<25} -> {cmd_name} (file not found, skipped)")
248
+ failed += 1
249
+ continue
250
+
251
+ with open(filename, "r", encoding="utf-8") as f:
252
+ code = f.read()
253
+
254
+ try:
255
+ result = api.push_command(bot_id, cmd_name, code)
256
+ if result.get("ok"):
257
+ click.echo(f" {click.style('[+]', fg='green')} {filename:<25} -> {cmd_name}")
258
+ config.update_lock_for_file(filename)
259
+ updated += 1
260
+ else:
261
+ err = result.get("error") or result.get("detail", "Unknown error")
262
+ click.echo(f" {click.style('[-]', fg='red')} {filename:<25} -> {cmd_name} ({err})")
263
+ failed += 1
264
+ except Exception as e:
265
+ click.echo(f" {click.style('[-]', fg='red')} {filename:<25} -> {cmd_name} (Error: {e})")
266
+ failed += 1
267
+
268
+ click.echo()
269
+ click.echo(f" Done! {click.style(str(updated), fg='green')} pushed, {click.style(str(failed), fg='red' if failed else 'bright_black')} failed.")
270
+
271
+
272
+ # ─── pull ────────────────────────────────────────────────────────────────────
273
+
274
+ @cli.command()
275
+ @click.option("--overwrite", is_flag=True, help="Overwrite local files without asking")
276
+ def pull(overwrite):
277
+ """Pull all commands from server and create local .py files."""
278
+ require_login()
279
+ require_config()
280
+
281
+ cfg = config.load_config()
282
+ bot_id = cfg.get("bot_id")
283
+
284
+ click.echo(f"\n Pulling from bot {click.style(bot_id, fg='cyan')}...\n")
285
+
286
+ result = api.get_all_commands(bot_id)
287
+ if not result.get("ok"):
288
+ click.echo(click.style(f"[-] Failed to fetch commands: {result.get('error', 'Unknown error')}", fg="red"))
289
+ sys.exit(1)
290
+
291
+ commands_data = result.get("commands") or result.get("result", {}).get("commands", [])
292
+
293
+ if not commands_data:
294
+ click.echo(" No commands found on the server.")
295
+ return
296
+
297
+ cfg_commands = cfg.setdefault("commands", {})
298
+ created = 0
299
+ skipped = 0
300
+
301
+ for cmd in commands_data:
302
+ cmd_name = cmd.get("command") or cmd.get("name") or cmd.get("trigger", "")
303
+ code = cmd.get("code", "")
304
+
305
+ if not cmd_name:
306
+ continue
307
+
308
+ # Generate a safe filename from the command name
309
+ safe_name = cmd_name.lstrip("/").replace(" ", "_").replace("*", "star").replace("@", "at") or "command"
310
+ filename = f"{safe_name}.py"
311
+
312
+ if Path(filename).exists() and not overwrite:
313
+ if not click.confirm(f" '{filename}' already exists. Overwrite?"):
314
+ click.echo(f" {click.style('-', fg='yellow')} {filename:<25} skipped")
315
+ skipped += 1
316
+ continue
317
+
318
+ with open(filename, "w", encoding="utf-8") as f:
319
+ f.write(code)
320
+
321
+ cfg_commands[cmd_name] = filename
322
+ config.update_lock_for_file(filename)
323
+ click.echo(f" {click.style('[+]', fg='green')} {filename:<25} <- {cmd_name}")
324
+ created += 1
325
+
326
+ config.save_config(cfg)
327
+ click.echo()
328
+ click.echo(f" Done! {click.style(str(created), fg='green')} pulled, {skipped} skipped.")
329
+
330
+
331
+ # ─── logs ────────────────────────────────────────────────────────────────────
332
+
333
+ @cli.command()
334
+ @click.option("--limit", "-n", default=10, help="Number of recent errors to show (default: 10)")
335
+ def logs(limit):
336
+ """Show recent error logs for your bot."""
337
+ require_login()
338
+ require_config()
339
+
340
+ cfg = config.load_config()
341
+ bot_id = cfg.get("bot_id")
342
+
343
+ result = api.get_bot_errors(bot_id)
344
+ if not result.get("ok"):
345
+ click.echo(click.style(f"[-] Failed to fetch logs: {result.get('error', 'Unknown error')}", fg="red"))
346
+ sys.exit(1)
347
+
348
+ errors = result.get("errors") or result.get("result", {}).get("errors", [])
349
+
350
+ if not errors:
351
+ click.echo(click.style("[+] No recent errors. Your bot is running clean!", fg="green"))
352
+ return
353
+
354
+ click.echo(f"\n Recent errors for bot {click.style(bot_id, fg='cyan')}:\n")
355
+
356
+ for i, err in enumerate(errors[:limit]):
357
+ cmd = err.get("command_name", "?")
358
+ msg = err.get("error_message", "?")
359
+ ts = err.get("created_at", "")
360
+ click.echo(f" [{click.style(str(i+1), fg='yellow')}] {click.style(cmd, fg='cyan')} — {ts}")
361
+ for line in msg.splitlines():
362
+ click.echo(f" {line}")
363
+ click.echo()
364
+
365
+
366
+ # ─── bots ────────────────────────────────────────────────────────────────────
367
+
368
+ @cli.command()
369
+ def bots():
370
+ """List all your bots."""
371
+ require_login()
372
+
373
+ result = api.list_bots()
374
+ if not result.get("ok"):
375
+ click.echo(click.style(f"[-] {result.get('error', 'Failed to list bots')}", fg="red"))
376
+ sys.exit(1)
377
+
378
+ bots_list = result.get("result", {}).get("bots", [])
379
+
380
+ if not bots_list:
381
+ click.echo("No bots found.")
382
+ return
383
+
384
+ click.echo(f"\n {click.style(str(len(bots_list)), fg='cyan')} bot(s) found:\n")
385
+ for b in bots_list:
386
+ status_color = "green" if b.get("is_active") else "red"
387
+ status = "Running" if b.get("is_active") else "Stopped"
388
+ click.echo(
389
+ f" {click.style(str(b.get('id', '?')), fg='cyan'):<12}"
390
+ f" @{str(b.get('username', '?')):<25}"
391
+ f" {click.style(status, fg=status_color)}"
392
+ )
393
+ click.echo()
394
+
395
+
396
+ if __name__ == "__main__":
397
+ cli()
@@ -0,0 +1,94 @@
1
+ """
2
+ config.py — Reads and writes botslt.json in the current project directory.
3
+ This file is safe to commit to Git — it only contains bot_id and command mappings.
4
+ """
5
+ import json
6
+ from pathlib import Path
7
+
8
+ CONFIG_FILE = Path("botslt.json")
9
+ LOCK_FILE = Path("botslt.lock")
10
+
11
+
12
+ def config_exists() -> bool:
13
+ return CONFIG_FILE.exists()
14
+
15
+
16
+ def load_config() -> dict:
17
+ """Load botslt.json from current directory."""
18
+ if not CONFIG_FILE.exists():
19
+ return {}
20
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
21
+ return json.load(f)
22
+
23
+
24
+ def save_config(data: dict):
25
+ """Save botslt.json to current directory."""
26
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
27
+ json.dump(data, f, indent=4)
28
+
29
+
30
+ def load_lock() -> dict:
31
+ """Load botslt.lock (local hash cache, NOT for Git)."""
32
+ if not LOCK_FILE.exists():
33
+ return {"files": {}}
34
+ try:
35
+ with open(LOCK_FILE, "r", encoding="utf-8") as f:
36
+ return json.load(f)
37
+ except Exception:
38
+ return {"files": {}}
39
+
40
+
41
+ def save_lock(data: dict):
42
+ """Save botslt.lock."""
43
+ with open(LOCK_FILE, "w", encoding="utf-8") as f:
44
+ json.dump(data, f, indent=4)
45
+
46
+
47
+ def get_file_hash(filepath: str) -> str:
48
+ """Return SHA256 hash of a local file."""
49
+ import hashlib
50
+ h = hashlib.sha256()
51
+ try:
52
+ with open(filepath, "rb") as f:
53
+ h.update(f.read())
54
+ return h.hexdigest()
55
+ except FileNotFoundError:
56
+ return ""
57
+
58
+
59
+ def update_lock_for_file(filename: str):
60
+ """Update the hash entry for a specific file in botslt.lock."""
61
+ lock = load_lock()
62
+ lock.setdefault("files", {})[filename] = get_file_hash(filename)
63
+ save_lock(lock)
64
+
65
+
66
+ def get_changed_files() -> list[dict]:
67
+ """Compare local files with botslt.lock hashes. Returns list of changed files."""
68
+ config = load_config()
69
+ lock = load_lock()
70
+ cached_hashes = lock.get("files", {})
71
+ commands = config.get("commands", {})
72
+
73
+ changed = []
74
+ for cmd_name, filename in commands.items():
75
+ if not Path(filename).exists():
76
+ continue
77
+ current_hash = get_file_hash(filename)
78
+ cached_hash = cached_hashes.get(filename, "")
79
+ if current_hash != cached_hash:
80
+ changed.append({"command": cmd_name, "file": filename})
81
+ return changed
82
+
83
+
84
+ def ensure_gitignore():
85
+ """Add botslt.lock to .gitignore if it's not already there."""
86
+ gitignore = Path(".gitignore")
87
+ entries_to_add = ["# BotsLT", "botslt.lock"]
88
+ if gitignore.exists():
89
+ content = gitignore.read_text(encoding="utf-8")
90
+ if "botslt.lock" not in content:
91
+ with open(gitignore, "a", encoding="utf-8") as f:
92
+ f.write("\n" + "\n".join(entries_to_add) + "\n")
93
+ else:
94
+ gitignore.write_text("\n".join(entries_to_add) + "\n", encoding="utf-8")
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: botslt
3
+ Version: 1.0.0
4
+ Summary: Official CLI for Bots.LT — sync your bot commands like Git
5
+ Author-email: "Bots.LT Team" <support@bots.lt>
6
+ License: MIT
7
+ Project-URL: Homepage, https://bots.lt
8
+ Project-URL: Documentation, https://bots.lt/docs
9
+ Keywords: telegram,bot,botslt,cli,sync
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
14
+ Classifier: Topic :: Communications :: Chat
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ Requires-Dist: requests>=2.28.0
18
+ Requires-Dist: click>=8.1.0
19
+
20
+ # blp — Official Bots.LT CLI
21
+
22
+ > Sync your Telegram bot commands like Git.
23
+
24
+ ```bash
25
+ pip install botslt
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ # 1. Save your API key (from Settings → Security on bots.lt)
34
+ blp login
35
+
36
+ # 2. Create a project in your bot's folder
37
+ mkdir my-bot && cd my-bot
38
+ blp init
39
+
40
+ # 3. Map your commands to .py files
41
+ blp add /start start.py
42
+ blp add /help help.py
43
+ blp add @ at_handler.py
44
+
45
+ # 4. Write your code, then push!
46
+ blp push
47
+ ```
48
+
49
+ ---
50
+
51
+ ## How it Works
52
+
53
+ Your project folder will look like this:
54
+
55
+ ```
56
+ my-telegram-bot/
57
+ ├── botslt.json ← Bot ID + command mappings (safe to commit to Git)
58
+ ├── botslt.lock ← Hash cache for change detection (Git-ignored)
59
+ ├── start.py ← Code for /start command
60
+ ├── help.py ← Code for /help command
61
+ └── at_handler.py ← Code for @ (global) handler
62
+ ```
63
+
64
+ Your **API key is stored globally** in `~/.botslt/credentials.json` — never in the project folder, so it's never accidentally committed to Git.
65
+
66
+ ---
67
+
68
+ ## Commands
69
+
70
+ | Command | Description |
71
+ |---------|-------------|
72
+ | `blp login` | Save your API key |
73
+ | `blp logout` | Remove saved credentials |
74
+ | `blp init` | Create `botslt.json` in current folder |
75
+ | `blp add /cmd file.py` | Map a command to a local file |
76
+ | `blp push` | Push changed commands to server |
77
+ | `blp push file.py` | Push a specific file |
78
+ | `blp push --all` | Push all commands (ignore change detection) |
79
+ | `blp pull` | Pull all server commands to local files |
80
+ | `blp status` | Show which files have changed |
81
+ | `blp logs` | Show recent bot error logs |
82
+ | `blp bots` | List all your bots |
83
+
84
+ ---
85
+
86
+ ## Example `botslt.json`
87
+
88
+ ```json
89
+ {
90
+ "bot_id": "12345678",
91
+ "commands": {
92
+ "/start": "start.py",
93
+ "/help": "help.py",
94
+ "/balance": "balance.py",
95
+ "@": "at_handler.py",
96
+ "*": "fallback.py"
97
+ }
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Example Bot Command (start.py)
104
+
105
+ ```python
106
+ bot.sendMessage(
107
+ text=f"<b>Welcome, {user.first_name}!</b>\n\nHow can I help you?",
108
+ parse_mode="html",
109
+ reply_markup={
110
+ "keyboard": [
111
+ [{"text": "💰 Balance"}],
112
+ [{"text": "❓ Help"}]
113
+ ],
114
+ "resize_keyboard": True
115
+ }
116
+ )
117
+ ```
118
+
119
+ ---
120
+
121
+ ## .gitignore
122
+
123
+ `blp init` automatically adds `botslt.lock` to your `.gitignore`:
124
+
125
+ ```gitignore
126
+ # BotsLT
127
+ botslt.lock
128
+ ```
129
+
130
+ > **Never commit `~/.botslt/credentials.json`** — it contains your API key.
131
+
132
+ ---
133
+
134
+ ## Links
135
+
136
+ - Platform: [https://bots.lt](https://bots.lt)
137
+ - Documentation: [https://bots.lt/docs](https://bots.lt/docs)
138
+ - Support: [https://t.me/JSOrganization](https://t.me/JSOrganization)
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ botslt/__init__.py
4
+ botslt/api.py
5
+ botslt/auth.py
6
+ botslt/cli.py
7
+ botslt/config.py
8
+ botslt.egg-info/PKG-INFO
9
+ botslt.egg-info/SOURCES.txt
10
+ botslt.egg-info/dependency_links.txt
11
+ botslt.egg-info/entry_points.txt
12
+ botslt.egg-info/requires.txt
13
+ botslt.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ blp = botslt.cli:cli
@@ -0,0 +1,2 @@
1
+ requests>=2.28.0
2
+ click>=8.1.0
@@ -0,0 +1 @@
1
+ botslt
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "botslt"
7
+ version = "1.0.0"
8
+ description = "Official CLI for Bots.LT — sync your bot commands like Git"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Bots.LT Team", email = "support@bots.lt" }
14
+ ]
15
+ keywords = ["telegram", "bot", "botslt", "cli", "sync"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
21
+ "Topic :: Communications :: Chat",
22
+ ]
23
+ dependencies = [
24
+ "requests>=2.28.0",
25
+ "click>=8.1.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ blp = "botslt.cli:cli"
30
+
31
+ [project.urls]
32
+ Homepage = "https://bots.lt"
33
+ Documentation = "https://bots.lt/docs"
botslt-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+