Skip to content

Commit

Permalink
Swap registries for handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
watzon committed Feb 20, 2020
1 parent 6300aeb commit 73e60e8
Show file tree
Hide file tree
Showing 14 changed files with 386 additions and 378 deletions.
10 changes: 10 additions & 0 deletions examples/echo_bot.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ require "../src/tourmaline"
class EchoBot < Tourmaline::Client
include Tourmaline

def initialize(api_key)
super(api_key)
end

@[Command("echo")]
def echo_command(ctx)
ctx.reply(ctx.text)
end

@[On(:message)]
def on_message(ctx)
pp ctx.message
end
end

bot = EchoBot.new(ENV["API_KEY"])
bot.poll

81 changes: 22 additions & 59 deletions src/tourmaline/client.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require "halite"
require "mime/multipart"

require "./helpers"
require "./error"
require "./logger"
require "./context"
Expand All @@ -10,8 +11,7 @@ require "./update_action"
require "./models/*"
require "./fiber"
require "./annotations"
require "./registries/*"
require "./middleware"
require "./handlers/*"
require "./client/*"
require "./markup"
require "./query_result_builder"
Expand All @@ -22,10 +22,7 @@ module Tourmaline
# instance of `Client` and add commands and listenters to it.
class Client
include Logger
include EventRegistry
include CommandRegistry
include PatternRegistry
include MiddlewareRegistry
include Handler::Annotator

API_URL = "https://api.telegram.org/"

Expand All @@ -43,6 +40,7 @@ module Tourmaline
# started. Refreshing can be done by setting
# `@bot_name` to `get_me.username.to_s`.
getter bot_name : String { get_me.username.to_s }
getter handlers : Hash(UpdateAction, Array(Handler))

property endpoint_url : String

Expand All @@ -56,67 +54,32 @@ module Tourmaline
@allowed_updates : Array(String)? = nil
)
@endpoint_url = Path[API_URL, "bot" + @api_key].to_s
register_commands
register_patterns
register_event_listeners
@handlers = {} of UpdateAction => Array(Handler)
register_annotated_methods

Container.client = self
end

private def handle_update(update : Update)
# @@logger.debug(update.to_pretty_json)
trigger_all_middleware(update)
trigger_commands(update)
trigger_patterns(update)
def add_handler(handler : Handler)
handler.actions.each do |action|
@handlers[action] ||= [] of Handler
@handlers[action] << handler
end
end

# Trigger events marked with the `On` annotation.
if message = update.message
trigger_event(UpdateAction::Message, update)
private def handle_update(update : Update)
actions = Helpers.actions_from_update(update)
actions.each do |action|
trigger_handlers(action, update)
end
end

if chat = message.chat
trigger_event(UpdateAction::PinnedMessage, update) if chat.pinned_message
def trigger_handlers(action : UpdateAction, update : Update)
if handlers = @handlers[action]?
handlers.each do |handler|
handler.handle_update(self, update)
end

trigger_event(UpdateAction::Text, update) if message.text
trigger_event(UpdateAction::Audio, update) if message.audio
trigger_event(UpdateAction::Document, update) if message.document
trigger_event(UpdateAction::Photo, update) if message.photo
trigger_event(UpdateAction::Sticker, update) if message.sticker
trigger_event(UpdateAction::Video, update) if message.video
trigger_event(UpdateAction::Voice, update) if message.voice
trigger_event(UpdateAction::Contact, update) if message.contact
trigger_event(UpdateAction::Location, update) if message.location
trigger_event(UpdateAction::Venue, update) if message.venue
trigger_event(UpdateAction::NewChatMembers, update) if message.new_chat_members
trigger_event(UpdateAction::LeftChatMember, update) if message.left_chat_member
trigger_event(UpdateAction::NewChatTitle, update) if message.new_chat_title
trigger_event(UpdateAction::NewChatPhoto, update) if message.new_chat_photo
trigger_event(UpdateAction::DeleteChatPhoto, update) if message.delete_chat_photo
trigger_event(UpdateAction::GroupChatCreated, update) if message.group_chat_created
trigger_event(UpdateAction::MigrateToChatId, update) if message.migrate_from_chat_id
trigger_event(UpdateAction::SupergroupChatCreated, update) if message.supergroup_chat_created
trigger_event(UpdateAction::ChannelChatCreated, update) if message.channel_chat_created
trigger_event(UpdateAction::MigrateFromChatId, update) if message.migrate_from_chat_id
trigger_event(UpdateAction::Game, update) if message.game
trigger_event(UpdateAction::VideoNote, update) if message.video_note
trigger_event(UpdateAction::Invoice, update) if message.invoice
trigger_event(UpdateAction::SuccessfulPayment, update) if message.successful_payment
trigger_event(UpdateAction::ConnectedWebsite, update) if message.connected_website
# trigger_event(UpdateAction::PassportData, update) if message.passport_data
end

