Class: Butler::IRC::Socket

Inherits:
Object
  • Object
show all
Includes:
Log::Comfort
Defined in:
lib/butler/irc/socket.rb

Overview

Description

Butler::IRC::Socket is a TCPSocket, retrofitted for communication with IRC-Servers. It provides specialized methods for sending messages to IRC-Server. All methods are safe to be used with Butler::IRC::* Objects (e.g. all parameters expecting a nickname will accept an Butler::IRC::User as well). It will adhere to its limit-settings, which will prevent from sending too many messages in a too short time to avoid excess flooding. Butler::IRC::Socket#write is the only synchronized method, since all other methods build up on it, IRC::Socket should be safe in threaded environments. Butler::IRC::Socket#read is NOT synchronized, so unless you read from only a single thread, statistics might get messed up. Length limits can only be safely guaranteed by specialized write methods, Butler::IRC::Socket#write will just warn and send the overlength message. If you are looking for queries (commands that get an answer from the server) take a look at Butler::IRC::Client.

Synopsis

irc = Butler::IRC::Socket.new(‘irc.freenode.org’, :port => 6667, :charset => ‘ISO-8859-1’) irc.connect irc.login(‘your_nickname’, ‘YourUser’, ‘Your realname’, [“#channel1”, “#channel2”]) irc.join(“#channel3”) irc.part(“#channel3”) irc.privmsg(“Hi all of you in #channel1!”, “#channel1”) irc.close

Notes

Errno::EHOSTUNREACH: server not reached Errno::ECONNREFUSED: server is up, but refuses connection Errno::ECONNRESET: connection works, server did not yet accept connection, resets after Errno::EPIPE: writing to a server-side closed connection, nil on gets, connection was terminated

FIXME

mode commands don’t test for length and split up

Constant Summary collapse

VERSION =
"1.0.0"
OptionsDefault =
{
	:port => 6667,
	:eol  => "\r\n",
	:host => nil,
}

Instance Attribute Summary collapse

Attributes included from Log::Comfort

#logger

Instance Method Summary collapse

Methods included from Log::Comfort

#debug, #error, #exception, #fail, #info, #log, #warn

Constructor Details

#initialize(server, options = {}) ⇒ Socket

Initialize properties, doesn’t connect automatically options:

  • :server: ip/domain of server (overrides a given server parameter)

  • :port: port to connect on, defaults to 6667

  • :eol: what character sequence terminates messages, defaults to rn

  • :host: what host address to bind to, defaults to nil

Raises:

  • (ArgumentError)


89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/butler/irc/socket.rb', line 89

def initialize(server, options={})
	options       = OptionsDefault.merge(options)
	@logger       = options.delete(:log)
	@server       = server # options.delete(:server)
	@port         = options.delete(:port)
	@eol          = options.delete(:eol).dup.freeze
	@host         = options[:host] ? options.delete(:host).dup.freeze : options.delete(:host)
	@last_sent    = Time.new()
	@count        = Hash.new(0)
	@limit        = OpenStruct.new({
		:message_length => 300, # max. length of a text message (e.g. in notice, privmsg) sent to server
		:raw_length     => 400, # max. length of a raw message sent to server
		:burst          => 4,   # max. messages that can be sent with send_delay (0 = infinite)
		:burst2         => 20,  # max. messages that can be sent with send_delay (0 = infinite)
		:send_delay     => 0.1, # minimum delay between each message
		:burst_delay    => 1.5, # delay after a burst
		:burst2_delay   => 15,  # delay after a burst2
	})
	@limit.each { |key, default|
		@limit[key] = options.delete(key) if options.has_key?(key)
	}
	@mutex        = Mutex.new
	@socket       = Diagnostics.new(self, :write => [NoMethodError, "Must connect first to write to the socket"])
	@connected    = false
	raise ArgumentError, "Unknown arguments: #{options.keys.inspect}" unless options.empty?
end

Instance Attribute Details

#countObject (readonly)

contains various counters, such as :received, :sent (lines)



71
72
73
# File 'lib/butler/irc/socket.rb', line 71

def count
  @count
end

#eolObject (readonly)

end-of-line used for communication



68
69
70
# File 'lib/butler/irc/socket.rb', line 68

def eol
  @eol
end

#hostObject (readonly)

the own host (nil if not supported)



66
67
68
# File 'lib/butler/irc/socket.rb', line 66

def host
  @host
end

#limitObject (readonly)

contains limits for the protocol, burst times/counts etc.



74
75
76
# File 'lib/butler/irc/socket.rb', line 74

def limit
  @limit
end

#portObject (readonly)

port used for connection



64
65
66
# File 'lib/butler/irc/socket.rb', line 64

def port
  @port
end

#serverObject (readonly)

server the instance is linked with



62
63
64
# File 'lib/butler/irc/socket.rb', line 62

def server
  @server
end

Instance Method Details

#action(message, *recipients) ⇒ Object

same as privmsg except it’s formatted for ACTION



234
235
236
237
238
239
240
# File 'lib/butler/irc/socket.rb', line 234

def action(message, *recipients)
	normalize_message(message).each { |message|
		recipients.each { |recipient|
			write("PRIVMSG #{recipient} :"+(1.chr)+"ACTION "+message+(1.chr))
		}
	}
end

#away(reason = "") ⇒ Object

set your status to away with reason ‘reason’



299
300
301
302
# File 'lib/butler/irc/socket.rb', line 299

def away(reason="")
	return back if reason.empty?
	write("AWAY :#{reason}")
end

#backObject

reset your away status to back



305
306
307
# File 'lib/butler/irc/socket.rb', line 305

def back
	write("AWAY")
end

#ban(mask, channel) ⇒ Object

Set ban in channel to mask



344
345
346
# File 'lib/butler/irc/socket.rb', line 344

def ban(mask, channel)
	write("MODE #{channel} +b #{mask}")
end

#closeObject

closes the connection to the irc-server



366
367
368
369
# File 'lib/butler/irc/socket.rb', line 366

def close
	raise "Socket not open" unless @socket
	@socket.close unless @socket.closed?
end

#connectObject

connects to the server



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/butler/irc/socket.rb', line 117

def connect
	@socket	= TCPSocket.open(@server, @port) #, @host)
	info("Connected to #{@server}:#{@port} from #{@host || '<default>'}")
rescue ArgumentError => error
	if @host then
		warn("host-parameter is not supported by your ruby version. Parameter discarted.")
		@host = nil
		retry
	else
		raise
	end
rescue Exception
	error("Connection failed.")
	raise
else
	@connected		= true
end

#deop(channel, *users) ⇒ Object

Take Op from user in channel User can be a nick or IRC::User, either one or an array.



327
328
329
# File 'lib/butler/irc/socket.rb', line 327

def deop(channel, *users)
	write("MODE #{channel} -#{'o'*users.length} #{users*' '}")
end

#devoice(channel, *users) ⇒ Object

Take voice from user in channel. User can be a nick or IRC::User, either one or an array.



339
340
341
# File 'lib/butler/irc/socket.rb', line 339

def devoice(channel, *users)
	write("MODE #{channel} -#{'v'*users.length} #{users*' '}")
end

#ghost(nickname, password) ⇒ Object

FIXME, figure out what the server supports, possibly requires it to be moved to Butler::IRC::Client (to allow ghosting, nickchange, identify)



210
211
212
# File 'lib/butler/irc/socket.rb', line 210

def ghost(nickname, password)
	write("NS :GHOST #{nickname} #{password}")
end

#identify(password) ⇒ Object

identify nickname to nickserv FIXME, figure out what the server supports, possibly requires it to be moved to Butler::IRC::Client (to allow ghosting, nickchange, identify)



204
205
206
# File 'lib/butler/irc/socket.rb', line 204

def identify(password)
	write("NS :IDENTIFY #{password}")
end

#inspectObject

:nodoc:



371
372
373
374
375
376
377
378
379
380
381
# File 'lib/butler/irc/socket.rb', line 371

def inspect # :nodoc:
	"#<%s:0x%08x %s:%s from %s using '%s', stats: %s>" %  [
		self.class,
		object_id << 1,
		@server,
		@port,
		@host || "<default>",
		@eol.inspect[1..-2],
		@count.inspect
	]
end

#join(*channels) ⇒ Object

join specified channels use an array [channel, password] to join password-protected channels returns the channels joined.



266
267
268
269
270
271
272
273
274
275
# File 'lib/butler/irc/socket.rb', line 266

def join(*channels)
	channels.map { |channel, password|
		if password then
			write("JOIN #{channel} #{password}")
		else
			write("JOIN #{channel}")
		end
		channel
	}
end

#kick(user, channel, reason) ⇒ Object

kick user in channel with reason



310
311
312
# File 'lib/butler/irc/socket.rb', line 310

def kick(user, channel, reason)
	write("KICK #{channel} #{user} :#{reason}")
end

#login(nickname, username, realname) ⇒ Object

log into the irc-server (and connect if necessary)



195
196
197
198
199
# File 'lib/butler/irc/socket.rb', line 195

def (nickname, username, realname)
	connect unless @connected
	write("NICK #{nickname}")
	write("USER #{username} 0 * :#{realname}")
end

#mode(channel, mode) ⇒ Object

send a mode command to a channel



315
316
317
# File 'lib/butler/irc/socket.rb', line 315

def mode(channel, mode)
	write("MODE #{channel} #{mode}")
end

#nick(nick) ⇒ Object

set your own nick does NO verification/validation of any kind



294
295
296
# File 'lib/butler/irc/socket.rb', line 294

def nick(nick)
	write("NICK #{nick}")
end

#normalize_message(message, limit = :message_length) ⇒ Object



214
215
216
217
218
219
220
# File 'lib/butler/irc/socket.rb', line 214

def normalize_message(message, limit=:message_length)
	messages	= []
	message.split(/\n/).each { |line|
		messages.concat(line.chunks(@limit[limit]))
	}
	messages
end

#notice(message, *recipients) ⇒ Object

sends a notice to receiver (or multiple if receiver is array of receivers) formatted=true allows usage of ![]-format commands (see IRCmessage.getFormatted) messages containing newline automatically get splitted up into multiple messages. Too long messages will be tokenized into fitting sized messages (see @limit)



246
247
248
249
250
251
252
# File 'lib/butler/irc/socket.rb', line 246

def notice(message, *recipients)
	normalize_message(message).each { |message|
		recipients.each { |recipient|
			write("NOTICE #{recipient} :#{message}")
		}
	}
end

#op(channel, *users) ⇒ Object

Give Op to user in channel User can be a nick or IRC::User, either one or an array.



321
322
323
# File 'lib/butler/irc/socket.rb', line 321

def op(channel, *users)
	write("MODE #{channel} +#{'o'*users.length} #{users*' '}")
end

#part(reason = nil, *channels) ⇒ Object

part specified channels returns the channels parted from.



279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/butler/irc/socket.rb', line 279

def part(reason=nil, *channels)
	if channels.empty?
		channels = [reason]
		reason   = nil
	end
	reason ||= "leaving"

	# some servers still can't process lists of channels in part
	channels.each { |channel|
		write("PART #{channel} #{reason}")
	}
end

#pong(*args) ⇒ Object

send a pong



255
256
257
258
259
260
261
# File 'lib/butler/irc/socket.rb', line 255

def pong(*args)
	if args.empty? then
		write("PONG")
	else
		write("PONG #{args.join(' ')}")
	end
end

#privmsg(message, *recipients) ⇒ Object

sends a privmsg to given user or channel (or multiple) messages containing newline or exceeding @limit are automatically splitted into multiple messages.



225
226
227
228
229
230
231
# File 'lib/butler/irc/socket.rb', line 225

def privmsg(message, *recipients)
	normalize_message(message).each { |message|
		recipients.each { |recipient|
			write("PRIVMSG #{recipient} :#{message}")
		}
	}
end

#quit(reason = "leaving", close = false) ⇒ Object

send the quit message to the server if you set close to true it will also close the socket



360
361
362
363
# File 'lib/butler/irc/socket.rb', line 360

def quit(reason="leaving", close=false)
	write("QUIT :#{reason}")
	close() if close
end

#readObject

get next message (eol already chomped) from server, blocking, returns nil if closed



136
137
138
139
140
141
142
143
# File 'lib/butler/irc/socket.rb', line 136

def read
	@count[:read]   += 1
	if m = @socket.gets(@eol) then
		m.chomp(@eol)
	else
		nil
	end
end

#voice(channel, *users) ⇒ Object

Give voice to user in channel User can be a nick or IRC::User, either one or an array.



333
334
335
# File 'lib/butler/irc/socket.rb', line 333

def voice(channel, *users)
	write("MODE #{channel} +#{'v'*users.length} #{users*' '}")
end

#who(channel) ⇒ Object

Send a “who” to channel



349
350
351
# File 'lib/butler/irc/socket.rb', line 349

def who(channel)
	write("WHO #{channel}")
end

#whois(nick) ⇒ Object

Send a “whois” to server



354
355
356
# File 'lib/butler/irc/socket.rb', line 354

def whois(nick)
	write("WHOIS #{nick}")
end

#write(data) ⇒ Object

Send a raw message to irc, eol will be appended Use specialized methods instead if possible since they will releave you from several tasks like translating newlines, take care of overlength messages etc. FIXME, wrong methodname, write implies nothing is appended



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/butler/irc/socket.rb', line 150

def write(data)
	@mutex.synchronize {
		warn("Raw too long (#{data.length} instead of #{@limit[:raw_length]})") if (data.length > @limit.raw_length)
		now	= Time.now
		
		# keep delay between single (bursted) messages
		sleeptime = @limit.send_delay-(now-@last_sent)
		if sleeptime > 0 then
			sleep(sleeptime)
			now += sleeptime
		end
		
		# keep delay after a burst (1)
		if (@count[:burst] >= @limit[:burst]) then
			sleeptime = @limit.burst_delay-(now-@last_sent)
			if sleeptime > 0 then
				sleep(sleeptime)
				now += sleeptime
			end
			@count[:burst]	= 0
		end
		
		# keep delay after a burst (2)
		if (@count[:burst2] >= @limit[:burst2]) then
			sleeptime = @limit.burst2_delay-(now-@last_sent)
			if sleeptime > 0 then
				sleep(sleeptime)
				now += sleeptime
			end
			@count[:burst2]	= 0
		end
		
		# send data and update data
		@last_sent       = Time.new
		@socket.write(data+@eol)
		@count[:burst]  += 1
		@count[:burst2] += 1
		@count[:sent]   += 1
	}
rescue IOError
	error("Writing #{data.inspect} failed")
	raise
end