Handlers and arguments¶
One handler can serve many buttons, and each button can carry what it stands for.
An argument per button¶
A ticket has a priority, and you want one button for each value, all running one handler. An enum.Enum holds the
values.
class Priority(enum.Enum):
LOW = 1
NORMAL = 2
URGENT = 3
risa.bind puts a value on a button, so render() binds one priority to each
button it draws.
@risa.register(name="ticket")
class Ticket(risa.View):
priority: Priority = Priority.NORMAL
def render(self) -> ui.Layout:
buttons = [
ui.Button(
risa.bind(self.pick, priority),
label=priority.name.title(),
disabled=priority is self.priority,
)
for priority in Priority
]
return ui.Container(
ui.TextDisplay(f"Priority: **{self.priority.name.lower()}**"),
ui.Row(*buttons),
)
In Discord
Priority: normal
The handler receives the value as a parameter after ctx.
@risa.handler
async def pick(self, ctx: risa.ComponentContext, priority: Priority) -> None:
self.priority = priority
await ctx.rerender()
Use a field for what changes while the view lives, and an argument for what a button stands for.
Where the argument sits¶
An argument travels in the button's custom_id, behind the route and any state, so risa's messages call it a wire
argument. These are the ticket's real ids:
What the buttons hold
The defaults
1aDehXDjA<!0Z2$`>Z+id$0FeR+X$0R1aDehXDjA<#!+X$0s1aDehXDjA<$!+X$0{Yellow routes the click to its view and handler. Pink is the state. Lavender is the bound argument.
What can be an argument¶
Arguments come right after ctx and are positional. They are annotated bool, int, str, hikari.Snowflake or an
enum.Enum of int or str values. Every parameter after them without a default is filled in by dependency injection
through linkd, as Dependency injection shows.
An argument takes room on its button, out of what the button can hold of the state. With the default codec, a button with
no state holds a str of up to 68 ASCII characters, and a longer one makes client.build raise.
Another codec fits more.
Full file
"""One handler behind several buttons, each carrying its own argument."""
from __future__ import annotations
import enum
import os
import hikari
import risa
from risa import ui
class Priority(enum.Enum):
LOW = 1
NORMAL = 2
URGENT = 3
@risa.register(name="ticket")
class Ticket(risa.View):
priority: Priority = Priority.NORMAL
def render(self) -> ui.Layout:
buttons = [
ui.Button(
risa.bind(self.pick, priority),
label=priority.name.title(),
disabled=priority is self.priority,
)
for priority in Priority
]
return ui.Container(
ui.TextDisplay(f"Priority: **{self.priority.name.lower()}**"),
ui.Row(*buttons),
)
@risa.handler
async def pick(self, ctx: risa.ComponentContext, priority: Priority) -> None:
self.priority = priority
await ctx.rerender()
def main() -> None:
bot = hikari.GatewayBot(
os.environ["DISCORD_TOKEN"],
intents=hikari.Intents.ALL_UNPRIVILEGED | hikari.Intents.MESSAGE_CONTENT,
)
client = risa.client_from_app(bot)
client.add_view(Ticket)
@bot.listen()
async def on_message(event: hikari.GuildMessageCreateEvent) -> None:
if event.content == "!ticket":
rendered = await client.build(Ticket())
await rendered.send_to(bot.rest, event.channel_id)
bot.run()
if __name__ == "__main__":
main()
hikari's enums¶
hikari's enums are not enum.Enum.
Annotate a parameter hikari.ChannelType and risa counts it as something to inject, so it is no argument at all,
and a value bound to it fails when client.build runs.
@risa.register(name="channel-kind-wrong")
class ChannelKindWrong(risa.View):
def render(self) -> ui.Layout:
voice = hikari.ChannelType.GUILD_VOICE
return ui.Container(
ui.TextDisplay("Which kind of channel?"),
ui.Row(ui.Button(risa.bind(self.pick, voice), label="Voice")),
)
@risa.handler
async def pick(
self,
ctx: risa.ComponentContext,
kind: hikari.ChannelType,
) -> None:
await ctx.respond(f"You picked {kind.name}.", ephemeral=True)
What client.build raises
handler 'ChannelKindWrong.pick': received 1 wire argument, but the handler declares 0To carry one on the wire, bind it as an int.
@risa.register(name="channel-kind")
class ChannelKind(risa.View):
def render(self) -> ui.Layout:
text = hikari.ChannelType.GUILD_TEXT
voice = hikari.ChannelType.GUILD_VOICE
return ui.Container(
ui.TextDisplay("Which kind of channel?"),
ui.Row(
ui.Button(risa.bind(self.pick, int(text)), label="Text"),
ui.Button(risa.bind(self.pick, int(voice)), label="Voice"),
),
)
In Discord
Which kind of channel?
Annotate the parameter int, and rebuild the enum in the handler with hikari.ChannelType(kind).
@risa.handler
async def pick(self, ctx: risa.ComponentContext, kind: int) -> None:
picked = hikari.ChannelType(kind)
await ctx.respond(f"You picked {picked.name}.", ephemeral=True)
Full file
"""A hikari enum as an argument: a view that fails to build, and the fix."""
from __future__ import annotations
import os
import hikari
import risa
from risa import ui
@risa.register(name="channel-kind-wrong")
class ChannelKindWrong(risa.View):
def render(self) -> ui.Layout:
voice = hikari.ChannelType.GUILD_VOICE
return ui.Container(
ui.TextDisplay("Which kind of channel?"),
ui.Row(ui.Button(risa.bind(self.pick, voice), label="Voice")),
)
@risa.handler
async def pick(
self,
ctx: risa.ComponentContext,
kind: hikari.ChannelType,
) -> None:
await ctx.respond(f"You picked {kind.name}.", ephemeral=True)
@risa.register(name="channel-kind")
class ChannelKind(risa.View):
def render(self) -> ui.Layout:
text = hikari.ChannelType.GUILD_TEXT
voice = hikari.ChannelType.GUILD_VOICE
return ui.Container(
ui.TextDisplay("Which kind of channel?"),
ui.Row(
ui.Button(risa.bind(self.pick, int(text)), label="Text"),
ui.Button(risa.bind(self.pick, int(voice)), label="Voice"),
),
)
@risa.handler
async def pick(self, ctx: risa.ComponentContext, kind: int) -> None:
picked = hikari.ChannelType(kind)
await ctx.respond(f"You picked {picked.name}.", ephemeral=True)
def main() -> None:
bot = hikari.GatewayBot(
os.environ["DISCORD_TOKEN"],
intents=hikari.Intents.ALL_UNPRIVILEGED | hikari.Intents.MESSAGE_CONTENT,
)
client = risa.client_from_app(bot)
client.add_view(ChannelKind)
@bot.listen()
async def on_message(event: hikari.GuildMessageCreateEvent) -> None:
if event.content == "!channelkind":
rendered = await client.build(ChannelKind())
await rendered.send_to(bot.rest, event.channel_id)
bot.run()
if __name__ == "__main__":
main()
The options of @risa.handler¶
Used bare, @risa.handler takes the defaults. With options, it takes keywords, as
in @risa.handler(handler_id="pick", version=2).
| Option | What it does |
|---|---|
handler_id |
The id the handler routes under, the method's name by default |
version |
Bump it when the handler's arguments change |
defer |
The handler's AutoDefer, overriding the view's |
Changing a handler shows what each change does to buttons already sent,
how handler_id keeps them working through a rename, and how a method at the old version answers them.