discordcr-middleware

extension to discordcr for webserver-like middlewares discordcr-middleware discord-bot discordcr
0.3.0 released

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.

discordcr-middleware:
  github: z64/discordcr-middleware
  version: ~> 0.3.0
License MIT
Crystal 0.24.1

Authors

Dependencies 2

Development Dependencies 0

Dependents 1

Last synced .
search fire star recently