discordcr-middleware
discordcr-middleware
An extension of discordcr's Client that adds middleware functionality, similar to that of webserver libraries.
The goal is provide a very customizable way to conditionally execute event handlers, and make great reuse of code and state between events.
Installation
Add this to your application's shard.yml:
dependencies:
discordcr-middleware:
github: z64/discordcr-middleware
Usage
Require the extension:
require "discordcr-middleware"
Creating Middleware
Custom middleware inherits from Discord::Middleware and implements def call(context, done).
class MyMiddleware < Discord::Middleware
def call(context : Discord::Context(Discord::Message), done)
# Do things with `context` here..
# ..
# Now call the next middleware in the chain:
done.call
end
end
context is an instance of Discord::Context(P), which contains a reference to your Client and tthe payload of type P, which in this case is the invoking Message.
A custom middleware can be used for multiple kinds of events by adding multiple methods that restrict context to the specific payload type.
class MyMiddleware < Discord::Middleware
def call(context : Discord::Context(Discord::Message), done)
# Handle Discord::Message payloads
done.call
end
def call(context : Discord::Context(Discord::Gateway::PresenceUpdatePayload), done)
# Handle Discord::Gateway::PresenceUpdatePayload payloads
done.call
end
end
done represents the next middleware in the chain, which is grabbed and subsequently called lazily. If you do not send done.call, the rest of the middleware chain won't be executed. Use this to leverage flow control across the middleware chain.
Currently, if you try to use a middleware on an event handler where the middleware does not have a matching #call method restricted appropriately, it will throw a runtime error.
# OK
client.on_message_create(MyMiddleware.new)
# OK
client.on_presence_update(MyMiddleware.new)
# Runtime error! :(
client.on_guild_member_update(MyMiddleware.new)
You can also extend Context class and add more custom properties to be set and shared between middleware, just like you would a class with property and property!:
# Add `Context#db_user`, with type `Database::UserModel?`
Discord.add_ctx_property(db_user, Database::UserModel)
# Add `Context#db_user`, with type `Database::UserModel`
# This performs a `not_nil!` assertion whenever you try to call it to guarantee the type to the compiler.
# You are responsible for ensuring it will never be `nil`.
Discord.add_ctx_property!(db_user, Database::UserModel)
And use it in some middleware:
Discord.add_ctx_property!(db_user, Database::UserModel)
class UserQuery < Discord::Middleware
def call(context : Discord::Context(Discord::Message), done)
author_id = context.message.author.id
user = Database::UserModel.find(discord_id: author_id)
context.db_user = user
# Only call the next middleware if the user was in our DB,
# otherwise send an error message
if user
done.call
else
channel_id = context.message.channel_id
context.client.create_message(channel_id, "User not found")
end
end
end
Middleware#initialize is not defined, so you can define this to accept any arguments to customize the behavior of your middleware per-handler.
class Prefix < Discord::Middleware
def initialize(@prefix : String)
end
def call(context : Discord::Context(Discord::Message), done)
# Only call the next middleware if the prefix matches:
done.call if context.message.content.starts_with?(@prefix)
end
end
client.on_message_create(Prefix.new("!")) do |context|
# Message started with "!"
end
client.on_message_create(Prefix.new("?")) do |context|
# Message started with "?"
end
Usage with Client
Middleware can be applied to any event handler in a few styles.
With a block:
client.on_messgae_create(Prefix.new("!dbinfo"), UserQuery.new) do |context|
# Access our custom context property that our middleware set:
results = context.query_results
channel_id = context.message.channel_id
# Send back some info about our database row..
client.create_message(channel_id, results.to_s)
end
As a pure middleware chain without a block:
client.on_message_create(MiddlewareA.new, MiddlewareB.new)
When the associated event is dispatched, it will be passed through each of your middleware sequentially, with the same context instance. If you supply a block, you can access that context instance that passed through your middleware, and the invoking payload is available as context.payload.
Note, that if you do not pass any middleware, it is the same as the base event handler method. It will be passed the raw payload (not wrapped in a Context):
client.on_message_create(MyMiddleware.new) do |context|
context #=> Context(Discord::Message)
context.payload #=> Discord::Message
end
client.on_message_create do |payload|
payload #=> Discord::Message
end
Additional Examples
Stock Middleware
A collection of basic, common use-case middleware are provided in discordcr-middleware/middleware.
Require them explicitly to make use of them:
require "discordcr-middleware/middleware/prefix"
DiscordMiddleware::Prefix.new("!help")
Contributors
- z64 - creator, maintainer
Inspired by the raze web framework and its middleware system.