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)


88
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
# File 'lib/butler/irc/socket.rb', line 88

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



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

def count
  @count
end

#eolObject (readonly)

end-of-line used for communication



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

def eol
  @eol
end

#hostObject (readonly)

the own host (nil if not supported)



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

def host
  @host
end

#limitObject (readonly)

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



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

def limit
  @limit
end

#portObject (readonly)

port used for connection



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

def port
  @port
end

#serverObject (readonly)

server the instance is linked with



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

def server
  @server
end

Instance Method Details

#action(message, *recipients) ⇒ Object

same as privmsg except it’s formatted for ACTION



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

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’



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

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

#backObject

reset your away status to back



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

def back
	write("AWAY")
end

#ban(mask, channel) ⇒ Object

Set ban in channel to mask



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

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

#closeObject

closes the connection to the irc-server



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

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

#connectObject

connects to the server



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

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.



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

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.



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

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



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

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)



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

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

#inspectObject

:nodoc:



365
366
367
368
369
370
371
372
373
374
375
# File 'lib/butler/irc/socket.rb', line 365

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.



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

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



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

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)



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

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

#nick(nick) ⇒ Object

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



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

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

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



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

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)



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

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.



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

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

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

part specified channels FIXME, better way to implement the reason? use a block (yay)? returns the channels parted from.



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

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



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

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.



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

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



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

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



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

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.



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

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

#who(channel) ⇒ Object

Send a “who” to channel



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

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

#whois(nick) ⇒ Object

Send a “whois” to server



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

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



149
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
# File 'lib/butler/irc/socket.rb', line 149

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