birdgrinder 0.1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/birdgrinder ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require File.join(File.dirname(__FILE__), "..", "lib", "bird_grinder")
4
+
5
+ BirdGrinder::Application.processing(ARGV) do |a|
6
+
7
+ a.banner = "BirdGrinder v#{BirdGrinder.version}"
8
+
9
+ a.generator!
10
+
11
+ a.controller! :console, "Starts up a BirdGrinder friendly IRB instance"
12
+ a.controller! :client, "Controls the current BirdGrinder instance"
13
+
14
+ a.option(:force, "force the creation of the application")
15
+ a.add("create PATH", "Creates a BirdGrinder instance at a specified location") do |path, options|
16
+
17
+ path = File.expand_path(path)
18
+ if File.exists?(path) && !options[:force]
19
+ die! "The path you tried to use, #{path}, already exists. Please try another or use the --force option"
20
+ end
21
+
22
+ setup_generator path
23
+
24
+ folders 'tmp', 'config', 'handlers', 'test'
25
+ template 'boot.erb', 'config/boot.rb'
26
+ template 'setup.erb', 'config/setup.rb'
27
+ template 'settings.yml.erb', 'config/settings.yml'
28
+ template 'debug_handler.erb', 'handlers/debug_handler.rb'
29
+ template 'hello_world_handler.erb', 'handlers/hello_world_handler.rb'
30
+ template 'rakefile.erb', 'Rakefile'
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,45 @@
1
+ require 'redis'
2
+ require 'json' unless Hash.new.respond_to?(:to_json)
3
+
4
+ # An example of using bird grinder with redis
5
+ # to create a remote tweeting queue. In essence, it
6
+ # lets you schedule up tweets to be sent by any
7
+ # app on your system as a part of your birdgrinder
8
+ # process, making it easier to do responses etc.
9
+ class BirdGrinderClient
10
+ class Error < StandardError; end
11
+
12
+ @@namespace = 'bg:messages'
13
+
14
+ def self.namespace
15
+ @@namespace
16
+ end
17
+
18
+ def self.namespace=(value)
19
+ @@namespace = value
20
+ end
21
+
22
+ def initialize(*args)
23
+ begin
24
+ @redis = Redis.new(*args)
25
+ rescue Errno::ECONNREFUSED
26
+ raise Error, "Unable to connect to Redis"
27
+ end
28
+ end
29
+
30
+ def dm(user, message)
31
+ send_action 'dm', [user, message]
32
+ end
33
+
34
+ def tweet(message)
35
+ send_action 'tweet', [message]
36
+ end
37
+
38
+ def send_action(name, args)
39
+ @redis.push_tail(@@namespace, {'action' => name.to_s, 'arguments' => args}.to_json)
40
+ return true
41
+ rescue Errno::ECONNREFUSED
42
+ raise Error, "Unable to connect to Redis to store message"
43
+ end
44
+
45
+ end
@@ -0,0 +1,134 @@
1
+ module BirdGrinder
2
+ # A generic base for building handlers. It makes
3
+ # it easy to implement the most common functionality (e.g.,
4
+ # clients, checking origin and the like) without
5
+ # having to reinvent the wheel. Typically used
6
+ # as a handler for Perennial::Dispatchable
7
+ #
8
+ # @see http://github.com/Sutto/perennial/blob/master/lib/perennial/dispatchable.rb
9
+ class Base
10
+ is :loggable
11
+
12
+ cattr_accessor :handler_mapping
13
+
14
+ @@handlers = Hash.new do |h,k|
15
+ h[k] = Hash.new { |h2,k2| h2[k2] = [] }
16
+ end
17
+
18
+ class << self
19
+
20
+ # Gets event handlers for a given event.
21
+ # @param [Symbol] name the name of the event
22
+ # @return [Array<Proc>] the resultant handlers
23
+ def event_handlers_for(name)
24
+ name = name.to_sym
25
+ handlers = []
26
+ klass = self
27
+ while klass != Object
28
+ handlers += @@handlers[klass][name]
29
+ klass = klass.superclass
30
+ end
31
+ return handlers
32
+ end
33
+
34
+ # Appends a handler for the given event, either as a
35
+ # block / proc or as a symbol (for a method name) which
36
+ # will be called when the event is triggered.
37
+ #
38
+ # @param [Symbol] name the event name
39
+ # @param [Symbol] method_name if present, will call the given instance method
40
+ # @param [Proc] blk the block to call if method_name isn't given
41
+ def on_event(name, method_name = nil, &blk)
42
+ blk = proc { self.send(method_name) } if method_name.present?
43
+ @@handlers[self][name.to_sym] << blk
44
+ end
45
+
46
+ # Registers the current handler instance to be used. If not
47
+ # registered, events wont be triggered
48
+ def register!
49
+ BirdGrinder::Client.register_handler(self.new)
50
+ end
51
+
52
+ end
53
+
54
+ attr_accessor :options, :client, :user
55
+
56
+ # Handles a message / event from a dispatcher. This triggers
57
+ # each respective part of the client / lets us act on events.
58
+ #
59
+ # @param [Symbol] message the name of the event, e.g. :incoming_mention
60
+ # @param [Hash, BirdGrinder::Nash] options the options / params for the given event.
61
+ def handle(message, options)
62
+ begin
63
+ setup_details(message, options)
64
+ h = self.class.event_handlers_for(message)
65
+ h.each { |handle| self.instance_eval(&handle) }
66
+ rescue Exception => e
67
+ raise e if e.is_a?(BirdGrinder::HaltHandlerProcessing)
68
+ logger.fatal "Exception processing handlers for #{message}:"
69
+ logger.log_exception(e)
70
+ ensure
71
+ reset_details
72
+ end
73
+ end
74
+
75
+ # Tweets a given message.
76
+ #
77
+ # @see BirdGrinder::Client#tweet
78
+ # @see BirdGrinder::Tweeter#tweet
79
+ def tweet(message, opts = {})
80
+ @client && @client.tweet(message, opts)
81
+ end
82
+
83
+ # Direct Messages a specific user if the client exists.
84
+ #
85
+ # @see BirdGrinder::Client#dm
86
+ # @see BirdGrinder::Tweeter#dm
87
+ def dm(user, message, opts = {})
88
+ @client && @client.dm(user, message, opts)
89
+ end
90
+
91
+ # Replies to the last received message in the correct format.
92
+ # if the last message direct, it will send a dm otherwise it
93
+ # will send a tweet with the correct @-prefix and :in_reply_to_status_id
94
+ # set correctly so twitter users can see what it is replying
95
+ # to.
96
+ #
97
+ # @param [String] message the message to reply with
98
+ # @see http://github.com/Sutto/perennial/blob/master/lib/perennial/dispatchable.rb
99
+ def reply(message)
100
+ message = message.to_s.strip
101
+ return if @user.blank? || @client.blank? || message.blank?
102
+ if @last_message_direct
103
+ @client.dm(@user, message)
104
+ else
105
+ opts = {}
106
+ opts[:in_reply_to_status_id] = @last_message_id.to_s if @last_message_id.present?
107
+ @client.reply(@user, message, opts)
108
+ end
109
+ end
110
+
111
+ protected
112
+
113
+ def reset_details
114
+ @direct_last_message = true
115
+ @last_message_origin = nil
116
+ @last_message_id = nil
117
+ @options = nil
118
+ @user = nil
119
+ end
120
+
121
+ def setup_details(message, options)
122
+ @options = options.to_nash
123
+ @user = options.user.screen_name if options.user? && options.user.screen_name?
124
+ @user ||= options.sender_screen_name if options.sender_screen_name?
125
+ @last_message_direct = (message == :incoming_direct_message)
126
+ @last_message_id = options.id
127
+ end
128
+
129
+ def halt_handlers!
130
+ raise BirdGrinder::HaltHandlerProcessing
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,66 @@
1
+ require 'moneta'
2
+ require 'moneta/memory'
3
+
4
+ module BirdGrinder
5
+
6
+ class << self
7
+
8
+ # Gets the current cache store in use. Defaults
9
+ # to Moneta::Memory
10
+ #
11
+ # @see http://github.com/wycats/moneta
12
+ # @see Moneta::Redis
13
+ # @see Moneta::BasicFile
14
+ def cache_store
15
+ @@__cache_store__ ||= Moneta::Memory.new
16
+ end
17
+
18
+ # Sets the cache store to a hash-like object.
19
+ #
20
+ # @param [Object] cs the cache store (must be hash-like with #[] and #[]=)
21
+ def cache_store=(cs)
22
+ @@__cache_store__ = cs
23
+ end
24
+
25
+ alias use_cache cache_store=
26
+
27
+ end
28
+
29
+ module Cacheable
30
+
31
+ # Gives the target class cache_set and cache_get
32
+ # on a class and instance level. triggered by:
33
+ # include BirdGrinder::Cacheable
34
+ def self.included(parent)
35
+ parent.send(:include, Methods)
36
+ parent.send(:extend, Methods)
37
+ end
38
+
39
+ module Methods
40
+
41
+ # Gets the value for the given key from the
42
+ # cache store if the cache store is set.
43
+ #
44
+ # @param [Symbol] key the key to get the value for
45
+ # @return [Object] the value for the given key
46
+ # @see BirdGrinder.cache_store
47
+ def cache_get(key)
48
+ cs = BirdGrinder.cache_store
49
+ cs && cs[key.to_s]
50
+ end
51
+
52
+ # Attempts to set the value for a given key in the
53
+ # current cache_store.
54
+ #
55
+ # @param [Symbol] key the key to set the value for
56
+ # @param [Object] value the value for said key
57
+ # @see BirdGrinder.cache_store
58
+ def cache_set(key, value)
59
+ cs = BirdGrinder.cache_store
60
+ cs && cs[key.to_s] = value
61
+ end
62
+
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,136 @@
1
+ require 'ostruct'
2
+
3
+ module BirdGrinder
4
+ # Glue between BirdGrinder::Tweeter and associated
5
+ # handlers to make it function in an evented fashion.
6
+ #
7
+ # The client basically brings it all together. It acts
8
+ # as a delegate for the tweeter and converts received
9
+ # results into dispatchable form for each handler.
10
+ #
11
+ # @see BirdGrinder::Tweeter
12
+ # @see BirdGrinder::Base
13
+ # @see http://github.com/Sutto/perennial/blob/master/lib/perennial/dispatchable.rb
14
+ class Client
15
+ is :loggable, :dispatchable, :cacheable
16
+
17
+ cattr_accessor :current
18
+ attr_reader :tweeter
19
+
20
+ # Initializes this client and creates a new, associated
21
+ # tweeter instance with this client set as the delegate.
22
+ # Also, for all of this clients handlers it will call
23
+ # client= if defined.
24
+ #
25
+ # Lastly, it updates BirdGrinder::Client.current to point
26
+ # to itself.
27
+ #
28
+ # @see BirdGrinder::Tweeter#initialize
29
+ # @see http://github.com/Sutto/perennial/blob/master/lib/perennial/dispatchable.rb
30
+ def initialize
31
+ logger.debug "Initializing client..."
32
+ @tweeter = BirdGrinder::Tweeter.new(self)
33
+ logger.debug "Notifying handlers of the client"
34
+ handlers.each { |h| h.client = self if h.respond_to?(:client=) }
35
+ self.current = self
36
+ end
37
+
38
+ # Forwards a given message type (with options) to each handler,
39
+ # storing the current id if changed.
40
+ def receive_message(type, options = BirdGrinder::Nash.new)
41
+ logger.debug "receiving message: #{type.inspect} - #{options.id}"
42
+ dispatch(type.to_sym, options)
43
+ update_stored_id_for(type, options.id)
44
+ end
45
+
46
+ # Fetches all direct messages and mentions and also schedules
47
+ # the next set of updates.
48
+ #
49
+ # @todo Schedule future fetch only when others are completed.
50
+ def update_all
51
+ fetch :direct_message, :mention
52
+ update_and_schedule_fetch
53
+ end
54
+
55
+ # Searches for a given query
56
+ #
57
+ # @see BirdGrinder::Tweeter#search
58
+ def search(q, opts = {})
59
+ @tweeter.search(q, opts)
60
+ end
61
+
62
+ # Tweets some text as the current user
63
+ #
64
+ # @see BirdGrinder::Tweeter#tweet
65
+ def tweet(text, opts = {})
66
+ @tweeter.tweet(text, opts)
67
+ end
68
+
69
+ # Direct messages a given user with the given text
70
+ #
71
+ # @see BirdGrinder::Tweeter#dm
72
+ def dm(user, text, opts = {})
73
+ @tweeter.dm(user, text, opts)
74
+ end
75
+
76
+ # Replies to a given user with the given text.
77
+ #
78
+ # @see BirdGrinder::Tweeter#reply
79
+ def reply(user, text, opts = {})
80
+ @tweeter.reply(user, text, opts)
81
+ end
82
+
83
+ # Starts processing as a new client instance. The main
84
+ # entry point into the programs event loop.
85
+ # Once started, will invoke the once_running hook.
86
+ def self.run
87
+ logger.info "Preparing to start BirdGrinder"
88
+ client = self.new
89
+ EventMachine.run do
90
+ client.update_all
91
+ BirdGrinder::Loader.invoke_hooks!(:once_running)
92
+ end
93
+ end
94
+
95
+ # Stops the event loop so the program can be stopped.
96
+ def self.stop
97
+ EventMachine.stop_event_loop
98
+ end
99
+
100
+ protected
101
+
102
+ def update_and_schedule_fetch
103
+ @last_run_at ||= Time.now
104
+ next_run_time = @last_run_at + BirdGrinder::Settings.check_every
105
+ next_time_spacing = [0, (next_run_time - @last_run_at).to_i].max
106
+ @last_run_at = Time.now
107
+ EventMachine.add_timer(next_time_spacing) { update_all }
108
+ end
109
+
110
+ def stored_id_for(type)
111
+ Integer(cache_get("#{type}-last-id"))
112
+ rescue ArgumentError
113
+ return -1
114
+ end
115
+
116
+ def update_stored_id_for(type, id)
117
+ return if id.blank?
118
+ last_id = stored_id_for(type)
119
+ cache_set("#{type}-last-id", id) if last_id.blank? || id > last_id
120
+ end
121
+
122
+ def fetch(*items)
123
+ items.each do |n|
124
+ fetch_latest :"#{n}s", :"incoming_#{n}"
125
+ end
126
+ end
127
+
128
+ def fetch_latest(name, type)
129
+ options = {}
130
+ id = stored_id_for(type)
131
+ options[:since_id] = id unless id.blank?
132
+ @tweeter.send(name, options)
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,84 @@
1
+ require 'set'
2
+
3
+ module BirdGrinder
4
+ # A simple, method to command mapping for handlers.
5
+ # E.g.
6
+ #
7
+ # class X < BirdGrinder::CommandHandler
8
+ # exposes :hello
9
+ # def hello(name)
10
+ # reply "Why hello there yourself!"
11
+ # end
12
+ # end
13
+ #
14
+ # When registerted, X will look for tweets that are of the form "@bot-name hello"
15
+ # or direct mentions with "hello" at the start (or if command_prefix is set to,
16
+ # for example, !, "!hello") and reply.
17
+ #
18
+ # Used for implementing the most common cases of bots that respond to commands.
19
+ class CommandHandler < Base
20
+
21
+ class_inheritable_accessor :exposed_methods, :command_prefix
22
+ self.command_prefix = ""
23
+ self.exposed_methods = Set.new
24
+
25
+ class << self
26
+
27
+ # Marks a set of method names as being available
28
+ # @param [Array<Symbol>] args the method names to expose
29
+ def exposes(*args)
30
+ args.each { |name| exposed_methods << name.to_sym }
31
+ end
32
+
33
+ # Gets a regexp for easy matching
34
+ #
35
+ # @return [Regexp] BirdGrinder::CommandHandler.command_prefix in regexp-form
36
+ def prefix_regexp
37
+ /^#{command_prefix}/
38
+ end
39
+
40
+ end
41
+
42
+ # Default events
43
+ on_event :incoming_mention, :check_for_commands
44
+ on_event :incoming_direct_message, :check_for_commands
45
+
46
+ # Checks in incoming mentions and direct messages for those
47
+ # that correctly match the format. If it's found, it will
48
+ # call the given method with the result of the message,
49
+ # minus the command, as an argument.
50
+ def check_for_commands
51
+ data, command = nil, nil
52
+ if !@last_message_direct
53
+ logger.debug "Checking for command in mention"
54
+ split_message = options.text.split(" ", 3)
55
+ name, command, data = split_message
56
+ if name.downcase != "@#{BirdGrinder::Settings.username}".downcase
57
+ logger.debug "Command is a mention but doesn't start with the username"
58
+ return
59
+ end
60
+ else
61
+ logger.debug "Checking for command in direct message"
62
+ command, data = options.text.split(" ", 2)
63
+ end
64
+ if (command_name = extract_command_name(command)).present?
65
+ logger.info "Processing command '#{command_name}' for #{user}"
66
+ send(command_name, data.to_s) if respond_to?(command_name)
67
+ end
68
+ end
69
+
70
+ # Given a prefix, e.g. "!awesome", will return the associated
71
+ # method name iff it is exposed.
72
+ #
73
+ # @param [String] the command to check
74
+ # @return [Symbol] the resultant method name or nil if not found.
75
+ def extract_command_name(command)
76
+ re = self.class.prefix_regexp
77
+ if command =~ re
78
+ method_name = command.gsub(re, "").underscore.to_sym
79
+ return method_name if exposed_methods.include?(method_name)
80
+ end
81
+ end
82
+
83
+ end
84
+ end