camper_van 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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