scamp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .rvmrc
2
+ pkg/
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # Scamp
2
+
3
+ A framework for writing [Campfire](http://campfirenow.com/) bots. Scamp is in early development so use it at your own risk, pull requests welcome.
4
+
5
+ ## Requirements
6
+
7
+ Ruby >= 1.9.2 (At least for the named captures)
8
+
9
+ ## Installation
10
+
11
+ `gem install scamp` or put `gem 'scamp'` in your Gemfile.
12
+
13
+ ## Usage and Examples
14
+
15
+ Matchers are tested in order and all that satisfy the match and conditions will be run. Careful, Scamp listens to itself, you could easily create an infinite loop. Look in the examples dir for more.
16
+
17
+ require 'Scamp'
18
+
19
+ scamp = Scamp.new(:api_key => "YOUR API KEY")
20
+
21
+ Scamp.behaviour do
22
+ #
23
+ # Simple matching based on regex or string:
24
+ #
25
+ match /^repeat (\w+), (\w+)$/ do
26
+ say "You said #{matches[0]} and #{matches[1]}"
27
+ end
28
+
29
+ #
30
+ # A special user and channel method is available in match blocks.
31
+ #
32
+ match "a user said" do
33
+ say "#{user} said something in channel #{channel}"
34
+ end
35
+
36
+ match "Hello!" do
37
+ say "Hi there"
38
+ end
39
+
40
+ #
41
+ # Limit the match to certain channels, users or both.
42
+ #
43
+ match /^Lets match (.+)$/, :conditions => {:channel => /someregex/} do
44
+ say "Only said if channel name mathces /someregex/"
45
+ end
46
+
47
+ match "some text", :conditions => {:user => /someregex/} do
48
+ say "Only said if user name mathces /someregex/"
49
+ end
50
+
51
+ match /some other text/, :conditions => {:user => /someregex/, :channel => /some other regex/} do
52
+ say "You can mix conditions"
53
+ end
54
+
55
+ #
56
+ # Named caputres become avaiable in your match block
57
+ #
58
+ match /^say (?<yousaid>.+)$/ do
59
+ say "You said #{yousaid}"
60
+ end
61
+
62
+ #
63
+ # You can say multiple times, and you can specify an alternate channel.
64
+ # Default behaviour is to 'say' in the channel that caused the match.
65
+ #
66
+ match "something" do
67
+ say "#{user} said something in channel #{channel}"
68
+ say "#{user} said something in channel #{channel}", 237872
69
+ say "#{user} said something in channel #{channel}", "System Administration"
70
+ end
71
+
72
+ # Connect and join some channels
73
+ scamp.connect!([293788, "Monitoring"])
74
+
75
+ In the channel/user conditions you can use the name, regex or ID of a user or channel, in say you can ise a string or ID, eg:
76
+
77
+ :conditions => {:channel => /someregex/}
78
+ :conditions => {:channel => "some string"}
79
+ :conditions => {:channel => 123456}
80
+
81
+ :conditions => {:user => /someregex/}
82
+ :conditions => {:user => "some string"}
83
+ :conditions => {:user => 123456}
84
+
85
+ say "#{user} said something in channel #{channel}", 237872
86
+ say "#{user} said something in channel #{channel}", "System Administration"
87
+
88
+ ## TODO
89
+
90
+ * Write the tests
91
+ * Allow multiple values for conditions, eg: :conditions => {:channel => [/someregex/, "Some channel"]}
92
+ * Remove debugging output
93
+ * Add support for a logger
94
+
95
+ ## Known issues
96
+
97
+ * Bot doesn't detect that it's been kicked out of a channel and recommect
98
+ * Bot tends to crash when it encounters an error.
99
+
100
+ ## How to contribute
101
+
102
+ Here's the most direct way to get your work merged into the project:
103
+
104
+ 1. Fork the project
105
+ 2. Clone down your fork
106
+ 3. Create a feature branch
107
+ 4. Add your feature + tests
108
+ 5. Make sure everything still passes by running the tests
109
+ 6. If necessary, rebase your commits into logical chunks, without errors
110
+ 7. Push the branch up
111
+ 8. Send a pull request for your branch
112
+
113
+ Take a look at the TODO list or known issues for some inspiration if you need it.
114
+
115
+ ## License
116
+
117
+ Copyright (C) 2011 by Will Jessop
118
+
119
+ Permission is hereby granted, free of charge, to any person obtaining a copy
120
+ of this software and associated documentation files (the "Software"), to deal
121
+ in the Software without restriction, including without limitation the rights
122
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
123
+ copies of the Software, and to permit persons to whom the Software is
124
+ furnished to do so, subject to the following conditions:
125
+
126
+ The above copyright notice and this permission notice shall be included in
127
+ all copies or substantial portions of the Software.
128
+
129
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
130
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
131
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
132
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
133
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
134
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
135
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require 'bundler/gem_tasks'
data/examples/bot.rb ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), '../lib')
4
+
5
+ require 'Scamp'
6
+
7
+ scamp = Scamp.new(:api_key => "YOUR API KEY")
8
+
9
+ Scamp.behaviour do
10
+ # Match some regex limited to a channel condition based on a channel id
11
+ match /^channel id (.+)$/, :conditions => {:channel => 401839} do
12
+ # Reply in the current channel
13
+ say "Match some regex limited to a channel condition based on a channel id"
14
+ end
15
+
16
+ # Limit a match to a channel condition based on a string
17
+ match "channel name check", :conditions => {:channel => "Monitoring"} do
18
+ say "Limit a match to a channel condition based on a string"
19
+ end
20
+
21
+ # Limit a match to a channel condition based on a regex
22
+ match /^channel regex (.+)$/, :conditions => {:channel => /someregex/} do
23
+ say "Limit a match to a channel condition based on a regex"
24
+ end
25
+
26
+ # Limit a match to a user condition based on a regex
27
+ match /^user regex (.+)$/, :conditions => {:user => /someregex/} do
28
+ say "Limit a match to a user condition based on a regex"
29
+ end
30
+
31
+ # Limit a match to a user condition based on a string
32
+ match /^user name (.+)$/, :conditions => {:user => "Will Jessop"} do
33
+ say "Limit a match to a user condition based on a string"
34
+ end
35
+
36
+ # Limit a match to a user condition based on a string
37
+ match "user id check", :conditions => {:user => 774016} do
38
+ say "Limit a match to a user condition based on an ID"
39
+ end
40
+
41
+ # Limit a match to a channel & user condition combined
42
+ match /^something (.+)$/, :conditions => {:channel => "Monitoring", :user => "Will Jessop"} do
43
+ # Reply in the current channel
44
+ say "Limit a match to a channel & user condition combined"
45
+ end
46
+
47
+ # Match text with a regex, access the captures from the match object
48
+ match /^repeat (\w+), (\w+)$/ do
49
+ say "You said #{matches[0]} and #{matches[1]}"
50
+ end
51
+
52
+ # Match text with a regex, access the named captures as a method
53
+ match /^say (?<yousaid>.+)$/ do
54
+ say "You said #{yousaid}"
55
+ end
56
+
57
+ # Simple string match, interpolating the channel and user in response.
58
+ match "something" do |data|
59
+ # Send the response to a different channel
60
+ say "#{user} said something in channel #{channel}", "Robot Army"
61
+
62
+ # Send the response to a different channel, using the channel ID
63
+ say "#{user} said something in channel #{channel}", 293788
64
+
65
+ # Send the response to the originating channel
66
+ say "#{user} said something in channel #{channel}"
67
+ end
68
+
69
+ match "multi-condition match", :conditions => {:channel => [401839, "Monitoring"], :nick => ["Will Jessop", "Noah Lorang"]} do
70
+ # Reply in the current channel
71
+ say "multi-condition match"
72
+ end
73
+ end
74
+
75
+ # FIXME: this does if the channel doesn't exist. Need a better error.
76
+ scamp.connect!([293788, "Monitoring"])
@@ -0,0 +1,57 @@
1
+ #
2
+ # Actions are run in the context of a Scamp::Action.
3
+ # This allows us to make channel, nick etc. methods
4
+ # available on a per-message basis
5
+ #
6
+
7
+ # {:room_id=>401839, :created_at=>"2011/09/10 00:23:19 +0000", :body=>"something", :id=>408089344, :user_id=>774016, :type=>"TextMessage"}
8
+
9
+ class Scamp
10
+ class Action
11
+
12
+ attr :matches, :bot
13
+
14
+ def initialize(bot, action, message)
15
+ @bot = bot
16
+ @action = action
17
+ @message = message
18
+ end
19
+
20
+ def matches=(match)
21
+ @matches = match[1..-1]
22
+ match.names.each do |name|
23
+ name_s = name.to_sym
24
+ self.class.send :define_method, name_s do
25
+ match[name_s]
26
+ end
27
+ end
28
+ end
29
+
30
+ def channel
31
+ puts "Need the real channel name at #{__FILE__}:#{__LINE__}"
32
+ @message[:room_id]
33
+ end
34
+
35
+ def user
36
+ bot.username_for(@message[:user_id])
37
+ end
38
+
39
+ def user_id
40
+ @message[:user_id]
41
+ end
42
+
43
+ def message
44
+ @message[:body]
45
+ end
46
+
47
+ def run
48
+ self.instance_eval &@action
49
+ end
50
+
51
+ private
52
+
53
+ def say(msg, channel_id_or_name = channel)
54
+ bot.say(msg, channel_id_or_name)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,101 @@
1
+ class Scamp
2
+ module Channels
3
+ # TextMessage (regular chat message),
4
+ # PasteMessage (pre-formatted message, rendered in a fixed-width font),
5
+ # SoundMessage (plays a sound as determined by the message, which can be either “rimshot”, “crickets”, or “trombone”),
6
+ # TweetMessage (a Twitter status URL to be fetched and inserted into the chat)
7
+
8
+ # curl -vvv -H 'Content-Type: application/json' -d '{"message":{"body":"Yeeeeeaaaaaahh", "type":"Textmessage"}}' -u API_KEY:X https://37s.campfirenow.com/room/293788/speak.json
9
+ def say(message, channel)
10
+ url = "https://37s.campfirenow.com/room/#{channel_id(channel)}/speak.json"
11
+ http = EventMachine::HttpRequest.new(url).post :head => {'Content-Type' => 'application/json', 'authorization' => [api_key, 'X']}, :body => Yajl::Encoder.encode({:message => {:body => message, :type => "Textmessage"}})
12
+
13
+ http.errback { STDERR.puts "Error speaking: '#{message}' to #{channel_id(channel)}" }
14
+ end
15
+
16
+ def paste(text, channel)
17
+ end
18
+
19
+ def upload
20
+ end
21
+
22
+ def join(channel_id)
23
+ url = "https://37s.campfirenow.com/room/#{channel_id}/join.json"
24
+ http = EventMachine::HttpRequest.new(url).post :head => {'Content-Type' => 'application/json', 'authorization' => [api_key, 'X']}
25
+
26
+ http.errback { STDERR.puts "Error joining channel: #{channel_id}" }
27
+ http.callback {
28
+ yield if block_given?
29
+ }
30
+ end
31
+
32
+ def channel_id(channel_id_or_name)
33
+ if channel_id_or_name.is_a? Integer
34
+ return channel_id_or_name
35
+ else
36
+ return channel_id_from_channel_name(channel_id_or_name)
37
+ end
38
+ end
39
+
40
+ def channel_name_for(channel_id)
41
+ data = channel_cache_data(channel_id)
42
+ return data["name"] if data
43
+ channel_id.to_s
44
+ end
45
+
46
+ private
47
+
48
+ def channel_cache_data(channel_id)
49
+ return channel_cache[channel_id] if channel_cache.has_key? channel_id
50
+ fetch_channel_data(channel_id)
51
+ return false
52
+ end
53
+
54
+ def populate_channel_list
55
+ url = "https://37s.campfirenow.com/rooms.json"
56
+ http = EventMachine::HttpRequest.new(url).get :head => {'authorization' => [api_key, 'X']}
57
+ http.errback { puts http.status }
58
+ http.callback {
59
+ new_channels = {}
60
+ Yajl::Parser.parse(http.response)['rooms'].each do |c|
61
+ new_channels[c["name"]] = c
62
+ end
63
+ # No idea why using the "channels" accessor here doesn't
64
+ # work but accessing the ivar directly does. There's
65
+ # Probably a bug.
66
+ @channels = new_channels # replace existing channel list
67
+ yield if block_given?
68
+ }
69
+ end
70
+
71
+ def fetch_channel_data(channel_id)
72
+ STDERR.puts "Fetching channel data for #{channel_id}"
73
+ url = "https://37s.campfirenow.com/room/#{channel_id}.json"
74
+ http = EventMachine::HttpRequest.new(url).get :head => {'authorization' => [api_key, 'X']}
75
+ http.errback { STDERR.puts "Couldn't get data for channel #{channel_id} at url #{url}" }
76
+ http.callback {
77
+ puts "Fetched channel data for #{channel_id}"
78
+ room = Yajl::Parser.parse(http.response)['room']
79
+ channel_cache[room["id"]] = room
80
+ room['users'].each do |u|
81
+ update_user_cache_with(u["id"], u)
82
+ end
83
+ }
84
+ end
85
+
86
+ def stream(channel_id)
87
+ json_parser = Yajl::Parser.new :symbolize_keys => true
88
+ json_parser.on_parse_complete = method(:process_message)
89
+
90
+ url = "https://streaming.campfirenow.com/room/#{channel_id}/live.json"
91
+ http = EventMachine::HttpRequest.new(url).get :head => {'authorization' => [api_key, 'X']}
92
+ http.errback { STDERR.puts "Couldn't stream channel #{channel_id} at url #{url}" }
93
+ http.stream {|chunk| json_parser << chunk }
94
+ end
95
+
96
+ def channel_id_from_channel_name(channel_name)
97
+ puts "Looking for channel id for #{channel_name}"
98
+ channels[channel_name]["id"]
99
+ end
100
+ end
101
+ end #class
@@ -0,0 +1,21 @@
1
+ class Scamp
2
+ module Connection
3
+ private
4
+
5
+ def connect(api_key, channels_to_join)
6
+ EventMachine.run do
7
+ # Ideally populate_channel_list would block, but I can't see an easy way to do this, so a hacky callback it is.
8
+ populate_channel_list do
9
+ channels_to_join.map{|c| channel_id(c) }.each do |id|
10
+ puts "Joining channel #{id}"
11
+ join(id) do
12
+ fetch_channel_data(id)
13
+ stream(id)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ end #module
21
+ end #class
@@ -0,0 +1,79 @@
1
+ class Scamp
2
+ class Matcher
3
+ attr_accessor :conditions, :trigger, :action, :bot
4
+
5
+ def initialize(bot, params = {})
6
+ params ||= {}
7
+ params[:conditions] ||= {}
8
+ params.each { |k,v| send("#{k}=", v) }
9
+ @bot = bot
10
+ end
11
+
12
+ def attempt(msg)
13
+ return false unless conditions_satisfied_by(msg)
14
+ match = triggered_by(msg[:body])
15
+ if match
16
+ if match.is_a? MatchData
17
+ run(msg, match)
18
+ else
19
+ run(msg)
20
+ end
21
+ return true
22
+ end
23
+ false
24
+ end
25
+
26
+ private
27
+
28
+ def triggered_by(message_text)
29
+ if trigger.is_a? String
30
+ return true if trigger == message_text
31
+ elsif trigger.is_a? Regexp
32
+ return trigger.match message_text
33
+ else
34
+ STDERR.puts "Don't know what to do with #{trigger.inspect} at #{__FILE__}:#{__LINE__}"
35
+ end
36
+ false
37
+ end
38
+
39
+ def run(msg, match = nil)
40
+ action_run = Action.new(bot, action, msg)
41
+ action_run.matches = match if match
42
+ action_run.run
43
+ end
44
+
45
+ def conditions_satisfied_by(msg)
46
+ # STDERR.puts "Need to take into account nick, channel and regexps at #{__FILE__}:#{__LINE__}"
47
+
48
+ # nick
49
+ # channel name
50
+ # nick regex
51
+ # channel regex
52
+
53
+ #{"room_id":1,"created_at":"2009-12-01 23:44:40","body":"hello","id":1,"user_id":1,"type":"TextMessage"}
54
+
55
+ # item will be :nick or :channel
56
+ # cond is the regex, int or string value.
57
+ conditions.each do |item, cond|
58
+ STDERR.puts "Checking #{item} against #{cond}"
59
+ puts "msg is #{msg.inspect}"
60
+ if cond.is_a? Integer
61
+ # puts "item is #{msg[{:channel => :room_id, :user => :user_id}[item]]}"
62
+ return false unless msg[{:channel => :room_id, :user => :user_id}[item]] == cond
63
+ elsif cond.is_a? String
64
+ case item
65
+ when :channel
66
+ return false unless bot.channel_name_for(msg[:room_id]) == cond
67
+ when :user
68
+ return false unless bot.username_for(msg[:user_id]) == cond
69
+ end
70
+ STDERR.puts "Don't know how to deal with a match item of #{item}, cond #{cond}"
71
+ elsif cond.is_a? Regexp
72
+ return false
73
+ return false unless msg[item].match(cond)
74
+ end
75
+ end
76
+ true
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,45 @@
1
+ class Scamp
2
+ module Users
3
+
4
+ # <user>
5
+ # <id type="integer">1</id>
6
+ # <name>Jason Fried</name>
7
+ # <email-address>jason@37signals.com</email-address>
8
+ # <admin type="boolean">true</admin>
9
+ # <created-at type="datetime">2009-11-20T16:41:39Z</created-at>
10
+ # <type>Member</type>
11
+ # <avatar-url>http://asset0.37img.com/global/.../avatar.png</avatar-url>
12
+ # </user>
13
+
14
+ # Return the user_id if we haven't got the real name and
15
+ # kick off a user data fetch
16
+ def username_for(user_id)
17
+ return user_cache[user_id]["name"] if user_cache[user_id]
18
+ fetch_data_for(user_id)
19
+ return user_id.to_s
20
+ end
21
+
22
+ private
23
+
24
+ def fetch_data_for(user_id)
25
+ url = "https://37s.campfirenow.com/users/#{user_id}.json"
26
+ http = EventMachine::HttpRequest.new(url).get(:head => {'authorization' => [api_key, 'X'], "Content-Type" => "application/json"})
27
+ puts http.inspect
28
+ http.callback do
29
+ STDERR.puts "Got the data for #{user_id}"
30
+ update_user_cache_with(user_id, Yajl::Parser.parse(http.response)['user'])
31
+ end
32
+ http.errback do
33
+ STDERR.puts "Couldn't fetch user data for #{user_id} with url #{url}"
34
+ STDERR.puts http.response_header.status
35
+ STDERR.puts http.response_header.inspect
36
+ STDERR.puts http.response.inspect
37
+ end
38
+ end
39
+
40
+ def update_user_cache_with(user_id, data)
41
+ STDERR.puts "Updated user cache for #{data['name']}"
42
+ user_cache[user_id] = data
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module Scamp
2
+ VERSION = "0.0.1"
3
+ end
data/lib/scamp.rb ADDED
@@ -0,0 +1,51 @@
1
+ require 'eventmachine'
2
+ require 'em-http-request'
3
+ require 'yajl'
4
+
5
+ require "scamp/version"
6
+ require 'scamp/connection'
7
+ require 'scamp/channels'
8
+ require 'scamp/users'
9
+ require 'scamp/matcher'
10
+ require 'scamp/action'
11
+
12
+ class Scamp
13
+ include Connection
14
+ include Channels
15
+ include Users
16
+
17
+ attr_accessor :channels, :user_cache, :channel_cache
18
+ attr :matchers, :api_key
19
+
20
+ def initialize(options = {})
21
+ options ||= {}
22
+ raise ArgumentError, "You must pass an API key" unless options[:api_key]
23
+
24
+ @api_key = options[:api_key]
25
+ @channels = {}
26
+ @user_cache = {}
27
+ @channel_cache = {}
28
+ @matchers ||= []
29
+ end
30
+
31
+ def behaviour &block
32
+ instance_eval &block
33
+ end
34
+
35
+ def connect!(channel_list)
36
+ connect(api_key, channel_list)
37
+ end
38
+
39
+ private
40
+
41
+ def match trigger, params={}, &block
42
+ params ||= {}
43
+ matchers << Matcher.new(self, {:trigger => trigger, :action => block, :conditions => params[:conditions]})
44
+ end
45
+
46
+ def process_message(msg)
47
+ matchers.each do |matcher|
48
+ matcher.attempt(msg)
49
+ end
50
+ end
51
+ end
data/scamp.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "scamp/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "scamp"
7
+ s.version = Scamp::VERSION
8
+ s.authors = ["Will Jessop"]
9
+ s.email = ["will@willj.net"]
10
+ s.homepage = ""
11
+ s.summary = %q{Eventmachine based Campfire bot framework}
12
+ s.description = %q{Eventmachine based Campfire bot framework}
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('eventmachine', '~> 0.12.10')
20
+ s.add_dependency('yajl-ruby', '~> 0.8.3')
21
+ s.add_dependency('em-http-request', '~> 0.3.0')
22
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scamp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Will Jessop
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-09-12 00:00:00.000000000 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: eventmachine
17
+ requirement: &2165823060 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 0.12.10
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *2165823060
26
+ - !ruby/object:Gem::Dependency
27
+ name: yajl-ruby
28
+ requirement: &2165822560 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 0.8.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *2165822560
37
+ - !ruby/object:Gem::Dependency
38
+ name: em-http-request
39
+ requirement: &2165822100 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: 0.3.0
45
+ type: :runtime
46
+ prerelease: false
47
+ version_requirements: *2165822100
48
+ description: Eventmachine based Campfire bot framework
49
+ email:
50
+ - will@willj.net
51
+ executables: []
52
+ extensions: []
53
+ extra_rdoc_files: []
54
+ files:
55
+ - .gitignore
56
+ - Gemfile
57
+ - README.md
58
+ - Rakefile
59
+ - examples/bot.rb
60
+ - lib/scamp.rb
61
+ - lib/scamp/action.rb
62
+ - lib/scamp/channels.rb
63
+ - lib/scamp/connection.rb
64
+ - lib/scamp/matcher.rb
65
+ - lib/scamp/users.rb
66
+ - lib/scamp/version.rb
67
+ - scamp.gemspec
68
+ has_rdoc: true
69
+ homepage: ''
70
+ licenses: []
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ requirements: []
88
+ rubyforge_project:
89
+ rubygems_version: 1.6.2
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Eventmachine based Campfire bot framework
93
+ test_files: []