trigger_event(UpdateAction::EditedMessage, update) if update.edited_message
trigger_event(UpdateAction::ChannelPost, update) if update.channel_post
trigger_event(UpdateAction::EditedChannelPost, update) if update.edited_channel_post
trigger_event(UpdateAction::InlineQuery, update) if update.inline_query
trigger_event(UpdateAction::ChosenInlineResult, update) if update.chosen_inline_result
trigger_event(UpdateAction::CallbackQuery, update) if update.callback_query
trigger_event(UpdateAction::ShippingQuery, update) if update.shipping_query
trigger_event(UpdateAction::PreCheckoutQuery, update) if update.pre_checkout_query
trigger_event(UpdateAction::Poll, update) if update.poll
trigger_event(UpdateAction::PollAnswer, update) if update.poll_answer
rescue ex
@@logger.error("Update was not handled because: #{ex.message}")
end

# Sends a json request to the Telegram Client API.
Expand Down
127 changes: 65 additions & 62 deletions src/tourmaline/context.cr
Original file line number Diff line number Diff line change
@@ -1,67 +1,70 @@
module Tourmaline
# `CommandContext` represents the data passed into a bot command. It gives access to
# the `client`, the full `update`, the `message`, the `command`
# (including the prefix), and the raw message `text`
# (not including the command).
#
# Since it can be annoying and verbose to have to type `ctx.message.method`
# every time, `CommandContext` also forwards missing methods to the message,
# update, and client in that order. So rather than calling
# `ctx.message.reply` you can just do `ctx.reply`.
record CommandContext, client : Tourmaline::Client, update : Tourmaline::Update,
message : Tourmaline::Message, command : String, text : String do
macro method_missing(call)
{% if Tourmaline::Message.has_method?(call.name) %}
message.{{call}}
{% elsif Tourmaline::Update.has_method?(call.name) %}
update.{{call}}
{% elsif Tourmaline::Client.has_method?(call.name) %}
client.{{call}}
{% else %}
{% raise "Unexpected method '##{call.name}' for class #{@type.id}" %}
{% end %}
end
module Context
end

# `EventContext` represents the data passed into an `On` event. It wraps the `update`,
# and possibly the `message`. It also includes access to the name of the event that
# triggered it.
#
# Like the other events, missing methods are forwarded to the client in this one. Since
# `message` might be nil, calls are not forwarded to it.
record EventContext, client : Tourmaline::Client, update : Tourmaline::Update,
message : Tourmaline::Message?, event : Tourmaline::UpdateAction do
macro method_missing(call)
{% if Tourmaline::Update.has_method?(call.name) %}
update.{{call}}
{% elsif Tourmaline::Client.has_method?(call.name) %}
client.{{call}}
{% else %}
{% raise "Unexpected method '##{call.name}' for class #{@type.id}" %}
{% end %}
end
end
# # `CommandContext` represents the data passed into a bot command. It gives access to
# # the `client`, the full `update`, the `message`, the `command`
# # (including the prefix), and the raw message `text`
# # (not including the command).
# #
# # Since it can be annoying and verbose to have to type `ctx.message.method`
# # every time, `CommandContext` also forwards missing methods to the message,
# # update, and client in that order. So rather than calling
# # `ctx.message.reply` you can just do `ctx.reply`.
# record CommandContext, client : Tourmaline::Client, update : Tourmaline::Update,
# message : Tourmaline::Message, command : String, text : String do
# macro method_missing(call)
# {% if Tourmaline::Message.has_method?(call.name) %}
# message.{{call}}
# {% elsif Tourmaline::Update.has_method?(call.name) %}
# update.{{call}}
# {% elsif Tourmaline::Client.has_method?(call.name) %}
# client.{{call}}
# {% else %}
# {% raise "Unexpected method '##{call.name}' for class #{@type.id}" %}
# {% end %}
# end
# end

# `CallbackQueryContext` represents the data passed into an `Action` event. It includes
# access to the `client`, the full `update`, the `message`, the callback_query
# (`query`), and the query data.
#
# Missing methods are forwarded to, in order of most important, the `query`,
# `message`, `update`, and then `client`.
record CallbackQueryContext, client : Tourmaline::Client, update : Tourmaline::Update,
message : Tourmaline::Message, query : Tourmaline::CallbackQuery, data : String do
macro method_missing(call)
{% if Tourmaline::CallbackQuery.has_method?(call.name) %}
query.{{call}}
{% elsif Tourmaline::Message.has_method?(call.name) %}
message.{{call}}
{% elsif Tourmaline::Update.has_method?(call.name) %}
update.{{call}}
{% elsif Tourmaline::Client.has_method?(call.name) %}
client.{{call}}
{% else %}
{% raise "Unexpected method '##{call.name}' for class #{@type.id}" %}
{% end %}
end
end
# # `EventContext` represents the data passed into an `On` event. It wraps the `update`,
# # and possibly the `message`. It also includes access to the name of the event that
# # triggered it.
# #
# # Like the other events, missing methods are forwarded to the client in this one. Since
# # `message` might be nil, calls are not forwarded to it.
# record EventContext, client : Tourmaline::Client, update : Tourmaline::Update,
# message : Tourmaline::Message?, event : Tourmaline::UpdateAction do
# macro method_missing(call)
# {% if Tourmaline::Update.has_method?(call.name) %}
# update.{{call}}
# {% elsif Tourmaline::Client.has_method?(call.name) %}
# client.{{call}}
# {% else %}
# {% raise "Unexpected method '##{call.name}' for class #{@type.id}" %}
# {% end %}
# end
# end

