campfire-bot_alexchee 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.autotest +11 -0
- data/.gitignore +6 -0
- data/Gemfile +12 -0
- data/README.textile +62 -0
- data/TODO +72 -0
- data/bin/campfire-bot +59 -0
- data/campfire-bot.gemspec +24 -0
- data/cfbot-stop.sh +8 -0
- data/config.example.yml +32 -0
- data/lib/bot.rb +194 -0
- data/lib/event.rb +114 -0
- data/lib/message.rb +30 -0
- data/lib/plugin.rb +77 -0
- data/lib/version.rb +3 -0
- data/plugins-example/fun.rb +95 -0
- data/spec/command_spec.rb +96 -0
- data/spec/plugin_spec.rb +43 -0
- data/spec/spec.opts +1 -0
- data/tmp/.gitignore +0 -0
- data/vendor/escape/ChangeLog +30 -0
- data/vendor/escape/Makefile +5 -0
- data/vendor/escape/README +81 -0
- data/vendor/escape/VERSION +1 -0
- data/vendor/escape/escape.rb +302 -0
- data/vendor/escape/install.rb +109 -0
- data/vendor/escape/misc/README.erb +85 -0
- data/vendor/escape/rdoc/classes/Escape/HTMLAttrValue.html +113 -0
- data/vendor/escape/rdoc/classes/Escape/HTMLEscaped.html +113 -0
- data/vendor/escape/rdoc/classes/Escape/PercentEncoded.html +113 -0
- data/vendor/escape/rdoc/classes/Escape/ShellEscaped.html +113 -0
- data/vendor/escape/rdoc/classes/Escape/StringWrapper.html +242 -0
- data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000029.html +18 -0
- data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000030.html +18 -0
- data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000031.html +18 -0
- data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000032.html +18 -0
- data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000033.html +18 -0
- data/vendor/escape/rdoc/classes/Escape/StringWrapper.src/M000035.html +18 -0
- data/vendor/escape/rdoc/classes/Escape.html +427 -0
- data/vendor/escape/rdoc/classes/Escape.src/M000022.html +19 -0
- data/vendor/escape/rdoc/classes/Escape.src/M000023.html +32 -0
- data/vendor/escape/rdoc/classes/Escape.src/M000024.html +24 -0
- data/vendor/escape/rdoc/classes/Escape.src/M000025.html +19 -0
- data/vendor/escape/rdoc/classes/Escape.src/M000026.html +48 -0
- data/vendor/escape/rdoc/classes/Escape.src/M000027.html +19 -0
- data/vendor/escape/rdoc/classes/Escape.src/M000028.html +19 -0
- data/vendor/escape/rdoc/classes/TestEscapeHTML.html +182 -0
- data/vendor/escape/rdoc/classes/TestEscapeHTML.src/M000008.html +18 -0
- data/vendor/escape/rdoc/classes/TestEscapeHTML.src/M000009.html +18 -0
- data/vendor/escape/rdoc/classes/TestEscapeHTML.src/M000010.html +18 -0
- data/vendor/escape/rdoc/classes/TestEscapeHTML.src/M000011.html +18 -0
- data/vendor/escape/rdoc/classes/TestEscapePercentEncoded.html +182 -0
- data/vendor/escape/rdoc/classes/TestEscapePercentEncoded.src/M000012.html +18 -0
- data/vendor/escape/rdoc/classes/TestEscapePercentEncoded.src/M000013.html +19 -0
- data/vendor/escape/rdoc/classes/TestEscapePercentEncoded.src/M000014.html +20 -0
- data/vendor/escape/rdoc/classes/TestEscapePercentEncoded.src/M000015.html +22 -0
- data/vendor/escape/rdoc/classes/TestEscapeShellEscaped.html +167 -0
- data/vendor/escape/rdoc/classes/TestEscapeShellEscaped.src/M000016.html +18 -0
- data/vendor/escape/rdoc/classes/TestEscapeShellEscaped.src/M000017.html +20 -0
- data/vendor/escape/rdoc/classes/TestEscapeShellEscaped.src/M000018.html +20 -0
- data/vendor/escape/rdoc/classes/TestEscapeStringWrapper.html +167 -0
- data/vendor/escape/rdoc/classes/TestEscapeStringWrapper.src/M000019.html +20 -0
- data/vendor/escape/rdoc/classes/TestEscapeStringWrapper.src/M000020.html +24 -0
- data/vendor/escape/rdoc/classes/TestEscapeStringWrapper.src/M000021.html +22 -0
- data/vendor/escape/rdoc/files/escape_rb.html +136 -0
- data/vendor/escape/rdoc/files/install_rb.html +250 -0
- data/vendor/escape/rdoc/files/install_rb.src/M000001.html +23 -0
- data/vendor/escape/rdoc/files/install_rb.src/M000002.html +31 -0
- data/vendor/escape/rdoc/files/install_rb.src/M000003.html +27 -0
- data/vendor/escape/rdoc/files/install_rb.src/M000004.html +27 -0
- data/vendor/escape/rdoc/files/install_rb.src/M000005.html +21 -0
- data/vendor/escape/rdoc/files/install_rb.src/M000006.html +23 -0
- data/vendor/escape/rdoc/files/install_rb.src/M000007.html +21 -0
- data/vendor/escape/rdoc/files/test/test-escape_rb.html +109 -0
- data/vendor/escape/rdoc/fr_class_index.html +36 -0
- data/vendor/escape/rdoc/fr_file_index.html +29 -0
- data/vendor/escape/rdoc/fr_method_index.html +61 -0
- data/vendor/escape/rdoc/index.html +24 -0
- data/vendor/escape/rdoc/rdoc-style.css +208 -0
- data/vendor/escape/test/test-escape.rb +90 -0
- metadata +209 -0
data/.autotest
ADDED
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.textile
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
h1. Campfire Bot
|
|
2
|
+
|
|
3
|
+
This is a bot for 37 Signals' Campfire chat service.
|
|
4
|
+
|
|
5
|
+
It has plugin support so you can make your bot do whatever you want.
|
|
6
|
+
|
|
7
|
+
h2. Installation
|
|
8
|
+
|
|
9
|
+
@gem install campfire-bot@
|
|
10
|
+
|
|
11
|
+
h3. Download
|
|
12
|
+
|
|
13
|
+
Download the source from "github":http://github.com/joshwand/campfire-bot
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
h3. Configuration
|
|
17
|
+
|
|
18
|
+
Create a @config.yml@ in the root of the source directory. Use @config.example.yml@ as an example.
|
|
19
|
+
|
|
20
|
+
h2. Usage
|
|
21
|
+
|
|
22
|
+
To run the bot, run @campfire-bot@ with the following parameters:
|
|
23
|
+
|
|
24
|
+
-c config FILE path to config file
|
|
25
|
+
-e ENVIRONMENT_NAME environment name to load from config file
|
|
26
|
+
-p plugin PLUGIN_PATH path to your plugins
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
|
|
30
|
+
@campfire-bot -c config.yml -e production -p ./plugins@
|
|
31
|
+
|
|
32
|
+
h2. Plugins
|
|
33
|
+
|
|
34
|
+
See plugins-example/fun.rb for an example plugin.
|
|
35
|
+
|
|
36
|
+
The old plugins that used to be bundled with campfire-bot are now in their own repo: "campfire-bot-plugins":https://github.com/joshwand/campfire-bot-plugins
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
h2. Known issues
|
|
40
|
+
|
|
41
|
+
h3. [BUG] Bus Error
|
|
42
|
+
|
|
43
|
+
This is really an issue with EventMachine. If you are using this on a machine that has MacPorts, you'll need to make sure that you build both ruby and eventmachine linked against the openssl in /opt/local, not /usr/local.
|
|
44
|
+
|
|
45
|
+
h3. warning: peer certificate won't be verified in this SSL session
|
|
46
|
+
|
|
47
|
+
A bug in net/http. Noisy, but harmless.
|
|
48
|
+
|
|
49
|
+
h2. Original Author
|
|
50
|
+
|
|
51
|
+
Tim Riley - "github":http://github.com/timriley | "www":http://openmonkey.com/ | "email":mailto:tim@openmonkey.com
|
|
52
|
+
|
|
53
|
+
h2. Maintainer
|
|
54
|
+
|
|
55
|
+
* Josh Wand - "github":http://github.com/joshwand
|
|
56
|
+
|
|
57
|
+
h3. Contributors
|
|
58
|
+
|
|
59
|
+
* Marcel M. Cary - "github":http://github.com/mcary
|
|
60
|
+
* Hugh Evans - "github":http://github.com/artpop
|
|
61
|
+
* Sean O'Dowd - "github":http://github.com/seanodowd
|
|
62
|
+
* Andrew Erickson - "github":http://github.com/aerickson
|
data/TODO
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
- add chronic integration for interval-based events: at_time("every day at 9am", :method_name)
|
|
2
|
+
|
|
3
|
+
- per-plugin config files (inside a config-specific dir) - best not to pollute a single YAML file.
|
|
4
|
+
|
|
5
|
+
- get it to work with the newest version of mechanize.
|
|
6
|
+
|
|
7
|
+
/opt/local/lib/ruby/gems/1.8/gems/timriley-tinder-1.1.9/lib/tinder/mechanize_ext.rb:6: undefined method `set_headers' for class `WWW::Mechanize' (NameError)
|
|
8
|
+
from /opt/local/lib/ruby/vendor_ruby/1.8/rubygems/custom_require.rb:27:in `gem_original_require'
|
|
9
|
+
from /opt/local/lib/ruby/vendor_ruby/1.8/rubygems/custom_require.rb:27:in `require'
|
|
10
|
+
from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:510:in `require'
|
|
11
|
+
from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:355:in `new_constants_in'
|
|
12
|
+
from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:510:in `require'
|
|
13
|
+
from /opt/local/lib/ruby/gems/1.8/gems/timriley-tinder-1.1.9/lib/tinder.rb:10
|
|
14
|
+
from /opt/local/lib/ruby/gems/1.8/gems/timriley-tinder-1.1.9/lib/tinder.rb:10:in `each'
|
|
15
|
+
from /opt/local/lib/ruby/gems/1.8/gems/timriley-tinder-1.1.9/lib/tinder.rb:10
|
|
16
|
+
from /opt/local/lib/ruby/vendor_ruby/1.8/rubygems/custom_require.rb:32:in `gem_original_require'
|
|
17
|
+
from /opt/local/lib/ruby/vendor_ruby/1.8/rubygems/custom_require.rb:32:in `require'
|
|
18
|
+
from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:510:in `require'
|
|
19
|
+
from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:355:in `new_constants_in'
|
|
20
|
+
from /opt/local/lib/ruby/gems/1.8/gems/activesupport-2.1.2/lib/active_support/dependencies.rb:510:in `require'
|
|
21
|
+
from ./script/../lib/bot.rb:13
|
|
22
|
+
from ./script/bot:7:in `require'
|
|
23
|
+
from ./script/bot:7
|
|
24
|
+
|
|
25
|
+
- need flood control for bot plugins
|
|
26
|
+
|
|
27
|
+
- plugins need callbacks, like on_join, bot_loaded, etc. Especially one that is run whenever control+c is caught (for plugins to clean up)
|
|
28
|
+
|
|
29
|
+
- gem-ify the bot and have it provide an executable to generate an application directory
|
|
30
|
+
like rails
|
|
31
|
+
|
|
32
|
+
- move the plugins into their own repository and provide a way for them to be installed
|
|
33
|
+
into the app dir above (like script/plugin install)
|
|
34
|
+
|
|
35
|
+
- use a message handler chain in the same way that activesupport uses callback chains.
|
|
36
|
+
|
|
37
|
+
- provide a way for plugins to buffer themselves, so they don't run too many times
|
|
38
|
+
|
|
39
|
+
- buffer the "speak" calls so that they can be uniq'ed before printed to the channel?
|
|
40
|
+
|
|
41
|
+
- plugins should be able to register respond_to blocks with a notion of priority.
|
|
42
|
+
eg. a low-priority command can say it doesn't need to be processed if something else matches.
|
|
43
|
+
conversely, a high-priority command can cause to bot to stop processing the message once it has matched.
|
|
44
|
+
|
|
45
|
+
- create a plugin that pulls in unfuddle ticket reports, eg. !unfuddle project_name active_tickets
|
|
46
|
+
|
|
47
|
+
- modify the austin powers plugin to be a generic imdb quote puller. eg. !imdb austin_powers
|
|
48
|
+
|
|
49
|
+
- unit tests!
|
|
50
|
+
|
|
51
|
+
- help plugin-- extend plugin class to provide help for each registered command
|
|
52
|
+
|
|
53
|
+
- room.log method, can be called from plugins - store logs in ./log/*.log
|
|
54
|
+
|
|
55
|
+
DONE
|
|
56
|
+
|
|
57
|
+
- multi-room support
|
|
58
|
+
|
|
59
|
+
- need to catch timeout errors and do something sensible, like wait and retry the connection
|
|
60
|
+
|
|
61
|
+
http://www.google.com/search?q=ruby%20rescue%20Timeout::Error
|
|
62
|
+
http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/136555
|
|
63
|
+
http://jerith.livejournal.com/40063.html
|
|
64
|
+
http://rails.lighthouseapp.com/projects/8994/tickets/6-patch-activeresource-connection-should-rescue-from-timeout-error
|
|
65
|
+
|
|
66
|
+
- alternative addressing syntax (!command)
|
|
67
|
+
|
|
68
|
+
- need to catch no-network errors and return a sensible error message:
|
|
69
|
+
|
|
70
|
+
/opt/local/lib/ruby/1.8/net/http.rb:560:in `initialize': Network is unreachable - connect(2) (Errno::ENETUNREACH)
|
|
71
|
+
|
|
72
|
+
- plugin: svn commit notification (multiple repos)
|
data/bin/campfire-bot
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Run this script with the environment as the only argument. eg. ./script/bot development
|
|
4
|
+
# BOT_ENVIRONMENT = ARGV.first
|
|
5
|
+
# BOT_ROOT = File.join(File.dirname(__FILE__), '..')
|
|
6
|
+
|
|
7
|
+
require 'rubygems'
|
|
8
|
+
# require 'bundler/setup'
|
|
9
|
+
require 'optparse'
|
|
10
|
+
require 'bot'
|
|
11
|
+
# require File.join(BOT_ROOT, 'lib', 'bot')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
options = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
optparse = OptionParser.new do |opts|
|
|
19
|
+
opts.banner = "Usage: campfire-bot [options]"
|
|
20
|
+
|
|
21
|
+
opts.on("-cCONFIG_FILE", "--config CONFIG_FILE", "path to your config file") do |config|
|
|
22
|
+
options[:config_file] = config
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
opts.on("-eENVIRONMENT", "--environment ENVIRONMENT", "the environment name as specified in the config file") do |env|
|
|
26
|
+
options[:env] = env
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
opts.on( "-pPLUGIN_PATH", "--plugin_path PLUGIN_PATH",
|
|
30
|
+
"path to your plugins folder" ) do |pp|
|
|
31
|
+
options[:plugin_path] = pp
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
opts.on_tail("-h", "--help", "prints this help text") do
|
|
35
|
+
p opts
|
|
36
|
+
exit
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
optparse.parse!
|
|
41
|
+
|
|
42
|
+
mandatory = [:config_file, :env, :plugin_path]
|
|
43
|
+
missing = mandatory.select{ |param| options[param].nil? }
|
|
44
|
+
if not missing.empty?
|
|
45
|
+
puts "Missing options: #{missing.join(', ')}"
|
|
46
|
+
puts optparse
|
|
47
|
+
exit
|
|
48
|
+
end
|
|
49
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
|
50
|
+
puts $!.to_s
|
|
51
|
+
puts optparse
|
|
52
|
+
exit
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
bot.create(options[:config_file], options[:env], options[:plugin_path])
|
|
57
|
+
|
|
58
|
+
bot.connect
|
|
59
|
+
bot.run
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "campfire-bot_alexchee"
|
|
7
|
+
s.version = CampfireBot::VERSION
|
|
8
|
+
s.authors = ["Josh Wand", "Tim Riley"]
|
|
9
|
+
s.email = ["josh@joshwand.com"]
|
|
10
|
+
s.homepage = "https://github.com/alexchee/campfire-bot"
|
|
11
|
+
s.summary = %q{This is a bot for 37 Signals’ Campfire chat service.}
|
|
12
|
+
s.description = s.summary
|
|
13
|
+
|
|
14
|
+
s.files = `git ls-files`.split("\n")
|
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
17
|
+
s.require_paths = ["lib"]
|
|
18
|
+
|
|
19
|
+
s.add_dependency 'twitter-stream', '~> 0.1'
|
|
20
|
+
s.add_dependency 'tinder', '>= 1.4.3'
|
|
21
|
+
s.add_dependency 'activesupport', '>= 3.0.3'
|
|
22
|
+
s.add_dependency 'logging', '~> 1.4.3'
|
|
23
|
+
s.add_dependency 'i18n', '~> 0.5.0'
|
|
24
|
+
end
|
data/cfbot-stop.sh
ADDED
data/config.example.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
development:
|
|
2
|
+
site: campfire_subdomain_name
|
|
3
|
+
use_ssl: true
|
|
4
|
+
rooms:
|
|
5
|
+
- Main room
|
|
6
|
+
- Other room
|
|
7
|
+
api_key: your_api_key
|
|
8
|
+
nickname: Bot
|
|
9
|
+
fullname: Bot Name
|
|
10
|
+
password: not_actually_needed
|
|
11
|
+
twitter_feed: 'http://search.twitter.com/search.atom?q=from%3Atimriley+OR+from%3Ahughevans+OR+from%3Aschlick+OR+from%3Aseanodowd'
|
|
12
|
+
twitter_hide_replies: false
|
|
13
|
+
enable_plugins:
|
|
14
|
+
- xkcd
|
|
15
|
+
- basecamp
|
|
16
|
+
production:
|
|
17
|
+
site: campfire_subdomain_name
|
|
18
|
+
use_ssl: true
|
|
19
|
+
rooms:
|
|
20
|
+
- Company Chat
|
|
21
|
+
username: foo@bar.com
|
|
22
|
+
nickname: Bot Name
|
|
23
|
+
password: password
|
|
24
|
+
twitter_feed: 'http://search.twitter.com/search.atom?q=from%3Atimriley+OR+from%3Ahughevans+OR+from%3Aschlick+OR+from%3Aseanodowd'
|
|
25
|
+
twitter_hide_replies: false
|
|
26
|
+
enable_plugins:
|
|
27
|
+
- xkcd
|
|
28
|
+
- basecamp
|
|
29
|
+
svn_urls:
|
|
30
|
+
- http://your.svn/url/
|
|
31
|
+
svn_username: your_svn_username
|
|
32
|
+
svn_password: your_svn_password
|
data/lib/bot.rb
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# External Libs
|
|
2
|
+
require 'rubygems'
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/hash_with_indifferent_access'
|
|
5
|
+
require 'active_support/core_ext'
|
|
6
|
+
require 'yaml'
|
|
7
|
+
require 'eventmachine'
|
|
8
|
+
require 'logging'
|
|
9
|
+
require 'fileutils'
|
|
10
|
+
|
|
11
|
+
# Local Libs
|
|
12
|
+
require "message"
|
|
13
|
+
require "event"
|
|
14
|
+
require "plugin"
|
|
15
|
+
|
|
16
|
+
require 'tinder'
|
|
17
|
+
|
|
18
|
+
module CampfireBot
|
|
19
|
+
class Bot
|
|
20
|
+
# this is necessary so the room and campfire objects can be accessed by plugins.
|
|
21
|
+
include Singleton
|
|
22
|
+
|
|
23
|
+
# FIXME - these will be invalid if disconnected. handle this.
|
|
24
|
+
attr_reader :campfire, :rooms, :config, :log, :environment_name
|
|
25
|
+
|
|
26
|
+
def create(config_file, environment_name, plugin_path)
|
|
27
|
+
@config = YAML::load(File.read(config_file))[environment_name]
|
|
28
|
+
@plugin_path = plugin_path
|
|
29
|
+
@environment_name = environment_name
|
|
30
|
+
|
|
31
|
+
@root_logger = Logging.logger["CampfireBot"]
|
|
32
|
+
@log = Logging.logger[self]
|
|
33
|
+
|
|
34
|
+
# TODO much of this should be configurable per environment
|
|
35
|
+
|
|
36
|
+
@root_logger.add_appenders Logging.appenders.rolling_file("#{@environment_name}.log",
|
|
37
|
+
:layout => Logging.layouts.pattern(:pattern => "%d | %-6l | %-12c | %m\n"),
|
|
38
|
+
:age => 'daily',
|
|
39
|
+
:keep => 7)
|
|
40
|
+
@root_logger.level = @config['log_level'] rescue :info
|
|
41
|
+
|
|
42
|
+
@timeouts = 0
|
|
43
|
+
|
|
44
|
+
@rooms = {}
|
|
45
|
+
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# def initialize
|
|
49
|
+
# end
|
|
50
|
+
|
|
51
|
+
def connect
|
|
52
|
+
load_plugins unless !@config['enable_plugins']
|
|
53
|
+
begin
|
|
54
|
+
join_rooms
|
|
55
|
+
rescue Errno::ENETUNREACH, SocketError => e
|
|
56
|
+
@log.fatal "We had trouble connecting to the network: #{e.class}: #{e.message}"
|
|
57
|
+
abort "We had trouble connecting to the network: #{e.class}: #{e.message}"
|
|
58
|
+
rescue Exception => e
|
|
59
|
+
@log.fatal "Unhandled exception while joining rooms: #{e.class}: #{e.message} \n #{$!.backtrace.join("\n")}"
|
|
60
|
+
abort "Unhandled exception while joining rooms: #{e.class}: #{e.message} \n #{$!.backtrace.join("\n")}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run(interval = 5)
|
|
65
|
+
catch(:stop_listening) do
|
|
66
|
+
trap('INT') { throw :stop_listening }
|
|
67
|
+
|
|
68
|
+
# since room#listen blocks, stick it in its own thread
|
|
69
|
+
@rooms.each_pair do |room_name, room|
|
|
70
|
+
Thread.new do
|
|
71
|
+
begin
|
|
72
|
+
room.listen(:timeout => 8) do |raw_msg|
|
|
73
|
+
handle_message(CampfireBot::Message.new(raw_msg.merge({:room => room})))
|
|
74
|
+
end
|
|
75
|
+
rescue Exception => e
|
|
76
|
+
trace = e.backtrace.join("\n")
|
|
77
|
+
abort "something went wrong! #{e.message}\n #{trace}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
loop do
|
|
83
|
+
begin
|
|
84
|
+
@rooms.each_pair do |room_name, room|
|
|
85
|
+
|
|
86
|
+
# I assume if we reach here, all the network-related activity has occured successfully
|
|
87
|
+
# and that we're outside of the retry-cycle
|
|
88
|
+
@timeouts = 0
|
|
89
|
+
|
|
90
|
+
# Here's how I want it to look
|
|
91
|
+
# @room.listen.each { |m| EventHandler.handle_message(m) }
|
|
92
|
+
# EventHanlder.handle_time(optional_arg = Time.now)
|
|
93
|
+
|
|
94
|
+
# Run time-oriented events
|
|
95
|
+
Plugin.registered_intervals.each do |handler|
|
|
96
|
+
begin
|
|
97
|
+
handler.run(CampfireBot::Message.new(:room => room))
|
|
98
|
+
rescue
|
|
99
|
+
@log.error "error running #{handler.inspect}: #{$!.class}: #{$!.message} \n #{$!.backtrace.join("\n")}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
Plugin.registered_times.each_with_index do |handler, index|
|
|
104
|
+
begin
|
|
105
|
+
Plugin.registered_times.delete_at(index) if handler.run
|
|
106
|
+
rescue
|
|
107
|
+
@log.error "error running #{handler.inspect}: #{$!.class}: #{$!.message}, \n #{$!.backtrace.join("\n")}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
end
|
|
112
|
+
STDOUT.flush
|
|
113
|
+
sleep interval
|
|
114
|
+
rescue Timeout::Error => e
|
|
115
|
+
if @timeouts < 5
|
|
116
|
+
sleep(5 * @timeouts)
|
|
117
|
+
@timeouts += 1
|
|
118
|
+
retry
|
|
119
|
+
else
|
|
120
|
+
raise e.message
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def join_rooms
|
|
130
|
+
join_rooms_as_user
|
|
131
|
+
@log.info "Joined all rooms."
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def join_rooms_as_user
|
|
135
|
+
@campfire = Tinder::Campfire.new(@config['site'], :token => @config['api_key'])
|
|
136
|
+
|
|
137
|
+
@config['rooms'].each do |room_name|
|
|
138
|
+
@rooms[room_name] = @campfire.find_room_by_name(room_name)
|
|
139
|
+
raise Exception.new("couldn't find a room named #{room_name}!") if @rooms[room_name].nil?
|
|
140
|
+
res = @rooms[room_name].join
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def load_plugins
|
|
145
|
+
@config['enable_plugins'].each do |plugin_name|
|
|
146
|
+
load "#{@plugin_path}/#{plugin_name}.rb"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# And instantiate them
|
|
150
|
+
Plugin.registered_plugins.each_pair do |name, klass|
|
|
151
|
+
@log.info "loading plugin: #{name}"
|
|
152
|
+
STDOUT.flush
|
|
153
|
+
Plugin.registered_plugins[name] = klass.new
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def handle_message(message)
|
|
158
|
+
# puts message.inspect
|
|
159
|
+
|
|
160
|
+
case message[:type]
|
|
161
|
+
when "KickMessage"
|
|
162
|
+
if message[:user][:id] == @campfire.me[:id]
|
|
163
|
+
@log.info "got kicked... rejoining after 10 seconds"
|
|
164
|
+
sleep 10
|
|
165
|
+
join_rooms_as_user
|
|
166
|
+
@log.info "rejoined room."
|
|
167
|
+
return
|
|
168
|
+
end
|
|
169
|
+
when "TimestampMessage", "AdvertisementMessage"
|
|
170
|
+
return
|
|
171
|
+
when "TextMessage", "PasteMessage"
|
|
172
|
+
# only process non-bot messages
|
|
173
|
+
unless message[:user][:id] == @campfire.me[:id]
|
|
174
|
+
@log.info "#{message[:person]} | #{message[:message]}"
|
|
175
|
+
%w(commands speakers messages).each do |type|
|
|
176
|
+
Plugin.send("registered_#{type}").each do |handler|
|
|
177
|
+
begin
|
|
178
|
+
handler.run(message)
|
|
179
|
+
rescue Exception => e
|
|
180
|
+
@log.error "error running #{handler.inspect}: #{$!.class}: #{$!.message} \n #{$!.backtrace.join("\n")}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
else
|
|
186
|
+
@log.debug "got message of type #{message['type']} -- discarding"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def bot
|
|
193
|
+
CampfireBot::Bot.instance
|
|
194
|
+
end
|
data/lib/event.rb
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
module CampfireBot
|
|
2
|
+
module Event
|
|
3
|
+
|
|
4
|
+
# This is an abstract base class for all event types, not to be used directly.
|
|
5
|
+
class EventHandler
|
|
6
|
+
|
|
7
|
+
attr_reader :kind, :matcher, :plugin, :method
|
|
8
|
+
|
|
9
|
+
def self.handles(event_type)
|
|
10
|
+
@kind = event_type
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(matcher, plugin_name, method_name)
|
|
14
|
+
@matcher = matcher
|
|
15
|
+
@plugin = plugin_name
|
|
16
|
+
@method = method_name
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run(msg, force = false)
|
|
20
|
+
if force || match?(msg)
|
|
21
|
+
Plugin.registered_plugins[@plugin].send(@method, filter_message(msg))
|
|
22
|
+
else
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def filter_message(msg)
|
|
30
|
+
msg
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Command < EventHandler
|
|
35
|
+
handles :messages
|
|
36
|
+
|
|
37
|
+
def match?(msg)
|
|
38
|
+
(
|
|
39
|
+
msg[:message][0..0] == '!' ||
|
|
40
|
+
msg[:message] =~ Regexp.new("^#{bot.config['nickname']}(,|:)", Regexp::IGNORECASE)
|
|
41
|
+
) &&
|
|
42
|
+
msg[:message].gsub(/^\!/, '').gsub(Regexp.new("^#{bot.config['nickname']}(,|:)?\\s*", Regexp::IGNORECASE), '').split(' ')[0].to_s.downcase == @matcher.downcase
|
|
43
|
+
# FIXME - the above should be just done with one regexp to pull out the first non-! non-<bot name> word.
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
protected
|
|
47
|
+
|
|
48
|
+
def filter_message(msg)
|
|
49
|
+
msg[:message] = msg[:message].gsub(Regexp.new("^(!|#{bot.config['nickname']}[,:]\\s+)#{@matcher.downcase}\\s?", Regexp::IGNORECASE), '')
|
|
50
|
+
msg
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class Speaker < EventHandler
|
|
55
|
+
handles :messages
|
|
56
|
+
|
|
57
|
+
def match?(msg)
|
|
58
|
+
msg[:person] == @matcher
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class Message < EventHandler
|
|
63
|
+
handles :messages
|
|
64
|
+
|
|
65
|
+
def match?(msg)
|
|
66
|
+
msg[:message] =~ @matcher
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class Interval < EventHandler
|
|
71
|
+
handles :times
|
|
72
|
+
|
|
73
|
+
def initialize(*args)
|
|
74
|
+
@last_run = ::Time.now
|
|
75
|
+
super(*args)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def match?
|
|
79
|
+
@last_run < ::Time.now - @matcher
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def run(msg, force = false)
|
|
83
|
+
if match?
|
|
84
|
+
@last_run = ::Time.now
|
|
85
|
+
Plugin.registered_plugins[@plugin].send(@method, msg)
|
|
86
|
+
else
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class Time < EventHandler
|
|
93
|
+
handles :times
|
|
94
|
+
|
|
95
|
+
def initialize(*args)
|
|
96
|
+
@run = false
|
|
97
|
+
super(*args)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def match?
|
|
101
|
+
@matcher <= ::Time.now && !@run
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def run(force = false)
|
|
105
|
+
if match?
|
|
106
|
+
@run = true
|
|
107
|
+
Plugin.registered_plugins[@plugin].send(@method)
|
|
108
|
+
else
|
|
109
|
+
false
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/message.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module CampfireBot
|
|
2
|
+
class Message < ActiveSupport::HashWithIndifferentAccess
|
|
3
|
+
def initialize(attributes)
|
|
4
|
+
self.merge!(attributes)
|
|
5
|
+
self[:message] = self['body'] if !!self['body']
|
|
6
|
+
self[:person] = self['user']['name'] if !!self['user']
|
|
7
|
+
self[:room] = attributes[:room]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def reply(str)
|
|
11
|
+
speak(str)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def speak(str)
|
|
15
|
+
self[:room].speak(str)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def paste(str)
|
|
19
|
+
self[:room].paste(str)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def upload(file_path)
|
|
23
|
+
self[:room].upload(file_path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def play(str)
|
|
27
|
+
self[:room].play(str)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|