em-campfire 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ examples/Gemfile
6
+ examples/Gemfile.lock
7
+ coverage/
8
+ .rvmrc
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,24 @@
1
+ ## How to contribute
2
+
3
+ Here's the most direct way to get your work merged into the project:
4
+
5
+ 1. Fork the project
6
+ 2. Clone down your fork
7
+ 3. Create a feature branch
8
+ 4. Add your feature + tests
9
+ 5. Document new features in the README
10
+ 6. Make sure everything still passes by running the tests
11
+ 7. If necessary, rebase your commits into logical chunks, without errors
12
+ 8. Push the branch up
13
+ 9. Send a pull request for your branch
14
+
15
+ # What to contribute?
16
+
17
+ See the github issues or the TODO section in the README for some features to work on
18
+
19
+ # Coverage
20
+
21
+ When you run the tests a coverage report is made. We're not aiming for 100% coverage, it's just a guide.
22
+
23
+ Take a look at the TODO list or known issues for some inspiration if you need it.
24
+
data/Gemfile CHANGED
@@ -1,2 +1,2 @@
1
- source :rubygems
1
+ source "https://rubygems.org"
2
2
  gemspec
data/README.md CHANGED
@@ -1,6 +1,107 @@
1
+ em-campfire is a library for interacting with the [Campfire](http://campfirenow.com/) chat application. It was extracted from the [Scamp](https://github.com/wjessop/Scamp) v1 bot framework.
1
2
 
2
- This isn't ready for production use yet, so don't use it!
3
+ ## Installation
4
+
5
+ `gem install em-campfire` or put `gem 'em-campfire'` in your Gemfile.
3
6
 
4
7
  ## Example
5
8
 
6
- See examples/simple.rb
9
+ ``` ruby
10
+ require 'em-campfire'
11
+
12
+ EM.run {
13
+ connection = EM::Campfire.new(:subdomain => "foo", :api_key => "jhhekrlfjnksdjnliyherkjb", :verbose => true)
14
+
15
+ # Join a room, you will need the room id
16
+ connection.join(10101)
17
+
18
+ # Stream a room, need to join it first
19
+ connection.join(2345) {|id| connection.stream(id) }
20
+
21
+ # Dump out any message we get
22
+ connection.on_message do |msg|
23
+ puts msg.inspect
24
+ end
25
+
26
+ # Give lib a chance to connect
27
+ EM::Timer.new(10) do
28
+ # Say something on a specific channel
29
+ connection.say "foofoofoo", 10101
30
+
31
+ # Paste something
32
+ connection.paste "foo\nfoo\nfoo", 10101
33
+
34
+ # Play a sound
35
+ connection.play "nyan", 10101
36
+ end
37
+ }
38
+ ```
39
+
40
+ For more features see the examples.
41
+
42
+ ### Connection options
43
+
44
+ There are a few optional parameters you can create an EM::Campfire with:
45
+
46
+ ``` ruby
47
+ require 'em-campfire'
48
+
49
+ EM.run {
50
+ connection = EM::Campfire.new(
51
+ :subdomain => "foo",
52
+ :api_key => "jhhekrlfjnksdjnliyherkjb",
53
+ :verbose => true,
54
+ :logger => Logger::Syslog.new('process_name', Syslog::LOG_PID | Syslog::LOG_CONS),
55
+ :cache => custom_cache_object
56
+ )
57
+
58
+ # more code
59
+ }
60
+ ```
61
+
62
+ #### :verbose
63
+
64
+ If set to true sets the log level to DEBUG, defaults to false.
65
+
66
+ #### :logger
67
+
68
+ em-campfire uses a Logger instance from stdlib by default, you can switch this out by passing in your own logger instance.
69
+
70
+ #### :cache
71
+
72
+ em-campfire caches responses from the Campfire API and issues conditional requests using ETags. By default it uses an in-memory cache of data returned, and this is fine for most people, but if you want something custom, possibly more permanent, you can pass in your own cache object.
73
+
74
+ The cache object should conform to the get/set API of the [redis-rb](https://github.com/redis/redis-rb) lib (making that a drop-in replacement). Just make sure that you use the synchrony driver.
75
+
76
+ #### :ignore_self
77
+
78
+ em-campfire receives messages that it posted on it's streaming connections. By default it processes these just as it would any other message. set :ignore_self to true to make it ignore messages it sends.
79
+
80
+ ## Requirements
81
+
82
+ I've tested it in Ruby >= 1.9.3.
83
+
84
+ ## TODO
85
+
86
+ * I mock is\_me? in "should be able to ignore itself" in connection_spec.rb for convenience, work out a way to not do that
87
+ * See if http connection/cacheing/failure handling can be abstracted
88
+ * Allow user to pass success/error callbacks
89
+ * Re-try failed HTTP requests
90
+ * Maybe encapsulate actions in objects, for instance a Room object
91
+
92
+ ### Missing features
93
+
94
+ em-campfire was written primarily to support [Scamp](https://github.com/wjessop/Scamp)/[scamp-campfire ](https://github.com/omgitsads/scamp-campfire) so I've implemented the features required for that first. There are other features left-over that I didn't need and I'll get round to at some point. If you need one before then ping me and I might write it, or a pull request is of course welcome.
95
+
96
+ See the [Campfire API](https://github.com/37signals/campfire-api) for reference:
97
+
98
+ * [Messages (recent, highlight, unhighlight)](https://github.com/37signals/campfire-api/blob/master/sections/messages.md)
99
+ * [Rooms (updating, locking, unlocking, leaving)](https://github.com/37signals/campfire-api/blob/master/sections/rooms.md)
100
+ * [Transcripts](https://github.com/37signals/campfire-api/blob/master/sections/transcripts.md)
101
+ * [Uploads](https://github.com/37signals/campfire-api/blob/master/sections/uploads.md)
102
+ * [Search](https://github.com/37signals/campfire-api/blob/master/sections/search.md)
103
+ * [Account](https://github.com/37signals/campfire-api/blob/master/sections/account.md)
104
+
105
+ ## Contributing
106
+
107
+ See the CONTRIBUTING file
data/em-campfire.gemspec CHANGED
@@ -16,12 +16,13 @@ Gem::Specification.new do |s|
16
16
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
17
  s.require_paths = ["lib"]
18
18
 
19
- s.add_dependency('eventmachine', '~> 1.0.0.beta.4')
20
- s.add_dependency('yajl-ruby', '~> 0.8.3')
21
- s.add_dependency('em-http-request', '~> 1.0.0.beta.4')
19
+ s.add_dependency 'eventmachine', '~> 1.0'
20
+ s.add_dependency 'yajl-ruby', '~> 1.1'
21
+ s.add_dependency 'em-http-request', '~> 1.0'
22
22
 
23
- s.add_development_dependency "rake", "~> 0.9.2"
24
- s.add_development_dependency "rspec", "~> 2.6.0"
25
- s.add_development_dependency "mocha", "~> 0.10.0"
26
- s.add_development_dependency "webmock", "~> 1.7.6"
23
+ s.add_development_dependency "rake"
24
+ s.add_development_dependency "rspec", '~> 2.11'
25
+ s.add_development_dependency "mocha", '~> 0.12'
26
+ s.add_development_dependency "webmock", '~> 1.8.10'
27
+ s.add_development_dependency "simplecov"
27
28
  end
data/examples/simple.rb CHANGED
@@ -3,17 +3,40 @@ $:.unshift File.join(File.dirname(__FILE__), '../lib')
3
3
  require 'em-campfire'
4
4
 
5
5
  EM.run {
6
- connection = EM::Campfire.new(:subdomain => "foo", :api_key => "foo")
7
- connection.join 293788 # Robot Army
8
- connection.join 401839 # Monitoring
9
-
6
+ connection = EM::Campfire.new(:subdomain => "foo", :api_key => "jhhekrlfjnksdjnliyherkjb", :verbose => true)
7
+
8
+ # Join a room, you will need the room id
9
+ connection.join(10101)
10
+
11
+ # Stream a room, need to join it first
12
+ connection.join(2345) {|id| connection.stream(id) }
13
+
14
+ # Dump out any message we get
10
15
  connection.on_message do |msg|
11
16
  puts msg.inspect
12
17
  end
13
-
18
+
19
+ # Pull data for a room
20
+ connection.room_data_from_room_id(2345) {|data| puts data.inspect }
21
+
22
+ # Pull data for all rooms
23
+ connection.room_data_for_all_rooms {|data| puts data.inspect }
24
+
25
+ # Fetch user data for a specific user
26
+ connection.fetch_user_data_for_user_id(123) {|data| puts data.inspect }
27
+
28
+ # Fetch data about the 'sef' user
29
+ connection.fetch_user_data_for_self {|data| puts data.inspect }
30
+
14
31
  # Give lib a chance to connect
15
- EM::Timer.new(10) do
32
+ EM::Timer.new(5) do
16
33
  # Say something on a specific channel
17
- connection.say "foofoofoo", "Robot Army"
34
+ connection.say "foofoofoo", 10101
35
+
36
+ # Paste something
37
+ connection.paste "foo\nfoo\nfoo", 10101
38
+
39
+ # Play a sound
40
+ connection.play "nyan", 10101
18
41
  end
19
42
  }
@@ -0,0 +1,25 @@
1
+ module EventMachine
2
+ class Campfire
3
+ class Cache
4
+ def get(key)
5
+ key = cache_key_for(key)
6
+ yield store[key] if block_given?
7
+ store[key]
8
+ end
9
+
10
+ def set(key, val)
11
+ store[cache_key_for(key)] = val
12
+ end
13
+
14
+ private
15
+
16
+ def cache_key_for(object)
17
+ "em-campfire-#{object}"
18
+ end
19
+
20
+ def store
21
+ @store ||= {}
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,28 +1,29 @@
1
1
  module EventMachine
2
2
  class Campfire
3
3
  module Connection
4
-
5
4
  attr_accessor :ignore_self
6
-
5
+
7
6
  def on_message &blk
8
7
  @on_message_block = blk
9
8
  end
10
-
9
+
11
10
  private
12
-
11
+
13
12
  attr_accessor :on_message_block
14
-
13
+
15
14
  def process_message(msg)
16
15
  logger.debug "Received message #{msg.inspect}"
17
- return false if ignore_self && is_me?(msg[:user_id])
18
- if on_message_block
19
- logger.debug "on_message callback exists, calling it #{msg.inspect}"
20
- on_message_block.call(msg)
16
+ if ignore_self && is_me?(msg[:user_id])
17
+ logger.debug "Ignoring message with user_id #{msg[:user_id]} as that is me and ignore_self is true"
21
18
  else
22
- logger.debug "on_message callback does not exist"
19
+ if on_message_block
20
+ logger.debug "on_message callback exists, calling it with #{msg.inspect}"
21
+ on_message_block.call(msg)
22
+ else
23
+ logger.debug "on_message callback does not exist"
24
+ end
23
25
  end
24
26
  end
25
-
26
27
  end
27
28
  end
28
29
  end
@@ -2,7 +2,7 @@ module EventMachine
2
2
  class Campfire
3
3
  module Messages
4
4
  def say(message, room_id_or_name)
5
- send_message(room_id_or_name, message, "Textmessage")
5
+ send_message(room_id_or_name, message, "TextMessage")
6
6
  end
7
7
 
8
8
  def paste(message, room_id_or_name)
@@ -15,16 +15,15 @@ module EventMachine
15
15
 
16
16
  private
17
17
 
18
- # curl -vvv -H 'Content-Type: application/json' -d '{"message":{"body":"Yeeeeeaaaaaahh", "type":"Textmessage"}}' -u API_KEY:X https://something.campfirenow.com/room/2345678/speak.json
19
- def send_message(room_id_or_name, payload, type)
20
- if room_cache.size < 1
21
- logger.error "Couldn't post message \"#{payload}\" to room #{room_id_or_name} as no rooms have been joined"
22
- return
18
+ # curl -vvv -H 'Content-Type: application/json' -d '{"message":{"body":"Yeeeeeaaaaaahh", "type":"TextMessage"}}' -u API_KEY:X https://something.campfirenow.com/room/2345678/speak.json
19
+ def send_message(room_id, payload, type)
20
+ unless joined_rooms[room_id]
21
+ logger.error "Couldn't post message \"#{payload}\" to room #{room_id} as no rooms have been joined"
22
+ return false
23
23
  end
24
24
 
25
- room_id = room_id(room_id_or_name)
26
25
  url = "https://#{subdomain}.campfirenow.com/room/#{room_id}/speak.json"
27
- http = EventMachine::HttpRequest.new(url).post :head => {'Content-Type' => 'application/json', 'authorization' => [api_key, 'X']}, :body => Yajl::Encoder.encode({:message => {:body => payload, :type => type}})
26
+ http = EventMachine::HttpRequest.new(url).post :head => {'Content-Type' => 'application/json', 'authorization' => [api_key, 'X']}, :body => Yajl::Encoder.encode({:message => {:body => payload.to_s, :type => type}})
28
27
  http.errback { logger.error "Couldn't connect to #{url} to post message \"#{payload}\" to room #{room_id}" }
29
28
  http.callback {
30
29
  if [200,201].include? http.response_header.status
@@ -1,122 +1,118 @@
1
1
  module EventMachine
2
2
  class Campfire
3
3
  module Rooms
4
-
5
- # attr_accessor :rooms
6
- attr_accessor :room_cache
7
4
 
8
- def join(room, &blk)
9
- id = room_id(room)
10
- # logger.info "Joining room #{id}"
11
- if id
12
- url = "https://#{subdomain}.campfirenow.com/room/#{id}/join.json"
13
- http = EventMachine::HttpRequest.new(url).post :head => {'Content-Type' => 'application/json', 'authorization' => [api_key, 'X']}
14
- http.errback { logger.error "Error joining room: #{id}" }
15
- http.callback {
16
- if http.response_header.status == 200
17
- logger.info "Joined room #{id} successfully"
18
- # fetch_room_data(id)
19
- stream(id)
20
- else
21
- logger.error "Error joining room: #{id}"
22
- end
23
- }
24
- end
5
+ attr_accessor :room_cache, :joined_rooms
6
+
7
+ def join(room_id, &blk)
8
+ logger.info "Joining room #{room_id}"
9
+ url = "https://#{subdomain}.campfirenow.com/room/#{room_id}/join.json"
10
+ http = EventMachine::HttpRequest.new(url).post :head => {'Content-Type' => 'application/json', 'authorization' => [api_key, 'X']}
11
+ http.errback { logger.error "Error joining room: #{room_id}" }
12
+ http.callback {
13
+ if http.response_header.status == 200
14
+ logger.info "Joined room #{room_id} successfully"
15
+ joined_rooms[room_id] = true
16
+ yield(room_id) if block_given?
17
+ else
18
+ logger.error "Error joining room: #{room_id}"
19
+ end
20
+ }
25
21
  end
26
22
 
27
- private
28
-
29
- attr_accessor :populating_room_list
30
-
31
23
  def stream(room_id)
32
24
  json_parser = Yajl::Parser.new :symbolize_keys => true
33
25
  json_parser.on_parse_complete = method(:process_message)
34
-
26
+
35
27
  url = "https://streaming.campfirenow.com/room/#{room_id}/live.json"
36
28
  # Timeout per https://github.com/igrigorik/em-http-request/wiki/Redirects-and-Timeouts
37
29
  http = EventMachine::HttpRequest.new(url, :connect_timeout => 20, :inactivity_timeout => 0).get :head => {'authorization' => [api_key, 'X']}
38
- http.errback { logger.error "Couldn't stream room #{room_id} at url #{url}" }
39
- http.callback { logger.info "Disconnected from #{url}"; join(room_id) if rooms[room_id] }
40
- http.stream {|chunk| json_parser << chunk }
41
- end
42
-
43
-
44
- def room_id(room_id_or_name)
45
- if room_id_or_name.is_a? Integer
46
- return room_id_or_name
47
- else
48
- return room_id_from_room_name(room_id_or_name)
49
- end
50
- end
51
-
52
- def room_id_from_room_name(room_name)
53
- logger.debug "Looking for room id for #{room_name}"
54
-
55
- if room_cache.has_key? room_name
56
- return room_cache[room_name]["id"]
57
- else
58
- logger.warn "Attempted to join #{room_name} but could not find an ID for it"
59
- return false
30
+ http.errback {
31
+ logger.error "Couldn't stream room #{room_id} at url #{url}, error was #{http.error}"
32
+ EM.next_tick {stream(room_id)}
33
+ }
34
+ http.callback {
35
+ if http.response_header.status == 200
36
+ logger.info "Disconnected from #{url}"
37
+ else
38
+ logger.error "Couldn't stream room with url #{url}, http response from API was #{http.response_header.status}"
39
+ end
40
+ EM.next_tick {stream(room_id)}
41
+ }
42
+ http.stream do |chunk|
43
+ begin
44
+ json_parser << chunk
45
+ rescue Yajl::ParseError => e
46
+ logger.error "Couldn't parse json data for room 123, data was #{chunk}, error was: #{e}"
47
+ end
60
48
  end
61
49
  end
62
-
50
+
63
51
  # curl -vvv -H 'Content-Type: application/json' -u API_KEY:X https://something.campfirenow.com/rooms.json
64
- def populate_room_list
65
- url = "https://#{subdomain}.campfirenow.com/rooms.json"
66
- http = EventMachine::HttpRequest.new(url).get :head => {'authorization' => [api_key, 'X']}
67
- http.errback { logger.error "Couldn't connect to url #{url} to fetch room list"; puts http.inspect }
52
+ def room_data_from_room_id(room_id, &block)
53
+ url = "https://#{subdomain}.campfirenow.com/room/#{room_id}.json"
54
+
55
+ etag_header = {}
56
+ if cached_room_data = cache.get(room_cache_key(room_id))
57
+ etag_header = {"ETag" => cached_room_data["etag"]}
58
+ end
59
+
60
+ http = EventMachine::HttpRequest.new(url).get :head => {'authorization' => [api_key, 'X'], 'Content-Type'=>'application/json'}.merge(etag_header)
61
+ http.errback { logger.error "Couldn't connect to url #{url} to fetch room data" }
68
62
  http.callback {
69
63
  if http.response_header.status == 200
70
- logger.debug "Fetched room list"
71
- new_rooms = {}
72
- Yajl::Parser.parse(http.response)['rooms'].each do |c|
73
- new_rooms[c["name"]] = c
74
- end
75
- @room_cache = new_rooms # replace existing room list
64
+ room_data = Yajl::Parser.parse(http.response)['room']
65
+ cache.set(room_cache_key(room_id), room_data.merge({'etag' => http.response_header.etag}))
66
+ logger.debug "Fetched room data for #{room_id} (#{room_data['name']})"
67
+ yield room_data if block_given?
68
+ elsif http.response_header.status == 304
69
+ logger.debug "HTTP response was 304, serving room data for room #{room_id} (#{cached_room_data['name']}) from cache"
70
+ yield cached_room_data if block_given?
76
71
  else
77
- logger.error "Couldn't fetch room list with url #{url}, http response from API was #{http.response_header.status}"
72
+ logger.error "Couldn't fetch room data with url #{url}, http response from API was #{http.response_header.status}"
78
73
  end
79
74
  }
80
75
  end
81
-
82
- # def fetch_room_data(room_id)
83
- # url = "https://#{subdomain}.campfirenow.com/room/#{room_id}.json"
84
- # http = EventMachine::HttpRequest.new(url).get :head => {'authorization' => [api_key, 'X']}
85
- # http.errback { logger.error "Couldn't connect to #{url} to fetch room data for room #{room_id}" }
86
- # http.callback {
87
- # if http.response_header.status == 200
88
- # logger.debug "Fetched room data for #{room_id}"
89
- # room = Yajl::Parser.parse(http.response)['room']
90
- # room_cache[room["id"]] = room
91
- #
92
- # room['users'].each do |u|
93
- # update_user_cache_with(u["id"], u)
94
- # end
95
- # else
96
- # logger.error "Couldn't fetch room data for room #{room_id} with url #{url}, http response from API was #{http.response_header.status}"
97
- # end
98
- # }
99
- # end
100
-
101
- def fetch_room_data(room_id)
102
- url = "https://#{subdomain}.campfirenow.com/room/#{room_id}.json"
103
- http = EventMachine::HttpRequest.new(url).get :head => {'authorization' => [api_key, 'X']}
104
- http.errback { logger.error "Couldn't connect to #{url} to fetch room data for room #{room_id}" }
76
+
77
+ def room_data_for_all_rooms
78
+ url = "https://#{subdomain}.campfirenow.com/rooms.json"
79
+
80
+ etag_header = {}
81
+ if cached_room_list_data = cache.get(room_list_data_cache_key)
82
+ etag_header = {"ETag" => cached_room_list_data["etag"]}
83
+ end
84
+
85
+ http = EventMachine::HttpRequest.new(url).get :head => {'Content-Type' => 'application/json', 'authorization' => [api_key, 'X']}.merge(etag_header)
86
+
87
+ http.errback { logger.error "Error processing url #{url} to fetch room data: #{http.error}" }
105
88
  http.callback {
106
89
  if http.response_header.status == 200
107
- logger.debug "Fetched room data for #{room_id}"
108
- room = Yajl::Parser.parse(http.response)['room']
109
- room_cache[room["id"]] = room
110
-
111
- room['users'].each do |u|
112
- update_user_cache_with(u["id"], u)
113
- end
90
+ room_data = Yajl::Parser.parse(http.response)['rooms']
91
+ cache.set(room_list_data_cache_key, {'data' => room_data, 'etag' => http.response_header.etag})
92
+ logger.debug "Fetched room data for all rooms"
93
+ yield room_data if block_given?
94
+ elsif http.response_header.status == 304
95
+ logger.debug "HTTP response was 304, serving room list data from cache"
96
+ yield cached_room_list_data if block_given?
114
97
  else
115
- logger.error "Couldn't fetch room data for room #{room_id} with url #{url}, http response from API was #{http.response_header.status}"
98
+ logger.error "Couldn't fetch room list with url #{url}, http response from API was #{http.response_header.status}"
116
99
  end
117
100
  }
118
101
  end
119
-
102
+
103
+ private
104
+
105
+ def joined_rooms
106
+ @rooms ||= {}
107
+ end
108
+
109
+ def room_cache_key(room_id)
110
+ "room-data-#{room_id}"
111
+ end
112
+
113
+ def room_list_data_cache_key
114
+ "room-list-data"
115
+ end
120
116
  end # Rooms
121
117
  end # Campfire
122
118
  end # EventMachine