# # `CallbackQueryContext` represents the data passed into an `Action` event. It includes
# # access to the `client`, the full `update`, the `message`, the callback_query
# # (`query`), and the query data.
# #
# # Missing methods are forwarded to, in order of most important, the `query`,
# # `message`, `update`, and then `client`.
# record CallbackQueryContext, client : Tourmaline::Client, update : Tourmaline::Update,
# message : Tourmaline::Message, query : Tourmaline::CallbackQuery, data : String do
# macro method_missing(call)
# {% if Tourmaline::CallbackQuery.has_method?(call.name) %}
# query.{{call}}
# {% elsif Tourmaline::Message.has_method?(call.name) %}
# message.{{call}}
# {% elsif Tourmaline::Update.has_method?(call.name) %}
# update.{{call}}
# {% elsif Tourmaline::Client.has_method?(call.name) %}
# client.{{call}}
# {% else %}
# {% raise "Unexpected method '##{call.name}' for class #{@type.id}" %}
# {% end %}
# end
# end
end
Empty file.
118 changes: 118 additions & 0 deletions src/tourmaline/handlers/command_handler.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
require "./handler"

module Tourmaline
class CommandHandler < Handler
ANNOTATIONS = [ Command ]

getter commands : Array(String)
getter proc : Proc(CommandContext, Void)
getter prefix : String
getter anywhere : Bool

def initialize(
commands : String | Array(String),
proc : CommandContext ->,
@prefix : String = "/",
@anywhere : Bool = false,
@private_only : Bool = false
)
@commands = commands.is_a?(Array) ? commands : [commands]
@proc = ->(ctx : CommandContext) { proc.call(ctx); nil }
validate_commands(@commands)
end

def actions : Array(UpdateAction)
[ UpdateAction::Message ]
end

def call(client : Client, update : Update)
if message = update.message
if message_text = message.text
return unless message_text.size >= 2

if command = command_match(client, message)
return if @private_only && !(message.chat.type == "private")
context = CommandContext.new(client, update, message, command, message_text)
@proc.call(context)
end
end
end
end

def check_update(client : Client, update : Update) : Bool
if message = update.message
text = message.text
if command_match(client, message)
return true
end
end
false
end

private def validate_commands(commands)
commands.each do |command|
if command.match(/\s/)
raise InvalidCommandError.new(command)
end
end
end

private def command_match(client : Client, message : Message)
if text = message.text
tokens = text.split(/\s+/)

if !@anywhere && !tokens.empty?
tokens = [tokens.first]
end

tokens.each do |token|
if token.starts_with?(@prefix)
token = token[@prefix.size..-1]
if token.includes?("@")
parts = token.split("@")
if parts[0].in?(@commands) && parts[1]?
if parts[1] == client.bot_name
return parts[0]
end
end
elsif token.in?(@commands)
return token
end
end
end
end
end

class InvalidCommandError < Exception
def initialize(command)
super("Invalid command format for command '#{command}'")
end
end
end

# `CommandContext` represents the data passed into a bot command. It gives access to
# the `client`, the full `update`, the `message`, the `command`
# (including the prefix), and the raw message `text`
# (not including the command).
#
# Since it can be annoying and verbose to have to type `ctx.message.method`
# every time, `CommandContext` also forwards missing methods to the message,
# update, and client in that order. So rather than calling
# `ctx.message.reply` you can just do `ctx.reply`.
record CommandContext, client : Tourmaline::Client, update : Tourmaline::Update,
message : Tourmaline::Message, command : String, text : String do
include Tourmaline::Context

macro method_missing(call)
{% if Tourmaline::Message.has_method?(call.name) %}
message.{{call}}
{% elsif Tourmaline::Update.has_method?(call.name) %}
update.{{call}}
{% elsif Tourmaline::Client.has_method?(call.name) %}
client.{{call}}
{% else %}
{% raise "Unexpected method '##{call.name}' for class #{@type.id}" %}
{% end %}
end
end
end
Loading

0 comments on commit 73e60e8

Please sign in to comment.