camper_van 0.0.1

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.
@@ -0,0 +1,390 @@
1
+ # encoding: utf-8
2
+
3
+ require "yaml"
4
+
5
+ module CamperVan
6
+ class Channel
7
+
8
+ # The irc channel name this channel instance is for
9
+ attr_reader :channel
10
+
11
+ # The connected irc client connection
12
+ attr_reader :client
13
+
14
+ # The campfire room to which this channel is connected
15
+ attr_reader :room
16
+
17
+ # Accessor for the EM http request representing the live stream from
18
+ # the campfire api
19
+ attr_reader :stream
20
+
21
+ # Accessor for hash of known users in the room/channel
22
+ # Kept up to date by update_users command, as well as Join/Leave
23
+ # campfire events.
24
+ attr_reader :users
25
+
26
+ include Utils
27
+ include Logger
28
+
29
+ # Public: create a new campfire channel
30
+ #
31
+ # channel - name of the channel we're joining
32
+ # client - the EM::Connection representing the irc client
33
+ # room - the campfire room we're joining
34
+ def initialize(channel, client, room)
35
+ @channel, @client, @room = channel, client, room
36
+ @users = {}
37
+ end
38
+
39
+ # Public: Joins a campfire room and sends the necessary topic
40
+ # and name list messages back to the IRC client.
41
+ #
42
+ # Returns true if the join was successful,
43
+ # false if the room was full or locked.
44
+ def join
45
+ if room.locked?
46
+ numeric_reply :err_inviteonlychan, "Cannot join #{channel} (locked)"
47
+ return false
48
+
49
+ elsif room.full?
50
+ numeric_reply :err_channelisfull, "Cannot join #{channel} (full)"
51
+ return false
52
+
53
+ else
54
+ update_users do
55
+ # join channel
56
+ client.user_reply :join, ":#{channel}"
57
+
58
+ # current topic
59
+ client.numeric_reply :rpl_topic, channel, ':' + room.topic
60
+
61
+ # List the current users, which must always include myself
62
+ # (race condition, server may not realize the user has joined yet)
63
+ nicks = users.values.map { |u| u.nick }
64
+ nicks.unshift client.nick unless nicks.include? client.nick
65
+
66
+ nicks.each_slice(10) do |list|
67
+ client.numeric_reply :rpl_namereply, "=", channel, ":#{list.join ' '}"
68
+ end
69
+ client.numeric_reply :rpl_endofnames, channel, "End of /NAMES list."
70
+
71
+ # begin streaming the channel events (joins room implicitly)
72
+ stream_campfire_to_channel
73
+ end
74
+ end
75
+
76
+ true
77
+ end
78
+
79
+ # Public: "leaves" a campfire room, per the PART irc command.
80
+ # Confirms with the connected client to PART the channel.
81
+ #
82
+ # Does not actually leave the campfire room, just closes out the campfire
83
+ # connections, so the server can idle the connection out. This behavior
84
+ # was chosen so periodic joins/parts wouldn't spam the campfire rooms
85
+ # unnecessarily, and also to reflect how Propane et. al. treat open
86
+ # connections: allowing them to time out rather than leaving explicitly.
87
+ def part
88
+ client.user_reply :part, channel
89
+ stream.close_connection if stream
90
+ # room.leave # let the timeout do it rather than being explicit!
91
+ end
92
+
93
+ # Public: replies to a WHO command with a list of users for a campfire room,
94
+ # including their nicks, names, and status.
95
+ #
96
+ # For WHO response: http://www.mircscripts.org/forums.php?cid=3&id=159227
97
+ # In short, H = here, G = away, append @ for chanops (admins)
98
+ def list_users
99
+ update_users(:include_joins_and_parts) do
100
+ users.values.each do |user|
101
+ status = (user.idle? ? "G" : "H") + (user.admin? ? "@" : "")
102
+ client.numeric_reply :rpl_whoreply, channel, user.account, user.server,
103
+ "camper_van", user.nick, status, ":0 #{user.name}"
104
+ end
105
+ client.numeric_reply :rpl_endofwho, channel, "End of WHO list"
106
+ end
107
+ end
108
+
109
+ # Public: accepts an IRC PRIVMSG and converts it to an appropriate
110
+ # campfire text message for the room.
111
+ #
112
+ # msg - the IRC PRIVMSG message contents
113
+ #
114
+ # TODO: substitute "nick: " with the nick's campfire name instead
115
+ def privmsg(msg)
116
+
117
+ # convert twitter urls to tweets
118
+ if msg =~ %r(^https://twitter.com/(\w+)/status/(\d+)$)
119
+ room.tweet(msg) { } # async, no-op callback
120
+ else
121
+ # convert ACTIONs
122
+ msg.sub! /^\01ACTION (.*)\01$/, '*\1*'
123
+
124
+ if matched = users.values.detect {|u| msg.start_with?(u.nick + ': ')}
125
+ msg = msg.sub(/^#{matched.nick}/, matched.name)
126
+ end
127
+
128
+ room.text(msg) { } # async, no-op callback
129
+ end
130
+
131
+ end
132
+
133
+ # Public: sends the current channel mode to the client
134
+ def current_mode
135
+ n = room.membership_limit
136
+ client.numeric_reply :rpl_channelmodeis, channel, current_mode_string, n
137
+ end
138
+
139
+ # Public: set the mode on the campfire channel, mapping from the provided
140
+ # IRC chanmode to the campfire setting.
141
+ #
142
+ # mode - the IRC mode flag change. Must be one of:
143
+ # "+i" - lock room
144
+ # "-i" - unlock room
145
+ #
146
+ # TODO support these when the firering client does:
147
+ # "+s" - disable guest access
148
+ # "-s" - enable guest access
149
+ #
150
+ # Returns nothing, but lets the client know the results of the call. Sends
151
+ # an error to the client for an invalid mode string.
152
+ def set_mode(mode)
153
+ case mode
154
+ # when "+s"
155
+ # when "-s"
156
+ when "+i"
157
+ room.lock
158
+ room.locked = true
159
+ client.user_reply :mode, channel,
160
+ current_mode_string, room.membership_limit
161
+ when "-i"
162
+ room.unlock
163
+ room.locked = false
164
+ client.user_reply :mode, channel,
165
+ current_mode_string, room.membership_limit
166
+ else
167
+ client.numeric_reply :err_unknownmode,
168
+ "is unknown mode char to me for #{channel}"
169
+ end
170
+ end
171
+
172
+ # Returns the current mode string
173
+ def current_mode_string
174
+ n = room.membership_limit
175
+ s = room.open_to_guests? ? "" : "s"
176
+ i = room.locked? ? "i" : ""
177
+ "+#{i}l#{s}"
178
+ end
179
+
180
+ # Public: returns the current topic of the campfire room
181
+ def current_topic
182
+ client.numeric_reply :rpl_topic, channel, ':' + room.topic
183
+ end
184
+
185
+ # Public: set the topic of the campfire room to the given string
186
+ # and lets the irc client know about the change
187
+ #
188
+ # topic - the new topic
189
+ def set_topic(topic)
190
+ room.update("topic" => topic) do
191
+ room.topic = topic
192
+ client.numeric_reply :rpl_topic, channel, ':' + room.topic
193
+ end
194
+ end
195
+
196
+ # Get the list of users from a room, and update the internal
197
+ # tracking state as well as the connected client. If the user list
198
+ # is out of sync, the irc client may receive the associated
199
+ # JOIN/PART commands.
200
+ #
201
+ # include_joins_and_parts - whether or not to include JOIN/PART commands if
202
+ # the user list has changed since the last update
203
+ # (defaults to false)
204
+ # callback - optional callback after the users have been
205
+ # updated
206
+ #
207
+ # Returns nothing, but keeps the users list updated
208
+ def update_users(include_joins_and_parts=false, &callback)
209
+ room.users do |user_list|
210
+ before = users.dup
211
+ present = {}
212
+
213
+ user_list.each do |user|
214
+ if before[user.id]
215
+ present[user.id] = before.delete user.id
216
+ # if present[user.id].nick != nick
217
+ # # NICK CHANGE
218
+ # present[user.id].nick = nick
219
+ # end
220
+ else
221
+ new_user = present[user.id] = User.new(user)
222
+ if include_joins_and_parts
223
+ client.campfire_reply :join, new_user.nick, channel
224
+ end
225
+ end
226
+ end
227
+
228
+ # Now that the list of users is updated, the remaining users
229
+ # in 'before' have left. Let the irc client know.
230
+ before.each do |id, user|
231
+ if include_joins_and_parts
232
+ client.campfire_reply :part, user.nick, channel
233
+ end
234
+ end
235
+
236
+ @users = present
237
+
238
+ callback.call if callback
239
+ end
240
+ end
241
+
242
+ # Stream messages from campfire and map them to IRC commands for the
243
+ # connected client.
244
+ def stream_campfire_to_channel
245
+ @stream = room.stream do |message|
246
+ map_message_to_irc message
247
+ end
248
+ end
249
+
250
+ # Map a campfire message to one or more IRC commands for the client
251
+ #
252
+ # message - the campfire message to map to IRC
253
+ #
254
+ # Returns nothing, but responds according to the message
255
+ def map_message_to_irc(message)
256
+ user_for_message(message) do |message, user|
257
+
258
+ # needed in most cases
259
+ name = user ? irc_name(user.name) : nil
260
+
261
+ if %w(Text Tweet Sound Paste Upload).include?(
262
+ message.type.sub(/Message$/, '')) && name == client.nick
263
+ logger.debug "skipping message from myself: #{message.type} #{message.body}"
264
+ return
265
+ end
266
+
267
+ # strip Message off the type to simplify readability
268
+ case message.type.sub(/Message$/,'')
269
+
270
+ when "Timestamp", "Advertisement"
271
+ # ignore these
272
+
273
+ when "Lock"
274
+ client.campfire_reply :mode, name, channel, "+i"
275
+
276
+ when "Unlock"
277
+ client.campfire_reply :mode, name, channel, "-i"
278
+
279
+ when "DisallowGuests"
280
+ name = irc_name(user.name)
281
+ client.campfire_reply :mode, name, channel, "+s"
282
+
283
+ when "AllowGuests"
284
+ name = irc_name(user.name)
285
+ client.campfire_reply :mode, name, channel, "-s"
286
+
287
+ when "Idle"
288
+ if u = users[user.id]
289
+ u.idle = true
290
+ end
291
+
292
+ when "Unidle"
293
+ if u = users[user.id]
294
+ u.idle = false
295
+ end
296
+
297
+ when "Enter"
298
+ client.campfire_reply :join, name, channel
299
+ users[user.id] = User.new(user)
300
+
301
+ when "Leave", "Kick" # kick is used for idle timeouts
302
+ client.campfire_reply :part, name, channel, "Leaving..."
303
+ users.delete user.id
304
+
305
+ when "Paste"
306
+ lines = message.body.split("\n")
307
+
308
+ lines[0..2].each do |line|
309
+ client.campfire_reply :privmsg, name, channel, "> " + line
310
+ end
311
+
312
+ if lines.size > 3
313
+ client.campfire_reply :privmsg, name, channel, "> more: " +
314
+ "https://#{client.subdomain}.campfirenow.com/room/#{room.id}/paste/#{message.id}"
315
+ end
316
+
317
+ when "Sound"
318
+ text = case message.body
319
+ when "crickets"
320
+ "hears crickets chirping"
321
+ when "rimshot"
322
+ "plays a rimshot"
323
+ when "trombone"
324
+ "plays a sad trombone"
325
+ when "vuvuzela"
326
+ "======<() ~ ♪ ~♫"
327
+ else
328
+ "played a #{message.body} sound"
329
+ end
330
+
331
+ client.campfire_reply :privmsg, name, channel, "\x01ACTION #{text}\x01"
332
+
333
+ # when "System"
334
+ # # NOTICE from :camper_van to channel?
335
+
336
+ when "Text"
337
+ if message.body =~ /^\*.*\*$/
338
+ client.campfire_reply :privmsg, name, channel, ":\01ACTION " + message.body[1..-2] + "\01"
339
+ else
340
+ matched = users.values.detect {|u| message.body.start_with?(u.name + ': ')}
341
+ if matched
342
+ body = message.body.sub(/^#{matched.name}/, matched.nick)
343
+ else
344
+ body = message.body
345
+ end
346
+ client.campfire_reply :privmsg, name, channel, body
347
+ end
348
+
349
+ when "TopicChange"
350
+ client.campfire_reply :topic, name, channel, message.body
351
+ room.topic = message.body
352
+ # client.numeric_reply :rpl_topic, channel, ':' + message.body
353
+
354
+ when "Upload"
355
+ client.campfire_reply :privmsg, name, channel, ":\01ACTION uploaded " +
356
+ "https://#{client.subdomain}.campfirenow.com/room/#{room.id}/uploads/#{message.id}/#{message.body}"
357
+
358
+ when "Tweet"
359
+ # stringify keys since campfire API is inconsistent about it
360
+ tweet = stringify_keys(YAML.load(message.body))
361
+ client.campfire_reply :privmsg, name, channel,
362
+ "@#{tweet["author_username"]}: #{tweet["message"]}" +
363
+ " (https://twitter.com/#{tweet["author_username"]}" +
364
+ "/status/#{tweet["id"]})"
365
+
366
+ else
367
+ logger.warn "unknown message #{message.type}: #{message.body}"
368
+ end
369
+ end
370
+ end
371
+
372
+ # Retrieve the user from a message, either by finding it in the current
373
+ # list of known users, or by asking campfire for the user.
374
+ #
375
+ # message - the message for which to look up the user
376
+ #
377
+ # Yields the message and the user associated with the message
378
+ def user_for_message(message)
379
+ if user = users[message.user_id]
380
+ yield message, user
381
+ else
382
+ message.user do |user|
383
+ yield message, user
384
+ end
385
+ end
386
+ end
387
+
388
+ end
389
+ end
390
+
@@ -0,0 +1,70 @@
1
+ module CamperVan
2
+ class HandlerMissing < StandardError
3
+ attr_reader :command
4
+ def initialize(command)
5
+ @command = command
6
+ @message = "no handler for the #{command.keys.first} command"
7
+ end
8
+ end
9
+
10
+ module CommandDefinition
11
+
12
+ def self.included(base)
13
+ base.module_eval { include InstanceMethods }
14
+ base.extend ClassMethods
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ # Public: defines a handler for the given irc command
20
+ #
21
+ # command - the irc command to define a handler for
22
+ #
23
+ # Example:
24
+ #
25
+ # handle :nick do |args|
26
+ # # ... change nickname to ...
27
+ # end
28
+ #
29
+ # ```
30
+ # def handle_nick(*args)
31
+ # # contents of block
32
+ # end
33
+ # ```
34
+ def handle(command, &block)
35
+ define_method "handle_#{command}".to_sym, &block
36
+ end
37
+ end
38
+
39
+ module InstanceMethods
40
+
41
+ # Public: handles the given command using the handler method
42
+ # defined by the class-level handler metaprogramming, if it
43
+ # exists.
44
+ #
45
+ # command - the Hash command as provided by the irc command parser
46
+ #
47
+ # Example:
48
+ #
49
+ # handle :nick => ["joe"]
50
+ #
51
+ # Raises CamperVan::HandlerMissing if there is no handler method
52
+ # defined for the given command.
53
+ def handle(command)
54
+ name, args = command.to_a.first
55
+ method_name = "handle_#{name}".to_sym
56
+ if self.respond_to? method_name
57
+ m = method(method_name)
58
+ if m.arity > 0
59
+ send method_name, args
60
+ else
61
+ send method_name
62
+ end
63
+ else
64
+ raise CamperVan::HandlerMissing, command
65
+ end
66
+ end
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,43 @@
1
+ module CamperVan
2
+
3
+ # simplistic IRC command parser
4
+ module CommandParser
5
+
6
+ # returns hash, e.g.
7
+ #
8
+ # malformed # => nil
9
+ # NICK joe # => # { :nick => ["joe"] }
10
+ # LIST # => # { :list => [] }
11
+ # PRIVMSG #foo :test # => { :privmsg => ['#foo', 'test'] }
12
+ #
13
+ def parse(line)
14
+ line = line.dup
15
+ match = /^([A-Z]+)(\b|$)/.match(line)
16
+ cmd = match && match[0]
17
+
18
+ return nil unless cmd
19
+
20
+ # strip off the command and any whitespace
21
+ line.sub! /^#{cmd}\s*/, ""
22
+
23
+ args = []
24
+ until line.empty? do
25
+ line =~ /^(\S+)(\s|$)/
26
+ if $1
27
+ if $1.start_with?(":")
28
+ args << line[1..-1]
29
+ break
30
+ else
31
+ args << $1
32
+ line = line[$1.size..-1]
33
+ line = line.sub(/^\s+/,"")
34
+ end
35
+ else
36
+ break
37
+ end
38
+ end
39
+
40
+ return {cmd.downcase.to_sym => args }
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,62 @@
1
+ # debug proxy for dumping all irc traffic between a client and a server
2
+ module CamperVan
3
+ class DebugProxy < EM::Connection
4
+ include EM::Protocols::LineText2
5
+
6
+ def self.run(server, server_port=6667)
7
+ EM.run do
8
+ EM.start_server "localhost", 6667, DebugProxy, server, server_port
9
+ puts "* waiting for connections..."
10
+
11
+ trap("INT") do
12
+ puts "* shutting down"
13
+ EM.stop
14
+ end
15
+ end
16
+ end
17
+
18
+ class Server < EM::Connection
19
+ include EM::Protocols::LineText2
20
+
21
+ attr_reader :client
22
+
23
+ def initialize(client)
24
+ super
25
+ @lt2_delimiter = "\r\n"
26
+ @client = client
27
+ end
28
+
29
+ def post_init
30
+ puts "* established connection to server"
31
+ end
32
+
33
+ def receive_line(line)
34
+ puts "> #{line}"
35
+ client.send_data line + "\r\n"
36
+ end
37
+
38
+ def unbind
39
+ puts "* server closed connection"
40
+ end
41
+ end
42
+
43
+ def initialize(server, server_port)
44
+ @server = EM.connect(server, server_port, IrcProxy::Server, self)
45
+ @lt2_delimiter = "\r\n"
46
+ end
47
+
48
+ def post_init
49
+ puts "* client connected, establishing connection to server..."
50
+ end
51
+
52
+ def receive_line(line)
53
+ puts "< #{line}"
54
+ @server.send_data line + "\r\n"
55
+ end
56
+
57
+ def unbind
58
+ puts "* client closed connection"
59
+ end
60
+
61
+ end
62
+ end