banterbotapi 0.2.4__tar.gz → 0.2.5__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.
Files changed (21) hide show
  1. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/PKG-INFO +1 -1
  2. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/__init__.py +1 -1
  3. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/client.py +179 -8
  4. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterbotapi.egg-info/PKG-INFO +1 -1
  5. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/pyproject.toml +1 -1
  6. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/LICENSE +0 -0
  7. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/README.md +0 -0
  8. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/commands.py +0 -0
  9. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/embed.py +0 -0
  10. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/errors.py +0 -0
  11. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/gateway.py +0 -0
  12. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/http.py +0 -0
  13. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/intents.py +0 -0
  14. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/interactions.py +0 -0
  15. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/models.py +0 -0
  16. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterapi/permissions.py +0 -0
  17. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterbotapi.egg-info/SOURCES.txt +0 -0
  18. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterbotapi.egg-info/dependency_links.txt +0 -0
  19. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterbotapi.egg-info/requires.txt +0 -0
  20. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/banterbotapi.egg-info/top_level.txt +0 -0
  21. {banterbotapi-0.2.4 → banterbotapi-0.2.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: banterbotapi
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Python SDK for building bots on Banter (banterchat.org) — discord.py-style.
5
5
  Author-email: Banter <contact@banterchat.org>
6
6
  License-Expression: MIT
@@ -23,7 +23,7 @@ The library mirrors discord.py conventions where possible. See the Bot,
23
23
  Intents, Embed, and Permissions classes for the main entry points.
24
24
  """
25
25
 
26
- __version__ = "0.2.4"
26
+ __version__ = "0.2.5"
27
27
 
28
28
  from .client import Bot
29
29
  from .intents import Intents
@@ -47,6 +47,140 @@ _DEFAULT_BASE_URL = "https://banterchat.org"
47
47
  _GATEWAY_PATH = "/api/v1/bot/gateway"
48
48
 
49
49
 
50
+ def _auto_options_from_signature(fn):
51
+ """Best-effort convert a prefix-command signature into slash options.
52
+
53
+ Skips the leading ctx parameter. Maps int → integer, bool → boolean,
54
+ everything else → string. A parameter with a default is marked
55
+ optional; one without is required. Keyword-only (*, text: str)
56
+ gets the same treatment as a normal positional — the interaction
57
+ adapter joins all option values into args_raw so the
58
+ remainder-grabbing behaviour still works.
59
+ """
60
+ from .commands import OPTION_STRING, OPTION_INTEGER, OPTION_BOOLEAN
61
+ try:
62
+ sig = inspect.signature(fn)
63
+ except (ValueError, TypeError):
64
+ return []
65
+ params = list(sig.parameters.values())[1:] # skip ctx
66
+ out = []
67
+ for p in params:
68
+ if p.kind is inspect.Parameter.VAR_POSITIONAL:
69
+ # *args — too unstructured for the slash UI to render.
70
+ continue
71
+ if p.annotation is int:
72
+ opt_type = OPTION_INTEGER
73
+ elif p.annotation is bool:
74
+ opt_type = OPTION_BOOLEAN
75
+ else:
76
+ opt_type = OPTION_STRING
77
+ required = p.default is inspect.Parameter.empty
78
+ out.append({
79
+ "name": p.name,
80
+ "type": opt_type,
81
+ "description": p.name,
82
+ "required": required,
83
+ })
84
+ return out
85
+
86
+
87
+ class _InteractionContext:
88
+ """Context adapter for prefix commands invoked via slash.
89
+
90
+ Presents the same surface as commands.Context (bot/message/author/
91
+ channel_id/guild_id/reply/send/trigger_typing) but routes replies
92
+ through interaction.respond() so the server emits the correct
93
+ slash_command_response event + ephemeral gating.
94
+
95
+ Built on the fly in Bot._dispatch for interaction_create events
96
+ when the command is registered as a prefix command with slash=True.
97
+ """
98
+
99
+ def __init__(self, bot, interaction):
100
+ self.bot = bot
101
+ self.interaction = interaction
102
+ self.invoked_with = interaction.command_name
103
+ # Build args_raw from the options dict in stable (alphabetical)
104
+ # order so prefix-command parsers that read positional args
105
+ # still work. Values are space-joined; string values with
106
+ # spaces are NOT quoted — options with spaces need to declare
107
+ # a keyword-only parameter (def cmd(ctx, *, text: str)) which
108
+ # consumes the rest of the string.
109
+ opts = interaction.options or {}
110
+ self.args_raw = " ".join(str(opts[k]) for k in sorted(opts.keys()))
111
+ # author_perms isn't carried in the interaction event today;
112
+ # set to 0 and rely on server-side perm checks. has_permissions
113
+ # will return False for everything, which is safe-by-default.
114
+ self.author_perms = 0
115
+ # Synthetic author + channel + message stubs so handlers that
116
+ # access these attrs don't crash. Kept minimal — handlers that
117
+ # need real user data should use the Interaction API directly
118
+ # or upgrade to @bot.slash_command.
119
+ self.channel_id = interaction.channel_id
120
+ self.guild_id = interaction.guild_id
121
+ self.author = _StubUser(interaction.user_id)
122
+ self.channel = None
123
+ self.message = _StubMessage(interaction)
124
+
125
+ def has_permissions(self, required):
126
+ # Without perm data in the interaction payload we can't check.
127
+ # Return False so guarded commands fail closed; handlers that
128
+ # rely on this should be @bot.slash_command instead.
129
+ return False
130
+
131
+ async def send(self, content="", embed=None):
132
+ return await self.reply(content=content, embed=embed)
133
+
134
+ async def reply(self, content="", embed=None):
135
+ if self.interaction._responded:
136
+ await self.interaction.followup(content=content, embed=embed)
137
+ else:
138
+ await self.interaction.respond(content=content, embed=embed)
139
+
140
+ async def trigger_typing(self):
141
+ # Mapped to defer so the thinking indicator stays up.
142
+ if not self.interaction._responded:
143
+ try:
144
+ await self.interaction.defer()
145
+ except Exception:
146
+ pass
147
+
148
+
149
+ class _StubUser:
150
+ __slots__ = ("id", "username", "display_name", "bot")
151
+
152
+ def __init__(self, user_id):
153
+ self.id = user_id
154
+ self.username = ""
155
+ self.display_name = ""
156
+ self.bot = False
157
+
158
+
159
+ class _StubMessage:
160
+ """Enough of a Message to keep handlers that touch ctx.message happy."""
161
+ __slots__ = ("_interaction", "id", "user_id", "channel_id", "guild_id", "content")
162
+
163
+ def __init__(self, interaction):
164
+ self._interaction = interaction
165
+ self.id = interaction.id
166
+ self.user_id = interaction.user_id
167
+ self.channel_id = interaction.channel_id
168
+ self.guild_id = interaction.guild_id
169
+ self.content = ""
170
+
171
+ async def reply(self, content="", embed=None):
172
+ if self._interaction._responded:
173
+ await self._interaction.followup(content=content, embed=embed)
174
+ else:
175
+ await self._interaction.respond(content=content, embed=embed)
176
+
177
+ async def add_reaction(self, emoji):
178
+ # Not meaningful on a synthetic interaction message. No-op so
179
+ # example code doesn't crash; bot devs who need real reactions
180
+ # should use @bot.slash_command + explicit channel.send().
181
+ return None
182
+
183
+
50
184
  class Bot:
51
185
  """Main bot entry point.
52
186
 
@@ -213,14 +347,20 @@ class Bot:
213
347
  "name": cmd.name,
214
348
  "description": description or cmd.help or cmd.name,
215
349
  }
