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_with_eol 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#read from only 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

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
115
# 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)
	@log_out      = nil
	@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)



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

def count
  @count
end

#eolObject (readonly)

end-of-line used for communication



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

def eol
  @eol
end

#hostObject (readonly)

the own host (nil if not supported)



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

def host
  @host
end

#limitObject (readonly)

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



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

def limit
  @limit
end

#log_outObject

log raw out, will use log_out.puts(raw)



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

def log_out
  @log_out
end

#portObject (readonly)

port used for connection



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

def port
  @port
end

#serverObject (readonly)

server the instance is linked with



59
60
61
# File 'lib/butler/irc/socket.rb', line 59

def server
  @server
end

Instance Method Details

#action(message, *recipients) ⇒ Object

same as privmsg except it’s formatted for ACTION



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

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

#away(reason = "") ⇒ Object

set your status to away with reason ‘reason’



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

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

#backObject

reset your away status to back



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

def back
	write_with_eol("AWAY")
end

#ban(channel, *masks) ⇒ Object

Set ban in channel to mask



364
365
366
# File 'lib/butler/irc/socket.rb', line 364

def ban(channel, *masks)
	multiple_mode(channel, '+', 'b', masks)
end

#closeObject

closes the connection to the irc-server



391
392
393
394
# File 'lib/butler/irc/socket.rb', line 391

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

#connectObject

connects to the server



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/butler/irc/socket.rb', line 122

def connect
	info("Connecting to #{@server} on port #{@port} from #{@host || '<default>'}")
	@socket	= TCPSocket.open(@server, @port) #, @host)
	info("Successfully connected")
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 Interrupt
	raise
rescue Exception
	error("Connection failed.")
	raise
else
	@connected		= true
end

#connected?Boolean

Returns:

  • (Boolean)


117
118
119
# File 'lib/butler/irc/socket.rb', line 117

def connected?
	@connected
end

#deop(channel, *users) ⇒ Object

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



347
348
349
# File 'lib/butler/irc/socket.rb', line 347

def deop(channel, *users)
	multiple_mode(channel, '-', 'o', 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.



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

def devoice(channel, *users)
	multiple_mode(channel, '-', 'v', 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)



221
222
223
# File 'lib/butler/irc/socket.rb', line 221

def ghost(nickname, password)
	write_with_eol("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)



215
216
217
# File 'lib/butler/irc/socket.rb', line 215

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

#inspectObject

:nodoc:



396
397
398
399
400
401
402
403
404
405
406
# File 'lib/butler/irc/socket.rb', line 396

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.



277
278
279
280
281
282
283
284
285
286
# File 'lib/butler/irc/socket.rb', line 277

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

#kick(user, channel, reason) ⇒ Object

kick user in channel with reason



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

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

#login(nickname, username, realname, serverpass = nil) ⇒ Object

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



205
206
207
208
209
210
# File 'lib/butler/irc/socket.rb', line 205

def (nickname, username, realname, serverpass=nil)
	connect unless @connected
	write_with_eol("PASS #{serverpass}") if serverpass
	write_with_eol("NICK #{nickname}")
	write_with_eol("USER #{username} 0 * :#{realname}")
end

#mode(channel, mode) ⇒ Object

send a mode command to a channel



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

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

#multiple_mode(channel, pre, flag, targets) ⇒ Object

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



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

def multiple_mode(channel, pre, flag, targets)
	(0...targets.length).step(10) { |i|
		slice = targets[i,10]
		write_with_eol("MODE #{channel} +#{flag*slice.length} #{slice*' '}")
	}
end

#nick(nick) ⇒ Object

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



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

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

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



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

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)



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

def notice(message, *recipients)
	normalize_message(message).each { |message|
		recipients.each { |recipient|
			write_with_eol("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.



341
342
343
# File 'lib/butler/irc/socket.rb', line 341

def op(channel, *users)
	multiple_mode(channel, '+', 'o', users)
end

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

part specified channels returns the channels parted from.



290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/butler/irc/socket.rb', line 290

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_with_eol("PART #{channel} #{reason}")
	}
end

#pong(*args) ⇒ Object

send a pong



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

def pong(*args)
	if args.empty? then
		write_with_eol("PONG")
	else
		write_with_eol("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.



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

def privmsg(message, *recipients)
	normalize_message(message).each { |message|
		recipients.each { |recipient|
			write_with_eol("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



385
386
387
388
# File 'lib/butler/irc/socket.rb', line 385

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

#readObject

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



144
145
146
147
148
149
150
151
152
# File 'lib/butler/irc/socket.rb', line 144

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

#unban(channel, *masks) ⇒ Object

Remove ban in channel to mask



369
370
371
# File 'lib/butler/irc/socket.rb', line 369

def unban(channel, *masks)
	multiple_mode(channel, '-', 'b', masks)
end

#voice(channel, *users) ⇒ Object

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



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

def voice(channel, *users)
	multiple_mode(channel, '+', 'v', users)
end

#who(channel) ⇒ Object

Send a “who” to channel



374
375
376
# File 'lib/butler/irc/socket.rb', line 374

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

#whois(nick) ⇒ Object

Send a “whois” to server



379
380
381
# File 'lib/butler/irc/socket.rb', line 379

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

#write_with_eol(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



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
193
194
195
196
197
198
199
200
201
202
# File 'lib/butler/irc/socket.rb', line 159

def write_with_eol(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
		@log_out.puts(data) if @log_out
	}
rescue IOError
	error("Writing #{data.inspect} failed")
	raise
end