bsm-cli 1.6.0b3__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.
bsm_cli/main_menus.py ADDED
@@ -0,0 +1,293 @@
1
+ import click
2
+ import questionary
3
+ from bsm_cli.server import list_servers
4
+ from questionary import Separator
5
+
6
+
7
+ async def _world_management_menu(ctx: click.Context, server_name: str):
8
+ """Displays a sub-menu for world management actions."""
9
+ world_group = ctx.obj["cli"].get_command(ctx, "world")
10
+ if not world_group:
11
+ click.secho("Error: World command group not found.", fg="red")
12
+ return
13
+
14
+ menu_map = {
15
+ "Install/Replace World": world_group.get_command(ctx, "install"),
16
+ "Export Current World": world_group.get_command(ctx, "export"),
17
+ "Reset Current World": world_group.get_command(ctx, "reset"),
18
+ "Back": None,
19
+ }
20
+
21
+ while True:
22
+ choice = await questionary.select(
23
+ f"World Management for '{server_name}':",
24
+ choices=list(menu_map.keys()),
25
+ use_indicator=True,
26
+ ).ask_async()
27
+
28
+ if choice is None or choice == "Back":
29
+ return
30
+ command = menu_map.get(choice)
31
+ if command:
32
+ await ctx.invoke(command, server_name=server_name)
33
+ break
34
+
35
+
36
+ async def _backup_restore_menu(ctx: click.Context, server_name: str):
37
+ """Displays a sub-menu for backup and restore actions."""
38
+ backup_group = ctx.obj["cli"].get_command(ctx, "backup")
39
+ if not backup_group:
40
+ click.secho("Error: Backup command group not found.", fg="red")
41
+ return
42
+
43
+ menu_map = {
44
+ "Create Backup": backup_group.get_command(ctx, "create"),
45
+ "Restore from Backup": backup_group.get_command(ctx, "restore"),
46
+ "Prune Old Backups": backup_group.get_command(ctx, "prune"),
47
+ "Back": None,
48
+ }
49
+
50
+ while True:
51
+ choice = await questionary.select(
52
+ f"Backup/Restore for '{server_name}':",
53
+ choices=list(menu_map.keys()),
54
+ use_indicator=True,
55
+ ).ask_async()
56
+
57
+ if choice is None or choice == "Back":
58
+ return
59
+ command = menu_map.get(choice)
60
+ if command:
61
+ await ctx.invoke(command, server_name=server_name)
62
+ break
63
+
64
+
65
+ async def _bans_menu(ctx: click.Context, server_name: str):
66
+ """Displays a sub-menu for ban management actions."""
67
+ bans_group = ctx.obj["cli"].get_command(ctx, "bans")
68
+ if not bans_group:
69
+ click.secho("Error: Bans command group not found.", fg="red")
70
+ return
71
+
72
+ menu_map = {
73
+ "List Bans": bans_group.get_command(ctx, "list"),
74
+ "Add Ban": bans_group.get_command(ctx, "add"),
75
+ "Remove Ban": bans_group.get_command(ctx, "remove"),
76
+ "Back": None,
77
+ }
78
+
79
+ while True:
80
+ choice = await questionary.select(
81
+ f"Bans for '{server_name}':",
82
+ choices=list(menu_map.keys()),
83
+ use_indicator=True,
84
+ ).ask_async()
85
+
86
+ if choice is None or choice == "Back":
87
+ return
88
+ command = menu_map.get(choice)
89
+ if command:
90
+ await ctx.invoke(command, server_name=server_name)
91
+ break
92
+
93
+
94
+ async def main_menu(ctx: click.Context): # noqa: C901
95
+ """Displays the main application menu and drives interactive mode."""
96
+ client = ctx.obj.get("client")
97
+ if not client:
98
+ click.secho(
99
+ "You are not logged in. Please run `bsm-cli auth login` first.", fg="red"
100
+ )
101
+ return
102
+
103
+ cli = ctx.obj["cli"]
104
+
105
+ while True:
106
+ try:
107
+ click.clear()
108
+ click.secho("BSM API Client - Main Menu", fg="magenta", bold=True)
109
+
110
+ await ctx.invoke(list_servers)
111
+
112
+ # --- Dynamically build menu choices ---
113
+ response = await client.async_get_servers()
114
+ server_names = (
115
+ [s.name for s in response.servers] if response.servers else []
116
+ )
117
+
118
+ from questionary import Choice
119
+
120
+ menu_choices: list[Choice | Separator | str] = ["Install New Server"]
121
+ if server_names:
122
+ menu_choices.append("Manage Existing Server")
123
+
124
+ menu_choices.append("Manage Plugins")
125
+ menu_choices.append("Manage Users")
126
+ menu_choices.append(Separator("--- Application ---"))
127
+ menu_choices.append("Exit")
128
+
129
+ choice = await questionary.select(
130
+ "\nChoose an action:",
131
+ choices=menu_choices, # type: ignore
132
+ use_indicator=True,
133
+ ).ask_async()
134
+
135
+ if choice is None or choice == "Exit":
136
+ return
137
+
138
+ if choice == "Install New Server":
139
+ server_group = cli.get_command(ctx, "server")
140
+ install_cmd = server_group.get_command(ctx, "install")
141
+ await ctx.invoke(install_cmd)
142
+ click.pause("Press any key to return to the main menu...")
143
+
144
+ elif choice == "Manage Existing Server":
145
+ server_name = await questionary.select(
146
+ "Select a server:", choices=server_names
147
+ ).ask_async()
148
+ if server_name:
149
+ await manage_server_menu(ctx, server_name)
150
+
151
+ elif choice == "Manage Plugins":
152
+ plugin_group = cli.get_command(ctx, "plugin")
153
+ await ctx.invoke(plugin_group)
154
+ click.pause("Press any key to return to the main menu...")
155
+
156
+ elif choice == "Manage Users":
157
+ users_group = cli.get_command(ctx, "users")
158
+ await ctx.invoke(users_group)
159
+ click.pause("Press any key to return to the main menu...")
160
+
161
+ except (click.Abort, KeyboardInterrupt):
162
+ click.echo("\nAction cancelled. Returning to the main menu.")
163
+ click.pause()
164
+ except Exception as e:
165
+ click.secho(f"\nAn unexpected error occurred: {e}", fg="red")
166
+ click.pause("Press any key to return to the main menu...")
167
+
168
+
169
+ async def manage_server_menu(ctx: click.Context, server_name: str): # noqa: C901
170
+ """Displays the menu for managing a specific, existing server."""
171
+ cli = ctx.obj["cli"]
172
+
173
+ def get_cmd(group_name, cmd_name):
174
+ """Helper to safely retrieve a command object from the CLI."""
175
+ group = cli.get_command(ctx, group_name)
176
+ return group.get_command(ctx, cmd_name) if group else None
177
+
178
+ from typing import Any, Dict, Optional, Tuple
179
+
180
+ import click
181
+
182
+ # ---- Define static menu sections ----
183
+ control_map: Dict[str, Tuple[Optional[click.Command], Dict[str, Any]]] = {
184
+ "Start Server": (get_cmd("server", "start"), {}),
185
+ "Stop Server": (get_cmd("server", "stop"), {}),
186
+ "Restart Server": (get_cmd("server", "restart"), {}),
187
+ "Send Command to Server": (get_cmd("server", "send-command"), {}),
188
+ }
189
+ management_map = {
190
+ "Backup or Restore": _backup_restore_menu,
191
+ "Manage World": _world_management_menu,
192
+ "Install Addon": (get_cmd("addon", "install"), {}),
193
+ "Manage Addons": (get_cmd("addon", "manage"), {}),
194
+ }
195
+ config_map: Dict[str, Any] = {
196
+ "Configure Properties": (get_cmd("properties", "set"), {}),
197
+ "Configure Allowlist": (get_cmd("allowlist", "add"), {}),
198
+ "Configure Permissions": (get_cmd("permissions", "set"), {}),
199
+ "Configure Ban List": _bans_menu,
200
+ }
201
+ maintenance_map: Dict[str, Tuple[Optional[click.Command], Dict[str, Any]]] = {
202
+ "Update Server": (get_cmd("server", "update"), {}),
203
+ "Delete Server": (get_cmd("server", "delete"), {}),
204
+ }
205
+ system_map: Dict[str, Tuple[Optional[click.Command], Dict[str, Any]]] = {
206
+ "Configure Settings": (get_cmd("system", "settings"), {}),
207
+ "Monitor Resource Usage": (get_cmd("system", "monitor"), {}),
208
+ }
209
+
210
+ # ---- Combine all maps for easy lookup ----
211
+ full_menu_map = {
212
+ **control_map,
213
+ **management_map,
214
+ **config_map,
215
+ **system_map,
216
+ **maintenance_map,
217
+ "Back to Main Menu": "back",
218
+ }
219
+
220
+ # ---- Build the final choices list for questionary ----
221
+ menu_choices = [
222
+ Separator("--- Server Control ---"),
223
+ *control_map.keys(),
224
+ Separator("--- Management ---"),
225
+ *management_map.keys(),
226
+ Separator("--- Configuration ---"),
227
+ *config_map.keys(),
228
+ ]
229
+
230
+ if system_map:
231
+ menu_choices.extend(
232
+ [Separator("--- System & Monitoring ---"), *system_map.keys()]
233
+ )
234
+
235
+ menu_choices.extend(
236
+ [
237
+ Separator("--- Maintenance ---"),
238
+ *maintenance_map.keys(),
239
+ Separator("--------------------"),
240
+ "Back to Main Menu",
241
+ ]
242
+ )
243
+
244
+ while True:
245
+ click.clear()
246
+ click.secho(f"--- Managing Server: {server_name} ---", fg="magenta", bold=True)
247
+ await ctx.invoke(list_servers, server_name=server_name) # type: ignore
248
+
249
+ choice = await questionary.select(
250
+ f"\nSelect an action for '{server_name}':",
251
+ choices=menu_choices, # type: ignore
252
+ use_indicator=True,
253
+ ).ask_async()
254
+
255
+ if choice is None or choice == "Back to Main Menu":
256
+ return
257
+
258
+ action = full_menu_map.get(choice)
259
+ if not action:
260
+ continue
261
+
262
+ try:
263
+ if callable(action) and not hasattr(action, "commands"):
264
+ await action(ctx, server_name)
265
+ elif isinstance(action, tuple):
266
+ command_obj, kwargs = action
267
+ if not command_obj:
268
+ continue
269
+ if hasattr(command_obj, "name") and command_obj.name == "send-command":
270
+ cmd_str = await questionary.text(
271
+ "Enter command to send:"
272
+ ).ask_async()
273
+ if cmd_str:
274
+ kwargs["command_parts"] = cmd_str.split()
275
+ else:
276
+ continue
277
+ kwargs["server_name"] = server_name
278
+ await ctx.invoke(command_obj, **kwargs)
279
+ if hasattr(command_obj, "name") and command_obj.name == "delete":
280
+ click.echo("\nServer has been deleted. Returning to main menu.")
281
+ click.pause()
282
+ return
283
+ elif hasattr(action, "commands"):
284
+ ctx.invoke(action, server_name=server_name) # type: ignore
285
+
286
+ click.pause("\nPress any key to return to the server menu...")
287
+
288
+ except Exception as e:
289
+ import traceback
290
+
291
+ click.secho(f"An error occurred while executing '{choice}': {e}", fg="red")
292
+ click.secho(traceback.format_exc(), fg="red", dim=True)
293
+ click.pause()
bsm_cli/permissions.py ADDED
@@ -0,0 +1,191 @@
1
+ import click
2
+ import questionary
3
+
4
+ from bsm_api_client.models import PermissionsSetPayload, PlayerPermissionPayload
5
+
6
+
7
+ @click.group()
8
+ def permissions():
9
+ """Manages player permission levels on a server."""
10
+ pass
11
+
12
+
13
+ @permissions.command("set")
14
+ @click.option(
15
+ "-s",
16
+ "--server",
17
+ "server_name",
18
+ required=True,
19
+ help="The name of the target server.",
20
+ )
21
+ @click.option(
22
+ "-p",
23
+ "--player",
24
+ "player_name",
25
+ help="The gamertag of the player. Skips interactive mode.",
26
+ )
27
+ @click.option(
28
+ "-l",
29
+ "--level",
30
+ type=click.Choice(["visitor", "member", "operator"], case_sensitive=False),
31
+ help="The permission level to grant. Skips interactive mode.",
32
+ )
33
+ @click.pass_context
34
+ async def set_perm(ctx, server_name: str, player_name: str, level: str):
35
+ """Sets a permission level for a player on a specific server."""
36
+ client = ctx.obj.get("client")
37
+ if not client:
38
+ click.secho("You are not logged in.", fg="red")
39
+ return
40
+
41
+ try:
42
+ if not player_name or not level:
43
+ click.secho(
44
+ f"Player or level not specified; starting interactive editor for '{server_name}'...",
45
+ fg="yellow",
46
+ )
47
+ await interactive_permissions_workflow(client, server_name)
48
+ return
49
+
50
+ click.echo(f"Finding player '{player_name}' in global database...")
51
+ all_players_resp = await client.async_get_players()
52
+ player_data = next(
53
+ (
54
+ p
55
+ for p in all_players_resp.players or []
56
+ if p.get("name", "").lower() == player_name.lower()
57
+ ),
58
+ None,
59
+ )
60
+
61
+ if not player_data or not player_data.get("xuid"):
62
+ click.secho(
63
+ f"Error: Player '{player_name}' not found in the global player database.",
64
+ fg="red",
65
+ )
66
+ return
67
+
68
+ xuid = player_data["xuid"]
69
+ click.echo(
70
+ f"Setting permission for {player_name} (XUID: {xuid}) to '{level}'..."
71
+ )
72
+
73
+ payload = PermissionsSetPayload(
74
+ permissions=[
75
+ PlayerPermissionPayload(
76
+ name=player_name, xuid=xuid, permission_level=level
77
+ )
78
+ ]
79
+ )
80
+ response = await client.async_set_server_permissions(server_name, payload)
81
+
82
+ if response.status == "success":
83
+ click.secho("Permission updated successfully.", fg="green")
84
+ else:
85
+ click.secho(f"Failed to set permission: {response.message}", fg="red")
86
+
87
+ except Exception as e:
88
+ click.secho(f"An error occurred: {e}", fg="red")
89
+
90
+
91
+ @permissions.command("list")
92
+ @click.option(
93
+ "-s", "--server", "server_name", required=True, help="The name of the server."
94
+ )
95
+ @click.pass_context
96
+ async def list_perms(ctx, server_name: str):
97
+ """Lists all configured player permissions for a specific server."""
98
+ client = ctx.obj.get("client")
99
+ if not client:
100
+ click.secho("You are not logged in.", fg="red")
101
+ return
102
+
103
+ response = await client.async_get_server_permissions_data(server_name)
104
+
105
+ if response.status == "success":
106
+ permissions = response.permissions
107
+ if not permissions:
108
+ click.secho(
109
+ f"The permissions file for server '{server_name}' is empty.",
110
+ fg="yellow",
111
+ )
112
+ return
113
+
114
+ click.secho(f"\nPermissions for '{server_name}':", bold=True)
115
+ for p in permissions:
116
+ level = p.get("permission_level", "unknown").lower()
117
+ level_color = {"operator": "red", "member": "green", "visitor": "blue"}.get(
118
+ level, "white"
119
+ )
120
+ level_styled = click.style(level.capitalize(), fg=level_color, bold=True)
121
+
122
+ name = p.get("name", "Unknown Player")
123
+ xuid = p.get("xuid", "N/A")
124
+ click.echo(f" - {name:<20} (XUID: {xuid:<18}) {level_styled}")
125
+ else:
126
+ click.secho(f"Failed to list permissions: {response.message}", fg="red")
127
+
128
+
129
+ async def interactive_permissions_workflow(client, server_name: str):
130
+ """Guides the user through an interactive workflow to set a player's permission level."""
131
+ click.secho("\n--- Interactive Permission Configuration ---", bold=True)
132
+
133
+ while True:
134
+ player_response = await client.async_get_players()
135
+ all_players = player_response.players or []
136
+
137
+ if not all_players:
138
+ click.secho(
139
+ "No players found in the global player database (players.json).",
140
+ fg="yellow",
141
+ )
142
+ return
143
+
144
+ player_map = {f"{p['name']} (XUID: {p['xuid']})": p for p in all_players}
145
+ choices = sorted(list(player_map.keys())) + ["Cancel"]
146
+
147
+ player_choice_str = await questionary.select(
148
+ "Select a player to configure permissions for:", choices=choices
149
+ ).ask_async()
150
+
151
+ if not player_choice_str or player_choice_str == "Cancel":
152
+ click.secho("Exiting interactive permissions editor.", fg="blue")
153
+ break
154
+
155
+ selected_player = player_map[player_choice_str]
156
+ permission = await questionary.select(
157
+ f"Select permission level for {selected_player['name']}:",
158
+ choices=["member", "operator", "visitor", "Cancel"],
159
+ default="member",
160
+ ).ask_async()
161
+
162
+ if not permission or permission == "Cancel":
163
+ click.secho("Operation cancelled.", fg="blue")
164
+ continue
165
+
166
+ payload = PermissionsSetPayload(
167
+ permissions=[
168
+ PlayerPermissionPayload(
169
+ name=selected_player["name"],
170
+ xuid=selected_player["xuid"],
171
+ permission_level=permission,
172
+ )
173
+ ]
174
+ )
175
+ perm_response = await client.async_set_server_permissions(server_name, payload)
176
+
177
+ if perm_response.status == "success":
178
+ click.secho(
179
+ f"Permission for {selected_player['name']} set to '{permission}'.",
180
+ fg="green",
181
+ )
182
+ else:
183
+ click.secho(f"Failed to set permission: {perm_response.message}", fg="red")
184
+
185
+ if (
186
+ await questionary.confirm(
187
+ "Configure another player?", default=True
188
+ ).ask_async()
189
+ is False
190
+ ):
191
+ break
bsm_cli/player.py ADDED
@@ -0,0 +1,59 @@
1
+ import click
2
+
3
+
4
+ @click.group()
5
+ def player():
6
+ """Manages the central player database."""
7
+ pass
8
+
9
+
10
+ @player.command("scan")
11
+ @click.pass_context
12
+ async def scan_for_players(ctx):
13
+ """Scans all server logs to discover player gamertags and XUIDs."""
14
+ client = ctx.obj.get("client")
15
+ if not client:
16
+ click.secho("You are not logged in.", fg="red")
17
+ return
18
+
19
+ try:
20
+ click.echo("Scanning all server logs for player data...")
21
+ response = await client.async_scan_players()
22
+ if response.status == "success":
23
+ click.secho("Player database updated successfully.", fg="green")
24
+ else:
25
+ click.secho(f"Failed to scan for players: {response.message}", fg="red")
26
+ except Exception as e:
27
+ click.secho(f"An error occurred during scan: {e}", fg="red")
28
+
29
+
30
+ @player.command("add")
31
+ @click.option(
32
+ "-p",
33
+ "--player",
34
+ "players",
35
+ multiple=True,
36
+ required=True,
37
+ help="Player to add in 'Gamertag:XUID' format. Use multiple times for multiple players.",
38
+ )
39
+ @click.pass_context
40
+ async def add_players(ctx, players):
41
+ """Manually adds or updates player entries in the central player database."""
42
+ client = ctx.obj.get("client")
43
+ if not client:
44
+ click.secho("You are not logged in.", fg="red")
45
+ return
46
+
47
+ try:
48
+ from bsm_api_client.models import AddPlayersPayload
49
+
50
+ player_list = list(players)
51
+ click.echo(f"Adding/updating {len(player_list)} player(s) in the database...")
52
+ payload = AddPlayersPayload(players=player_list)
53
+ response = await client.async_add_players(payload)
54
+ if response.status == "success":
55
+ click.secho("Players added/updated successfully.", fg="green")
56
+ else:
57
+ click.secho(f"Failed to add players: {response.message}", fg="red")
58
+ except Exception as e:
59
+ click.secho(f"An error occurred while adding players: {e}", fg="red")