em-campfire 0.0.1 → 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/.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