This doc walks through the individual components of discord-menu.
Menus are easiest understood through the lens of the underlying ViewState (a.k.a ViewModel in MVVM architecture). Views are a constructed from combination of a declarative template (i.e HTML DOM) and dynamic data from the ViewState.
EmbedTranstions are code that determintes how ViewStates transform based on external input (e.g emoji clicked). As the ViewState change, in turn so does the visualization (View) associated with it.
-
EmbedViewState - This is the set of data that can be modified by external inputs (e.g user clicks). The state can be used to display dynamic information on the View (e.g page number).
-
EmbedView - This is the code for what is displayed on the user's screen in Discord. It takes input from the ViewState and transforms it into UI elements.
-
EmbedTransition - Transitions are code that is run in order to convert the current ViewState to the next ViewState. Often this means recomputing a new ViewState entirely to show different data. In more complex cases, data can be carried over from the previous state in order to influence what to display next (e.g query params, page history)
-
EmbedMenu - Finally, the Menu is conceptually a container for all of the components described above. It is what a user sees and interacts with on Discord.
discord-menu
does not require sessions, which allows the service it runs on to be stateless (note: this is different from the menu being unable to record state - see below). If the bot turns off and on again, previous menus that were instantiated by the bot will still be able to function when the bot returns and responds to the user request. discord-menu
stores menu state within Discord Embed images in locations that generally do not interfere with the user experience. Intra Message State is the data that is needed to reconstruct the menu from scratch.
Due to this, all Menus that require state also need to contain a Discord Embed image. This will be the case for most menus, short of simple displays that are never edited (a "menu" with no controls).
These images can either be in the Embed author
, image
, thumbnail
, or footer
. By default, we recommend using the Embed footer as the UI element is most pleasant and unintrusive.
In practice, Intra Message State is saved by subclassing ViewState
and attaching it on Menu.create(...)
or within an EmbedTransition
function as part of the main API flow.
Menus in this library are called upon in two separate contexts - on menu creation and on discord reaction. These two contexts do not share any process state, which allows the system to be independent and scalable.
In the creation case, a user defined EmbedMenu
and seed ViewState
is created from user input, and is translated by discord-menu
into an EmbedWrapper
that is sent to Discord. Discord servers receive the request and displays the menu message to the end user in the Discord app.
Sometime later, a user may click a reaction on the menu, which triggers the on_reaction_add
code path in the bot. discord-menu
extracts the ViewState
from the event, and executes user defined code in EmbedTransition
which produces the next EmbedWrapper
to be sent to discord.
In both cases above, the EmbedWrapper
is independently derived from the underlying EmbedViewState
. As long as the user defined EmbedMenu
and its corresponding code is independently loadable in the two code paths, the system's main limitation is the data storage size of an EmbedViewState
, which is unlikely to be an issue.
See test cases[link] for sample code. At a high level:
- Define a view by subclassing
EmbedView
. - Encapsulate view state data that needs to be passed between views. See
ViewState
. - Tie it together with a Menu class that conforms to
PMenuable
- Register the menu with the bot's Menu Listener so that it can respond to emoji reactions.
The container for arrays of things. This is most similar to html <div>
. The inline
parameter controls how to display items in the box. inline = false
is most similar to css display: inline-block
, while inline = true
is most similar to display: flex
.
Simple text entry. Similar to html <span>
.
Bold text entry. Similar to html <b>
.
A convenience class for "key-value pair" like data. Similar to <b>{text1}</b> <span>{text2}</span>
Text with a link. Similar to html <a>
.
Markdown emphasis. e.g
This is emphasized text.
Markdown code blocks. e.g
This is block of text....
Is a code block.
Convenience class for a series of links where one of them is selected (unclickable).
Bots have access to the emojis in all the servers (guilds) they reside in. discord-menu
holds an EmojiCache
and can load emojis from the servers they are in on start. You can set the server ids that you want to read from by calling emoji_cache.set_guild_ids(...)
Your server's emoji game may be really strong and you may exceed the maximum number of emojis for your server. One option around this limitiation is to create dedicated emoji servers and invite your bot to them. Be wary of name collisions, as different emojis with the same names across servers will interfere with each other. Use the singleton emoji_cache
provided inside discord-menu emoji_cache.set_guild_ids(...)
to tell your bot which servers to read emojis from.
The previous section on UI Components demonstrated how to generate an Embed for display. However, menu's are obviously most useful when they can change state as one interacts with it.
The most critical component of a menu is the MenuListener
, which attaches itself to the on_raw_reaction_add
and on_raw_reaction_remove
Discord events. The menu listener handles receiving and filtering discord reaction events, then forwarding them to the appropriate menu.
There should only be one MenuListener
per bot process. Upon construction, it takes a MenuMap
containing MenuMapEntries
which holds a menu type and its corresponding menu transitions.
menu_map = MenuMap()
menu_map[SimpleTextMenu.MENU_TYPE] = MenuMapEntry(SimpleTextMenu, EmbedTransitions)
A bot may naturally have multiple cogs with menus. Because MenuListener
(and likely MenuMap
) are only defined once, it is recommended that you define these in a dedicated cog (e.g menulistenercog
).
One can then utilize the reflective function bot.get_cog(...)
to load and modify the MenuMap
member on the dedicated cog as appropriate.
Perhaps an extension in the future could be to provide menulistenercog
as part of discord-menu
.
The MenuListener
recieves every single reaction event where the bot is in. A filtering mechanism discards irrelevant events such that the bot is not overwhelmed.
There are 3 default reaction filters that are attached to the MenuListener
:
ValidEmojiReactionFilter
- discards reactions that do not control the menu.NotPosterEmojiReactionFilter
- discards reactions events from the bot itself.BotAuthoredMessageReactionFilter
- discards reactions on messages that the bot didn't post.
Additional filters can be added by subclassing MenuListener
and overriding the get_additional_reaction_filters
method.
Subclass the ReactionFilter
class in discord menu, and implement _allow_reaction
and _allow_reaction_raw
. Filters can be composed and mimic boolean AND and OR logic based on the following mechanisms:
AND
- filter 1 and 2 are sequentially listed in theget_additional_reaction_filters
list.OR
- filter 2 is nested on filter 1's constructor parameter namedreaction_filter
.
Nesting is only 1 deep at the moment, as more complicated filters were not concievable at the time of writing. Reach out to the developer team if one has a use case that isn't supported yet.
Some examples of things you could build using discord-menu:
- Simple poll / voting
- Access control on menus
- Child menus (controls on one message affect another message)
- Image slideshow
- User profile viewer
- Moderation tools