350
+ from .commands import SlashOption as _SO
216
351
  if options:
217
- # Defer import to avoid a circular: commands.py is
218
- # loaded by client.py, and SlashOption lives there.
219
- from .commands import SlashOption as _SO
220
352
  entry["options"] = [
221
353
  (o.to_dict() if isinstance(o, _SO) else dict(o))
222
354
  for o in options
223
355
  ]
356
+ else:
357
+ # No explicit options declared: try to auto-generate
358
+ # from the function signature so prefix-style typed
359
+ # handlers (def echo(ctx, *, text: str)) work via
360
+ # slash without the author repeating themselves.
361
+ auto = _auto_options_from_signature(fn)
362
+ if auto:
363
+ entry["options"] = auto
224
364
  self._slash_commands.append(entry)
225
365
  return fn
226
366
  return decorator
@@ -413,13 +553,9 @@ class Bot:
413
553
  return
414
554
 
415
555
  if event_type == "interaction_create":
416
- # Route slash-command invocations to their registered
417
- # handlers. Falls back to a no-op (plus on_raw_event) if
418
- # the command name isn't known — that shouldn't normally
419
- # happen because the server filters by command ownership,
420
- # but we don't want to crash the dispatch loop on drift.
421
556
  from .interactions import Interaction
422
557
  interaction = Interaction(payload, client=self)
558
+ # Dedicated @bot.slash_command handler — takes an Interaction.
423
559
  handler = self._slash_handlers.get(interaction.command_name)
424
560
  if handler is not None:
425
561
  try:
@@ -428,6 +564,41 @@ class Bot:
428
564
  await result
429
565
  except Exception:
430
566
  log.exception("slash handler %r raised", interaction.command_name)
567
+ await self._fire("on_interaction", interaction)
568
+ return
569
+ # Fall back to a prefix-style @bot.command handler. Build a
570
+ # Context adapter so the handler's existing ctx.reply / send
571
+ # / args-parsing keep working; responses route through
572
+ # interaction.respond.
573
+ cmd = self._commands.get(interaction.command_name)
574
+ if cmd is not None:
575
+ ctx = _InteractionContext(self, interaction)
576
+ try:
577
+ await cmd.invoke(ctx)
578
+ # If the handler never called reply/send, ack the
579
+ # interaction so the server stops spinning. Sending
580
+ # an empty message would fail validation; defer is
581
+ # the safe no-op.
582
+ if not interaction._responded:
583
+ try:
584
+ await interaction.defer()
585
+ except Exception:
586
+ pass
587
+ except CommandError as e:
588
+ await self._fire("on_command_error", ctx, e)
589
+ if not interaction._responded:
590
+ try:
591
+ await interaction.respond(f"error: {e}")
592
+ except Exception:
593
+ pass
594
+ except Exception as e:
595
+ await self._fire("on_command_error", ctx, e)
596
+ log.exception("prefix handler %r raised on interaction", interaction.command_name)
597
+ if not interaction._responded:
598
+ try:
599
+ await interaction.respond(f"error: {e}")
600
+ except Exception:
601
+ pass
431
602
  else:
432
603
  log.warning(
433
604
  "received interaction for unknown slash command %r",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: banterbotapi
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Python SDK for building bots on Banter (banterchat.org) — discord.py-style.
5
5
  Author-email: Banter <contact@banterchat.org>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "banterbotapi"
7
- version = "0.2.4"
7
+ version = "0.2.5"
8
8
  description = "Python SDK for building bots on Banter (banterchat.org) — discord.py-style."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
File without changes
File without changes
File without changes