potato 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .DS_Store
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ Potato
2
+ ======
3
+ $ gem install potato
4
+
5
+ THEN
6
+
7
+ $ potato [--port port] [--room room1 --room room2] [--debug] [--max-users N]
8
+
9
+ OR
10
+
11
+ #!/usr/bin/env ruby
12
+ require 'potato'
13
+
14
+ Potato::Server.start(ARGV)
15
+
16
+ THEN
17
+
18
+ Connect your IRC client to localhost on port 6667 (or whatever other port you chose). Potato will give you instructions once connected. Happy potatoing!
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/bin/potato ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require "potato"
3
+
4
+ Potato::Server.start(ARGV)
data/lib/potato.rb ADDED
@@ -0,0 +1,39 @@
1
+ require "socket"
2
+ require "ostruct"
3
+ require "optparse"
4
+ require "timeout"
5
+ require "iconv"
6
+ require "cgi"
7
+ require "open-uri"
8
+ require "net/http"
9
+
10
+ $stdout.sync = true
11
+
12
+ # A dAmn <=> IRC server.
13
+ module Potato
14
+ # Used for making file requiring work properly
15
+ # @api private
16
+ ROOT = File.expand_path(File.dirname(__FILE__))
17
+
18
+ autoload :Server, "#{ROOT}/potato/irc/server.rb"
19
+
20
+ # IRC namespace
21
+ module IRC
22
+ require "#{ROOT}/potato/irc/events.rb"
23
+ autoload :Client, "#{ROOT}/potato/irc/client.rb"
24
+ autoload :Packet, "#{ROOT}/potato/irc/packet.rb"
25
+ autoload :User, "#{ROOT}/potato/irc/user.rb"
26
+ end
27
+
28
+ # dAmn namespace
29
+ module DAmn
30
+ require "#{ROOT}/potato/damn/events.rb"
31
+ autoload :Client, "#{ROOT}/potato/damn/client.rb"
32
+ autoload :Packet, "#{ROOT}/potato/damn/packet.rb"
33
+ autoload :Token, "#{ROOT}/potato/damn/token.rb"
34
+ end
35
+ end
36
+
37
+ Dir["#{Potato::ROOT}/potato/helpers/*.rb"].each do |f|
38
+ require f
39
+ end
@@ -0,0 +1,139 @@
1
+ # Hack to manage reads on dAmn sockets in the select() loop
2
+ class DamnSocket < TCPSocket; end
3
+
4
+ module Potato
5
+ module DAmn
6
+ # Represents a connection to the dAmn server.
7
+ class Client
8
+ include Events
9
+
10
+ # This instance's socket connection to the dAmn server.
11
+ # @return [DamnSocket]
12
+ attr_reader :server
13
+
14
+ # An associative privclass level list.
15
+ # @return [Hash<String, Integer>]
16
+ attr_reader :privclasses
17
+
18
+ # Instance variable hack to prevent unwanted NAMES listing to the IRC client
19
+ # @return [Boolean]
20
+ attr_accessor :whoising
21
+
22
+ # @param [IRC::Client] user a reference to the irc client object that owns this dAmn client
23
+ def initialize user
24
+ @user = user
25
+ @privclasses = {}
26
+ @whoising = false
27
+ end
28
+
29
+ # Handshake and login.
30
+ # @param [String] user username to login with
31
+ # @param [String] token authtoken to login with
32
+ # @see DAmn::Token
33
+ # @return [void]
34
+ def connect_with user, token
35
+ send_packet "dAmnClient", "0.3", :agent => "potato 0.1"
36
+ send_packet "login", user, :pk => token
37
+ end
38
+
39
+ # Attempt to retrieve user information.
40
+ # @param [String] user user to investigate
41
+ # @return [void]
42
+ def whois user
43
+ send_packet "get", "login:#{user}", :p => "info"
44
+ end
45
+
46
+ # Attempt to join a room.
47
+ # @param [String] room room to join
48
+ # @return [void]
49
+ def join room
50
+ send_packet "join", "chat:#{room.sub("#", "")}"
51
+ end
52
+
53
+ # Attempt to part a room.
54
+ # @param [String] room room to part
55
+ # @return [void]
56
+ def part room
57
+ send_packet "part", "chat:#{room.sub("#", "")}"
58
+ end
59
+
60
+ # Attempt to say something in a room.
61
+ # @param [String] room room to speak in
62
+ # @param [String] line line to speak
63
+ # @return [void]
64
+ def say room, line
65
+ send_packet "send", "chat:#{room.sub("#", "")}", :body => packet("msg", "main", :body => line)
66
+ end
67
+
68
+ # Attempt to say an action in a room.
69
+ # @param [String] room room to action in
70
+ # @param [String] line line to say
71
+ # @return [void]
72
+ def action room, line
73
+ send_packet "send", "chat:#{room.sub("#", "")}", :body => packet("action", "main", :body => line)
74
+ end
75
+
76
+ # Attempt to set the topic in a room.
77
+ # @param [String] room the room to set the topic of
78
+ # @param [String] topic the topic to set
79
+ # @return [void]
80
+ def topic room, topic
81
+ send_packet "set", "chat:#{room}", :p => "topic", :body => topic
82
+ end
83
+
84
+ # Disconnect (attempt gracefully) from the server.
85
+ # @return [void]
86
+ def quit
87
+ send_packet "disconnect"
88
+ @server.close
89
+ end
90
+
91
+ # Compose a dAmn packet.
92
+ # @param [String] pktname the packet command ("login", "join", etc.)
93
+ # @param [String] pktparam the packet parameter (usually a username or chatroom)
94
+ # @param [Hash] opts a key-value store of packet arguments
95
+ # @return [String]
96
+ def packet pktname, pktparam = nil, opts = {}
97
+ str = ""
98
+ str << pktname.to_s
99
+ if pktparam
100
+ str << " #{pktparam.to_s}"
101
+ end
102
+ opts.each{|k,v|
103
+ next if k == :body
104
+ str << "\n#{k}=#{v}"
105
+ }
106
+ if opts[:body]
107
+ str << "\n\n"
108
+ str << opts.delete(:body).to_s
109
+ end
110
+ str << "\n" unless pktname == "send"
111
+ str
112
+ end
113
+
114
+ # Write out a packet to the server.
115
+ # @example Writes "login incluye\npk=my authtoken\n\0"
116
+ # send_packet "login", "incluye",
117
+ # :pk => "my authtoken"
118
+ # @example Writes "send chat:Botdom\n\nmsg main\n\nHello world!\n\0"
119
+ # send_packet "send", "chat:Botdom",
120
+ # :body => packet("msg", "main",
121
+ # :body => "Hello world!")
122
+ # @see Client#packet
123
+ # @return [String]
124
+ def send_packet pktname, pktparam = nil, opts = {}
125
+ @server ||= DamnSocket.new "chat.deviantart.com", 3900
126
+ str = packet(pktname, pktparam, opts)
127
+ debug str
128
+ @server.write(str + "\0")
129
+ str
130
+ end
131
+
132
+ # Debug one or more messages.
133
+ # @param [*String] strs any number of messages to print
134
+ def debug *strs
135
+ Server.debug *strs, :damn_out
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,82 @@
1
+ module Potato
2
+ module DAmn
3
+ # @api private
4
+ # Manages packets received from dAmn
5
+ module Events
6
+ # @param [DAmn::Packet] pkt
7
+ # @return [void]
8
+ def on_login pkt
9
+ @user.logged_in = true
10
+ end
11
+
12
+ # @param [DAmn::Packet] pkt
13
+ # @return [void]
14
+ def on_ping pkt
15
+ send_packet "pong"
16
+ end
17
+
18
+ # @param [DAmn::Packet] pkt
19
+ # @return [void]
20
+ def on_join pkt
21
+ @user.join(pkt.room)
22
+ end
23
+
24
+ # @param [DAmn::Packet] pkt
25
+ # @return [void]
26
+ def on_part pkt
27
+ @user.part(pkt.room)
28
+ end
29
+
30
+ # @param [DAmn::Packet] pkt
31
+ # @return [void]
32
+ def on_property pkt
33
+ if pkt.param =~ /login:/ && @whoising
34
+ @user.whois(pkt.param.sub("login:", ""), pkt)
35
+ @whoising = false
36
+ else
37
+ case pkt.args[:p]
38
+ when "topic"
39
+ @user.topic(pkt.room, pkt.body.decode_entities, pkt.args[:by], pkt.args[:ts])
40
+ when "members"
41
+ @user.names(pkt.room, pkt.subpkts)
42
+ when "privclasses"
43
+ @privclasses[pkt.room] = Hash[pkt.body.split(/\n/).map{|x|[x.split(":")[1], x.split(":")[0].to_i]}]
44
+ end
45
+ end
46
+ end
47
+
48
+ # @param [DAmn::Packet] pkt
49
+ # @return [void]
50
+ def on_recv pkt
51
+ case pkt.subpkts[0].cmd
52
+ when "action", "msg"
53
+ @user.send(pkt.subpkts[0].cmd.to_sym,
54
+ pkt.room,
55
+ pkt.subpkts[0].args[:from],
56
+ pkt.body) unless pkt.body.to_tags.strip == @user.config.last_line.strip
57
+ when "join", "part"
58
+ @server.write("get login:#{pkt.subpkts[0].param}\np=info\n\0")
59
+ @user.send((pkt.subpkts[0].cmd + "s").to_sym,
60
+ pkt.subpkts[0].param, pkt.room,
61
+ Packet.new(@server.readline("\0")).subpkts.count{|x|x.cmd == "ns" && x.param == "chat:#{pkt.room}"},
62
+ begin; pkt.subpkts[1].cmd.gsub(/\w+=/, ""); rescue; nil; end,
63
+ begin; pkt.subpkts[1].args[:r]; rescue; nil; end)
64
+ when "kicked"
65
+ @user.kick(pkt.room, pkt.subpkts[0].param, pkt.subpkts[0].args[:by], pkt.subpkts[0].body)
66
+ when "admin"
67
+ case pkt.subpkts[0].param
68
+ when "create", "update"
69
+ @user.send("#{pkt.subpkts[0].param}_privclass".to_sym,
70
+ pkt.room, *pkt.subpkts[0].args.values_at(:name, :privs, :by))
71
+ when "rename"
72
+ @user.rename_privclass(pkt.room, *pkt.subpkts[0].args.values_at(:prev, :name, :by))
73
+ when "remove"
74
+ @user.remove_privclass(pkt.room, *pkt.subpkts[0].args.values_at(:name, :by))
75
+ end
76
+ when "privchg"
77
+ @user.move_user(pkt.room, pkt.subpkts[0].param, pkt.subpkts[0].args[:pc], pkt.subpkts[0].args[:by])
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,112 @@
1
+ module Potato
2
+ module DAmn
3
+ # Parser for dAmn packets.
4
+ class Packet
5
+ # Matches packets that should contain a body.
6
+ BODIED = [
7
+ /^recv p?chat:(.*?)\n\nmsg main/,
8
+ /^recv p?chat:(.*?)\n\naction main/,
9
+ /^property p?chat:(.*?)\np=(topic|title|privclasses)/,
10
+ /^recv p?chat:(.*?)\n\nkicked/,
11
+ /^recv p?chat:(.*?)\n\nadmin show/,
12
+ /^recv p?chat:(.*?)\n\nadmin privclass/,
13
+ /^kicked/
14
+ ]
15
+ # All existing tablumps.
16
+ TABLUMPS = (%w[&b &/b &i &/i &u &/u &s &/s &sup
17
+ &/sup &sub &/sub &code &/code &br
18
+ &ul &/ul &ol &/ol &li &/li &bcode
19
+ &/bcode &/a &/acro &/abbr &p &/p].map{|lump| lump + "\t" } +
20
+ [/&emote\t([^\t]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t/,
21
+ /&a\t([^\t]+)\t([^\t]*)\t/,
22
+ /&link\t([^\t]+)\t&\t/,
23
+ /&link\t([^\t]+)\t([^\t]+)\t&\t/,
24
+ /&dev\t[^\t]\t([^\t]+)\t/,
25
+ /&avatar\t([^\t]+)\t[0-9]+\t/,
26
+ /&thumb\t([0-9]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t([^\t]+)\t/,
27
+ /&img\t([^\t]+)\t([^\t]*)\t([^\t]*)\t/,
28
+ /&iframe\t([^\t]+)\t([0-9%]*)\t([0-9%]*)\t&\/iframe\t/,
29
+ /&acro\t([^\t]+)\t/,
30
+ /&abbr\t([^\t]+)\t/,
31
+ /^.+?<abbr title="(.+?)"><\/abbr>:/]).
32
+ zip(["\x02", "\x0F", "\x16", "\x0F", "\x1F", "\x0F", "<s>", "</s>",
33
+ "<sup>", "</sup>", "<sub>", "</sub>", "<code>", "</code>", "<br>",
34
+ "<ul>", "</ul>", "<ol>", "</ol>", "<li>", "</li>", "", "", "</a>",
35
+ "</acronym>", "", "<p>", "</p>", '\1', '<a href="\1" title="\2">',
36
+ '\1', '\1 (\2)', ':dev\1:', ':icon\1:', ':thumb\1:',
37
+ '<img src="\1" alt="\2" title="\3" />', '<iframe src="\1" width="\2" height="\3" />',
38
+ '<acronym title="\1">', '', '\1:', ''])
39
+
40
+ # @param [String] pkt the string received from the server to parse
41
+ # @raise [PacketNilException] if the string is nil, meaning that the server connection has been closed
42
+ def initialize pkt
43
+ TABLUMPS.each{|find, replace| pkt.gsub!(find, replace)}
44
+ @response = {:cmd => nil, :args => {}, :param => nil, :subpkts => [], :raw => pkt}
45
+ chunks = pkt.split(/\n\n/)
46
+ details = chunks.shift.split(/\n/)
47
+ @response[:cmd], @response[:param] = *details.shift.split(" ")
48
+ details.each{|dt|
49
+ key, value = *dt.split(/\W/, 2)
50
+ @response[:args][key.to_sym] = CGI.unescapeHTML(value)
51
+ }
52
+ body = BODIED.map{|x|!!(pkt =~ x)}.include?(true)
53
+ range = if body then chunks[0...-1] else chunks end
54
+ range.each{|c|
55
+ @response[:subpkts] << self.class.new(c)
56
+ }
57
+ if body && chunks[-1]
58
+ if pkt =~ /\n\n$/
59
+ @response[:subpkts] << self.class.new(chunks[-1])
60
+ else
61
+ @response[:body] = CGI.unescapeHTML(chunks[-1].chomp("\0").decode_entities)
62
+ end
63
+ end
64
+ end
65
+
66
+ # @return [String] the unformatted chat name
67
+ def room
68
+ if @response[:param] =~ /p?chat:/ then @response[:param].sub(/p?chat:/, "") else nil end
69
+ end
70
+
71
+ # @return [Boolean] whether the current packet has an error or not
72
+ def ok?
73
+ @response[:args][:e].nil? || @response[:args][:e] == "ok"
74
+ end
75
+
76
+ # @return [String] the current packet error
77
+ def error
78
+ @response[:args][:e]
79
+ end
80
+
81
+ # @yield [argument] iterates through each argument
82
+ def each &blk
83
+ @response[:args].each(&blk)
84
+ end
85
+
86
+ # @param [String] key the key to retrieve from the arguments
87
+ # @return [String] the packet argument
88
+ def [](key) @response[:args][key.to_sym] end
89
+
90
+ # @return [Packet] the first subpacket
91
+ def subpkt
92
+ @response[:subpkts][0]
93
+ end
94
+
95
+ # Lookup @response entries, struct-style.
96
+ def method_missing(m, *args, &blk) @response[m] end
97
+
98
+ # Generates a human-readable representation of this packet
99
+ # @return [String]
100
+ def inspect
101
+ str = "#<Potato::DAmn::Packet '#{@response[:cmd]} #{@response[:param]}'"
102
+ str << ", args={#{@response[:args].map{|k,v|
103
+ "#{k}: #{v}"
104
+ }.join(", ")}}" if @response[:args].size > 0
105
+ str << ", body='#{@response[:body].rstrip}'" if @response[:body]
106
+ str << ", subpackets=#{@response[:subpkts]}" if @response[:subpkts].size > 0
107
+ str << ">"
108
+ str
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,53 @@
1
+ module Potato
2
+ module DAmn
3
+ # Interface to grab dAmn authtokens.
4
+ module Token
5
+ extend self
6
+
7
+ # Converts a hash to a URL query string.
8
+ # @param [Hash<String, String>] hash
9
+ # @return [String]
10
+ def safe_qstring hash
11
+ hash.map do |a,b|
12
+ a + "=" + b.to_s.gsub(/([^A-Za-z0-9\-])/) do
13
+ '%' + $1.unpack("U*")[0].to_s(16)
14
+ end
15
+ end.join("&")
16
+ end
17
+
18
+ # Gets an authtoken.
19
+ # @param [String] username
20
+ # @param [String] password
21
+ # @return [String, nil]
22
+ def get username, password
23
+ uri = URI.parse("https://www.deviantart.com/users/login")
24
+ http = Net::HTTP.new(uri.host, uri.port)
25
+ http.use_ssl = true
26
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
27
+ cookies = nil
28
+ begin
29
+ http.start do
30
+ data = Token.safe_qstring({"username" => username, "password" => password, "remember_me" => 1})
31
+ http.request_post(uri.path,data) do |res|
32
+ uri = URI.parse("http://chat.deviantart.com/chat/Botdom")
33
+ Net::HTTP.new(uri.host, uri.port).start do |ht|
34
+ request = Net::HTTP::Get.new(uri.path)
35
+ request["cookie"] = res["Set-Cookie"].scan(/[a-z_]+=[^ ]{20,}/).join(";")
36
+ ht.request(request) do |req|
37
+ if req.body.empty?
38
+ return nil
39
+ else
40
+ return req.body.scan(/[0-9a-f]{32}/)[0]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ rescue Errno::ECONNRESET
47
+ warn "Connection reset by peer when attempting to retrieve authtoken."
48
+ nil
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,28 @@
1
+ module Potato
2
+ # Mixin module for helper methods we want to add to classes.
3
+ module Helpers
4
+ # Helpers for String.
5
+ module String
6
+ # Convert numeric and hexadecimal HTML entities to Unicode codepoints.
7
+ # @return [String]
8
+ def decode_entities
9
+ gsub(/&#(\d+);/){[$1.to_i].pack("U*")}.gsub(/&#x([0-9a-fA-F]+);/u){[$1.to_i(16)].pack("U*")}
10
+ end
11
+
12
+ # Convert Unicode codepoints to numeric HTML entities.
13
+ # @return [String]
14
+ def encode_entities
15
+ gsub(/([^ a-zA-Z0-9_.\-'",;!@#\$%^&\*\(\)\{\}\?\/\\<>=\+:])/u){"&##{$1.unpack("U*")[0]};"}
16
+ end
17
+
18
+ # Convert IRC formatting to tags.
19
+ # @see Potato::DAmn::Client
20
+ # @return [String]
21
+ def to_tags
22
+ gsub(/\x02(.*?)\x0F/u, '<b>\1</b>').gsub(/\x16(.*?)\x0F/u, '<i>\1</i>').gsub(/\x1F(.*?)\x0F/u, '<u>\1</u>')
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ class String; include Potato::Helpers::String; end
@@ -0,0 +1,370 @@
1
+ module Potato
2
+ module IRC
3
+ # A single client that intends to use dAmn.
4
+ class Client
5
+ include Events
6
+
7
+ # The connection to the IRC user associated with this instance.
8
+ # @return [TCPSocket]
9
+ attr_reader :socket
10
+
11
+ # The dAmn connection associated with this instance.
12
+ # @return [DAmn::Client]
13
+ attr_reader :client
14
+
15
+ # Configuration value store
16
+ # @return [OpenStruct]
17
+ attr_reader :config
18
+
19
+ # Have we logged in?
20
+ # @return [Boolean]
21
+ attr_accessor :logged_in
22
+
23
+ def initialize sock
24
+ @config = OpenStruct.new({:last_line => ""})
25
+ @socket = sock
26
+ @client = Potato::DAmn::Client.new(self)
27
+ @logged_in = false
28
+ @users = {}
29
+ end
30
+
31
+ # Triggered when a user first connects.
32
+ # @return [void]
33
+ def welcome!
34
+ send_packet :cmd => "localhost", :args => ["001", @config.nick],
35
+ :content => "Welcome to potato #{@config.nick}!#{@config.nick}@chat.deviantart.com"
36
+ send_packet :cmd => "localhost", :args => ["002", @config.nick],
37
+ :content => "Your host is localhost, running version potato0.1"
38
+ send_packet :cmd => "localhost", :args => ["004", @config.nick,
39
+ "iowghraAsORTVSxNCWqBzvdHtGp lvhopsmntikrRcaqOALQbSeIKVfMCuzNTGj"]
40
+ send_packet :cmd => "localhost", :args => ["005", @config.nick,
41
+ %w[UHNAMES NAMESX SAFELIST HCN MAXCHANNELS=20 CHANLIMIT=#:20 MAXLIST=b:60,e:60,I:60
42
+ NICKLEN=20 CHANNELLEN=100 TOPICLEN=8192 KICKLEN=8192 AWAYLEN=8192 MAXTARGETS=20]].flatten,
43
+ :content => "are supported by this server"
44
+ send_packet :cmd => "localhost", :args => ["005", @config.nick,
45
+ %w[MODES=12 CHANTYPES=# PREFIX=(qaohv)~&@%+ NETWORK=dAmn ELIST=MNUCT STATUSMSG=~&@%+]].flatten,
46
+ :content => "are supported by this server"
47
+ send_packet :cmd => @config.nick, :args => ["MODE", @config.nick], :content => "+i"
48
+ greeting = <<-EOG
49
+ Welcome to dAmn, #{@config.nick}!
50
+ To connect you need to give me some information.
51
+ Type /pass yourpassword to authenticate.
52
+ EOG
53
+ greeting.split(/\n/).each do |g|
54
+ send_packet :colon => false, :cmd => "NOTICE", :args => %w[AUTH], :content => "*** #{g}"
55
+ end
56
+ end
57
+
58
+ # Returns an IRC hostname.
59
+ # @param [String] user user to generate a hostname for
60
+ # @return [String]
61
+ def host_for user
62
+ "%s!%s@chat.deviantart.com" % [user, user]
63
+ end
64
+
65
+ # Rejects one or more join requests if the user isn't logged in.
66
+ # @param [Array<String>] chans channels to notify for
67
+ # @return [void]
68
+ def cannot_join *chans
69
+ chans.each do |chan|
70
+ send_packet :cmd => "localhost", :args => [473, @config.nick, chan],
71
+ :content => "Not authenticated"
72
+ end
73
+ end
74
+
75
+ # Notify that you have joined a room
76
+ # @param [String] room the joined room
77
+ # @return [void]
78
+ def join room
79
+ send_packet :cmd => @config.host, :args => "JOIN", :content => "##{room}"
80
+ end
81
+
82
+ # Notify that you have parted a room
83
+ # @param [String] room the parted room
84
+ # @return [void]
85
+ def part room
86
+ send_packet :cmd => @config.host, :args => "PART", :content => "##{room}"
87
+ end
88
+
89
+ # Notify that another user has joined a room
90
+ # @param [String] user the user that has joined
91
+ # @param [String] room the room that the user has joined
92
+ # @param [Integer] conns the number of connections the user has in which they are joined to this channel
93
+ # @param [String] privclass the name of the privclass in which the user can be found
94
+ # @return [void]
95
+ def joins user, room, conns, privclass, reason
96
+ if conns < 2
97
+ @users[room] << User.new(user, privclass, sym(@client.privclasses[room][privclass]),
98
+ @client.privclasses[room][privclass])
99
+ send_packet :cmd => host_for(user), :args => "JOIN", :content => "##{room}"
100
+ send_packet :cmd => "localhost", :args =>
101
+ ["MODE", "##{room}", "+#{sym_to_level(sym(@client.privclasses[room][privclass]))}", user]
102
+ else
103
+ chan_notice(room, "#{user} has joined again (now joined #{conns} times)")
104
+ end
105
+ end
106
+
107
+ # Notify that a user has parted a room
108
+ # @param [String] user the user that has parted
109
+ # @param [String] room the room that the user has parted
110
+ # @param [Integer] conns the number of connections the user has in which they are joined to this channel
111
+ # @param [String] privclass
112
+ # @return [void]
113
+ def parts user, room, conns, privclass, reason
114
+ if conns < 1
115
+ @users[room].delete_if{|x|x.username == user}
116
+ send_packet :cmd => host_for(user), :args => ["PART", "##{room}"], :content => reason
117
+ else
118
+ chan_notice(room, "#{user} has parted (now joined #{conns} time#{"s" unless conns == 1})")
119
+ end
120
+ end
121
+
122
+ # A /names list for the current room.
123
+ # @param [String] room the room to query
124
+ # @param [Array<DAmn::Packet>] packets
125
+ # @return [void]
126
+ def names room, packets
127
+ @users[room] = packets.map{|x|User.new(x.param, x.args[:pc], sym(@client.privclasses[room][x.args[:pc]]),
128
+ @client.privclasses[room][x.args[:pc]])}
129
+ send_packet :cmd => "localhost", :args => [353, @config.nick, "=", "##{room}"],
130
+ :content => @users[room].reject{|k|k.username == @config.nick}.map{|k|k.symbol + k.username}.join(" ")
131
+ send_packet :cmd => "localhost", :args => [366, @config.nick, "##{room}"],
132
+ :content => "End of /NAMES list."
133
+ me_mode = sym_to_level(sym(@client.privclasses[room][packets.find{|x|x.param == @config.nick}.args[:pc]]))
134
+ unless me_mode.empty?
135
+ send_packet :cmd => "localhost", :args =>
136
+ ["MODE", "##{room}", "+#{me_mode}", @config.nick]
137
+ end
138
+ end
139
+
140
+ # A /whois query for a user.
141
+ # @param [String] name the queried user
142
+ # @param [DAmn::Packet] packet
143
+ # @return [void]
144
+ def whois name, packet
145
+ send_packet :cmd => "localhost", :args => [311, @config.nick, name, name, "chat.deviantart.com", "*"],
146
+ :content => packet.subpkts[0].args[:realname]
147
+ send_packet :cmd => "localhost", :args => [307, @config.nick, name],
148
+ :content => "is a registered nick"
149
+ send_packet :cmd => "localhost", :args => [319, @config.nick, name],
150
+ :content => packet.subpkts.select{|pk|pk.cmd == "ns"}.map{|x|x.param.sub("chat:", "#")}.uniq.join(" ")
151
+ send_packet :cmd => "localhost", :args => [312, @config.nick, name, "localhost"],
152
+ :content => "potato"
153
+ send_packet :cmd => "localhost", :args => [317, @config.nick, name,
154
+ packet.subpkts.find{|pk|pk.cmd == "conn"}[:idle],
155
+ (Time.now - packet.subpkts.find{|pk|pk.cmd == "conn"}[:online].to_i).to_i],
156
+ :content => "seconds idle, signon time"
157
+ send_packet :cmd => "localhost", :args => [318, @config.nick, name],
158
+ :content => "End of /WHOIS list."
159
+ end
160
+
161
+ # Convert a privclass level to a ~&@%+ symbol
162
+ # @param [#to_i] level the privclass level
163
+ # @return [String]
164
+ def sym level
165
+ case level.to_i
166
+ when 0..30
167
+ ""
168
+ when 31..70
169
+ "+"
170
+ when 71..80
171
+ "%"
172
+ when 81..98
173
+ "@"
174
+ when 99
175
+ "~"
176
+ end
177
+ end
178
+
179
+ # Convert a ~&@%+ symbol to a mode qaohv
180
+ # @param [String] sym the symbol to convert
181
+ # @return [String]
182
+ def sym_to_level sym
183
+ case sym
184
+ when ""
185
+ ""
186
+ when "~"
187
+ "q"
188
+ when "@"
189
+ "o"
190
+ when "+"
191
+ "v"
192
+ when "%"
193
+ "h"
194
+ end
195
+ end
196
+
197
+ # Notify that the topic has been set in the current room
198
+ # @param [String] room the room in which the topic has been set
199
+ # @param [String] topic the topic set
200
+ # @param [String] by the user who set the topic
201
+ # @param [String] time the time in seconds (since epoch) at which the topic was set
202
+ # @return [void]
203
+ def topic room, topic, by, time
204
+ send_packet :cmd => "localhost", :args => [332, @config.nick, "##{room}"],
205
+ :content => topic
206
+ send_packet :cmd => "localhost", :args => [333, @config.nick, "##{room}", by, time]
207
+ end
208
+
209
+ # Display a message from a user in a room.
210
+ # @param [String] room the receiving room
211
+ # @param [String] user the sayer of the line
212
+ # @param [String] line the line said
213
+ # @return [void]
214
+ def msg room, user, line
215
+ line.gsub("<br>", "\n").split(/\n/).each do |l|
216
+ send_packet :cmd => host_for(user), :args => ["PRIVMSG", "##{room}"],
217
+ :content => l
218
+ end
219
+ end
220
+
221
+ # Display an action from a user in a room.
222
+ # @param [String] room the receiving room
223
+ # @param [String] user the perpetrator of the action
224
+ # @param [String] line the action perpetrated
225
+ # @return [void]
226
+ def action room, user, line
227
+ line.gsub("<br>", "\n").split(/\n/).each do |l|
228
+ send_packet :cmd => host_for(user), :args => ["PRIVMSG", "##{room}"],
229
+ :content => "\x01ACTION #{l}\x01"
230
+ end
231
+ end
232
+
233
+ # Notify that a user has been kicked from a room.
234
+ # @param [String] room the room from which somebody has been kicked
235
+ # @param [String] user the kickee
236
+ # @param [String] by the kicker
237
+ # @param [String] line the reason for kicking
238
+ # @return [void]
239
+ def kick room, user, by, line
240
+ send_packet :cmd => host_for(by), :args => ["KICK", "##{room}", user], :content => line || by
241
+ end
242
+
243
+ # Notify that a privclass has been created in a room.
244
+ # @param [String] room the room in which has been added the privclass
245
+ # @param [String] privclass the name of the created privclass
246
+ # @param [String] privs the privileges of the created privclass
247
+ # @param [String] by the creator of the privclass
248
+ # @return [void]
249
+ def create_privclass room, privclass, privs, by
250
+ chan_notice room, "Privilege class \x16#{privclass}\x0F has been created by #{by} with {#{privs.split(" ").map{|x|x.split("=").join(": ")}.join(", ")}}"
251
+ end
252
+
253
+ # Notify that a privclass has been updated in a room.
254
+ # @param [String] room the room in which has been updated the privclass
255
+ # @param [String] privclass the name of the updated privclass
256
+ # @param [String] privs the privileges of the created privclass
257
+ # @param [String] by the updator of the privclass
258
+ # @return [void]
259
+ def update_privclass room, privclass, privs, by
260
+ chan_notice room, "Privilege class \x16#{privclass}\x0F has been updated by #{by} with {#{privs.split(" ").map{|x|x.split("=").join(": ")}.join(", ")}}"
261
+ users = @users[room].select{|x|x.privclass == privclass}
262
+ mode = ""
263
+ count = 0
264
+ unless users[0].symbol == sym(@client.privclasses[room][privclass])
265
+ unless users[0].symbol.empty?
266
+ mode << "-#{sym_to_level(users[0].symbol) * users.size}"
267
+ count += 1
268
+ end
269
+ unless sym(@client.privclasses[room][privclass]).empty?
270
+ mode << "+#{sym_to_level(sym(@client.privclasses[room][privclass])) * users.size}"
271
+ count += 1
272
+ end
273
+ send_packet :cmd => "localhost", :args =>
274
+ ["MODE", "##{room}", mode, *(users.map(&:username) * count)]
275
+ users.each do |user|
276
+ user.symbol = sym(@client.privclasses[room][privclass])
277
+ user.privclass = privclass
278
+ user.level = @client.privclasses[room][privclass]
279
+ end
280
+ end
281
+ end
282
+
283
+ # Notify that a privclass has been renamed in a room.
284
+ # @param [String] room the room in which has been renamed the privclass
285
+ # @param [String] prev the previous name of the privclass
286
+ # @param [String] priv the new name of the privclass
287
+ # @param [String] by the renamer of the privclass
288
+ # @return [void]
289
+ def rename_privclass room, prev, priv, by
290
+ chan_notice room, "Privclass \x16#{prev}\x0F has been renamed to \x16#{priv}\x0F by #{by}"
291
+ end
292
+
293
+ # Notify that a privclass has been removed from a room.
294
+ # @param [String] room the room from which the privclass has been removed
295
+ # @param [String] privclass the name of the removed privclass
296
+ # @param [String] by the remover of the privclass
297
+ # @return [void]
298
+ def remove_privclass room, privclass, by
299
+ chan_notice room, "Privclass \x16#{privclass}\x0F has been removed by #{by}"
300
+ end
301
+
302
+ # Notify that a user has been moved from one privclass to another.
303
+ # @param [String] room the room in which the user has been moved
304
+ # @param [String] user the name of the moved user
305
+ # @param [String] privclass the privclass to which the user has been moved
306
+ # @param [String] by the mover of the user
307
+ # @return [void]
308
+ def move_user room, user, privclass, by
309
+ chan_notice room, "#{user} has been moved to \x16#{privclass}\x0F by #{by}"
310
+ us = @users[room].find{|x|x.username == user}
311
+ mode = ""
312
+ count = 0
313
+ unless us.symbol == sym(@client.privclasses[room][privclass])
314
+ unless us.symbol.empty?
315
+ mode << "-#{sym_to_level(us.symbol)}"
316
+ count += 1
317
+ end
318
+ unless sym(@client.privclasses[room][privclass]).empty?
319
+ mode << "+#{sym_to_level(sym(@client.privclasses[room][privclass]))}"
320
+ count += 1
321
+ end
322
+ send_packet :cmd => "localhost", :args =>
323
+ ["MODE", "##{room}", mode, *([us.username] * count)]
324
+ us.symbol = sym(@client.privclasses[room][privclass])
325
+ us.privclass = privclass
326
+ us.level = @client.privclasses[room][privclass]
327
+ end
328
+ end
329
+
330
+ # Sends you an auth notice (in the server window)
331
+ # @param [String] line the line to display
332
+ # @return [void]
333
+ def notice line
334
+ send_packet :colon => false, :cmd => "NOTICE", :args => "AUTH", :content => "*** #{line}"
335
+ end
336
+
337
+ # Sends you a channel notice
338
+ # @param [String] room the room to notice
339
+ # @param [String] line the line to display
340
+ # @return [void]
341
+ def chan_notice room, line
342
+ send_packet :cmd => "localhost", :args => ["NOTICE", "##{room}"], :content => line
343
+ end
344
+
345
+ # Compiles a packet and sends it to the server
346
+ # @param [Hash] opts a list of options
347
+ # @option opts [String] :cmd
348
+ # @option opts [Array] :args
349
+ # @option opts [String, nil] :content
350
+ # @option opts [Boolean] :colon whether to put a colon in front or not
351
+ # @see IRC::Packet
352
+ # @return [void]
353
+ def send_packet opts = {}
354
+ opts = ({:colon => true, :cmd => "", :args => [], :content => nil}).merge(opts)
355
+ str = opts[:colon] ? ":" : ""
356
+ str << opts[:cmd] << " " << Array(opts[:args]).join(" ")
357
+ str << " :#{opts[:content]}" if opts[:content]
358
+ @socket.write(str + "\r\n")
359
+ debug str
360
+ end
361
+
362
+ # Sends debug messages
363
+ # @param [Array] strs all messages to send
364
+ # @return [void]
365
+ def debug *strs
366
+ Server.debug *strs, :irc_out
367
+ end
368
+ end
369
+ end
370
+ end
@@ -0,0 +1,138 @@
1
+ module Potato
2
+ module IRC
3
+ # Module for handling lines sent from an IRC client.
4
+ # @api private
5
+ module Events
6
+ # @param [IRC::Packet] pkt
7
+ # @return [void]
8
+ def on_nick pkt
9
+ @config.nick = pkt.args[0]
10
+ @config.host = host_for(pkt.args[0])
11
+ end
12
+
13
+ # @param [IRC::Packet] pkt
14
+ # @return [void]
15
+ def on_user pkt
16
+ welcome!
17
+ end
18
+
19
+ # @param [IRC::Packet] pkt
20
+ # @return [void]
21
+ def on_mode pkt
22
+ if pkt.args[0] =~ /#/
23
+ if pkt.args[1] == "b"
24
+ send_packet :cmd => "localhost", :args => [368, @config.nick, pkt.args[0]],
25
+ :content => "End of Channel Ban List"
26
+ else
27
+ send_packet :cmd => "localhost", :args => [324, @config.nick, pkt.args[0], "+"]
28
+ send_packet :cmd => "localhost", :args => [329, @config.nick, pkt.args[0], Time.now.to_i]
29
+ end
30
+ end
31
+ end
32
+
33
+ # @param [IRC::Packet] pkt
34
+ # @return [void]
35
+ def on_who pkt
36
+ users = @users[pkt.args[0].sub("#", "")].map(&:first)
37
+ users.uniq.each do |user|
38
+ send_packet :cmd => "localhost", :args => [352, @config.nick, pkt.args[0], user,
39
+ host_for(user), "localhost", user], :content => "0 #{user}"
40
+ end
41
+ send_packet :cmd => "localhost", :args => [315, @config.nick, pkt.args[0]],
42
+ :content => "End of /WHO list."
43
+ end
44
+
45
+ # @param [IRC::Packet] pkt
46
+ # @return [void]
47
+ def on_whois pkt
48
+ @client.whoising = true
49
+ @client.whois(pkt.args[0])
50
+ end
51
+
52
+ # @param [IRC::Packet] pkt
53
+ # @return [void]
54
+ def on_ping pkt
55
+ @socket.write("PONG #{pkt.args[0]}\r\n")
56
+ end
57
+
58
+ # @param [IRC::Packet] pkt
59
+ # @return [void]
60
+ def on_join pkt
61
+ rooms = pkt.args[0].include?(",") ? pkt.args[0].split(",") : pkt.args
62
+ unless @logged_in
63
+ cannot_join(*rooms)
64
+ else
65
+ rooms.each do |room|
66
+ @client.join(room)
67
+ end
68
+ end
69
+ end
70
+
71
+ # @param [IRC::Packet] pkt
72
+ # @return [void]
73
+ def on_pass pkt
74
+ notice "Attempting to retrieve authtoken."
75
+ begin
76
+ tok = Timeout::timeout(12) {
77
+ Potato::DAmn::Token.get(@config.nick, pkt.args[0])
78
+ }
79
+ rescue Timeout::Error
80
+ notice "dAmn server is down. Try again later."
81
+ @client.quit
82
+ @socket.close
83
+ return
84
+ end
85
+ if tok.nil?
86
+ notice "Unable to retrieve an authtoken (server didn't respond)."
87
+ @client.quit
88
+ @socket.close
89
+ return
90
+ elsif tok.empty?
91
+ notice "Wrong password or username. (no authtoken retrieved)"
92
+ else
93
+ notice "You have successfully logged in."
94
+ @logged_in = true
95
+ @config.token = tok
96
+ @client.connect_with(@config.nick, @config.token)
97
+ unless Server.opts.rooms.empty?
98
+ Server.opts.rooms.each do |room|
99
+ @client.join(room)
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ # @param [IRC::Packet] pkt
106
+ # @return [void]
107
+ def on_part pkt
108
+ @client.part(pkt.room)
109
+ end
110
+
111
+ # @param [IRC::Packet] pkt
112
+ # @return [void]
113
+ def on_privmsg pkt
114
+ pkt.args.each do |room|
115
+ if pkt.content =~ /\x01ACTION/
116
+ @client.action(room, pkt.content.gsub(/\x01(ACTION|\x00)?\s*/, "").encode_entities)
117
+ else
118
+ @client.say(room, pkt.content.encode_entities)
119
+ end
120
+ end
121
+ @config.last_line = pkt.content.gsub(/\x01(ACTION|\x00)?\s*/, "")
122
+ end
123
+
124
+ # @param [IRC::Packet] pkt
125
+ # @return [void]
126
+ def on_topic pkt
127
+ @client.topic(pkt.room, pkt.content)
128
+ end
129
+
130
+ # @param [IRC::Packet] pkt
131
+ # @return [void]
132
+ def on_quit pkt
133
+ @client.quit
134
+ @socket.close
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,41 @@
1
+ module Potato
2
+ module IRC
3
+ # A line from an IRC client.
4
+ class Packet
5
+ # The first word of the packet, usually all-caps.
6
+ # @return [String]
7
+ attr_reader :cmd
8
+
9
+ # Any words in the packet that aren't part of the content.
10
+ # @return [Array<String>]
11
+ attr_reader :args
12
+
13
+ # Content of the packet (after a colon).
14
+ # @return [String, nil]
15
+ attr_reader :content
16
+
17
+ # The actual packet string.
18
+ # @return [String]
19
+ attr_reader :raw
20
+
21
+ def initialize str
22
+ str = str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
23
+ @raw = str
24
+ if str.include?(" :")
25
+ @cmd, *@args, @content = str.split(" ", str[0...str.index(" :")+1].count(" ") + 1)
26
+ @content.slice!(0)
27
+ else
28
+ @cmd, *@args = str.split(" ")
29
+ end
30
+ @cmd.upcase!
31
+ @content = @args.pop(@args.size - 1).join(" ") if @cmd == "PRIVMSG" && @content.nil?
32
+ end
33
+
34
+ # The first argument whose first character is an octothorpe (#), or nil if none is found.
35
+ # @return [String]
36
+ def room
37
+ @args.find{|a|a =~ /^#/}[1..-1]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,115 @@
1
+ module Potato
2
+ # An IRC server.
3
+ module Server
4
+ extend self
5
+
6
+ # A list of the current connections (IRC-dAmn clients)
7
+ # @return [Hash<IO, IRC::Client>]
8
+ attr_reader :connections
9
+
10
+ # Command-line options.
11
+ # @return [Hash]
12
+ attr_reader :opts
13
+
14
+ # Write color-coded debug information if debug is enabled.
15
+ def debug(*items, type)
16
+ color = {:damn => 33, :irc => 35, :damn_out => 32, :irc_out => 34, :sys => 31}[type || :sys]
17
+ colon = (type == :damn_out || type == :irc_out) ? ">>" : "<<"
18
+ if @opts.debug
19
+ items.each do |line|
20
+ line.split("\n").each do |l|
21
+ puts "\e[#{color}m#{colon}\e[0m #{l}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # Start the server and listen for connections.
28
+ # @param [Hash] opts command-line options
29
+ # @return [void]
30
+ def start opts
31
+ @connections = {}
32
+ @opts = self.parse_opts(opts)
33
+ @server = TCPServer.new(@opts.port)
34
+
35
+ loop do
36
+ begin
37
+ action = IO.select([@server, @connections.keys, @connections.values.map{|x|x.client.server}].flatten.compact, nil, nil)[0][0]
38
+ rescue IOError
39
+ @connections.delete_if { |k,v| v.client.server.closed? || k.closed? }
40
+ retry
41
+ rescue Interrupt
42
+ puts "\033[2DCaught interrupt (^C), exiting."
43
+ exit
44
+ end
45
+ if action == @server
46
+ if @connections.size <= @opts.max || @opts.max < 0
47
+ @connections[cl = @server.accept] = IRC::Client.new(cl)
48
+ debug "Accepted client #{cl}"
49
+ else
50
+ debug "Client #{cl = @server.accept} requested connection, but user limit (#{@opts.max}) has been reached"
51
+ cl.close
52
+ end
53
+ else
54
+ if DamnSocket === action
55
+ begin
56
+ pkt = DAmn::Packet.new(Iconv.conv("UTF-8", "LATIN1", action.readline("\0").chomp("\0")))
57
+ rescue Errno::ECONNRESET, EOFError => e
58
+ @connections.values.each do |cl|
59
+ cl.notice "Lost connection to dAmn (#{e.class})."
60
+ cl.socket.close
61
+ end
62
+ @connections = {}
63
+ end
64
+ debug pkt.raw, :damn
65
+ client = @connections.values.find{|x|x.client.server == action}.client
66
+ client.send("on_#{pkt.cmd}".to_sym, pkt) if client.respond_to?("on_#{pkt.cmd}".to_sym)
67
+ else
68
+ if action.eof?
69
+ @connections.delete(action)
70
+ debug "Client #{action} disconnected"
71
+ else
72
+ client = @connections[action]
73
+ pkt = IRC::Packet.new(action.readline("\r\n").chomp("\r\n"))
74
+ debug pkt.raw, :irc
75
+ client.send("on_#{pkt.cmd.downcase}".to_sym, pkt) if client.respond_to?("on_#{pkt.cmd.downcase}".to_sym)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ # Parses command-line options into <code>@opts</code>.
83
+ # @return [Hash]
84
+ def parse_opts(args)
85
+ options = OpenStruct.new
86
+ options.rooms = []
87
+ options.port = 6667
88
+ options.debug = false
89
+ options.max = -1
90
+
91
+ o = OptionParser.new do |opts|
92
+ opts.banner = "Usage: potato [options]"
93
+
94
+ opts.on("-p", "--port PORT", Integer, "Specify port number (default 6667)") do |v|
95
+ options.port = v
96
+ end
97
+
98
+ opts.on("-r", "--room ROOM", "Add a room to the autojoin list") do |v|
99
+ options.rooms << v
100
+ end
101
+
102
+ opts.on("--debug", "Turn debug mode on or off (prints color-coded messages to stdout)") do |v|
103
+ options.debug = v
104
+ end
105
+
106
+ opts.on("--max-users N", Integer, "Set the maximum number of users that may connect to this server (default unlimited)") do |v|
107
+ options.max = v
108
+ end
109
+ end
110
+
111
+ o.parse!(args)
112
+ options
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,10 @@
1
+ module Potato
2
+ module IRC
3
+ # Storage for a user's privilege level. Room-specific.
4
+ # @attr [String] username
5
+ # @attr [String] privclass
6
+ # @attr [String] symbol
7
+ # @attr [Integer] level
8
+ User = Struct.new(:username, :privclass, :symbol, :level)
9
+ end
10
+ end
@@ -0,0 +1,4 @@
1
+ module Potato
2
+ # Current version of Potato.
3
+ VERSION = "0.0.6"
4
+ end
data/potato.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "potato/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "potato"
7
+ s.version = Potato::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Joel Taylor"]
10
+ s.email = ["holla@joeldt.net"]
11
+ s.homepage = ""
12
+ s.summary = %q{A dAmn <=> IRC proxy.}
13
+ s.description = %q{Potato is an IRC server that communicates with the deviantART Message Network and allows you to treat it like a regular IRC server, with support for user modes, kicks/bans, /whois-ing, etc.}
14
+
15
+ s.rubyforge_project = "potato"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: potato
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.6
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Joel Taylor
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-04-22 00:00:00.000000000 -04:00
13
+ default_executable:
14
+ dependencies: []
15
+ description: Potato is an IRC server that communicates with the deviantART Message
16
+ Network and allows you to treat it like a regular IRC server, with support for user
17
+ modes, kicks/bans, /whois-ing, etc.
18
+ email:
19
+ - holla@joeldt.net
20
+ executables:
21
+ - potato
22
+ extensions: []
23
+ extra_rdoc_files: []
24
+ files:
25
+ - .gitignore
26
+ - .yardopts
27
+ - README.md
28
+ - Rakefile
29
+ - bin/potato
30
+ - lib/potato.rb
31
+ - lib/potato/damn/client.rb
32
+ - lib/potato/damn/events.rb
33
+ - lib/potato/damn/packet.rb
34
+ - lib/potato/damn/token.rb
35
+ - lib/potato/helpers/string.rb
36
+ - lib/potato/irc/client.rb
37
+ - lib/potato/irc/events.rb
38
+ - lib/potato/irc/packet.rb
39
+ - lib/potato/irc/server.rb
40
+ - lib/potato/irc/user.rb
41
+ - lib/potato/version.rb
42
+ - potato.gemspec
43
+ has_rdoc: true
44
+ homepage: ''
45
+ licenses: []
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubyforge_project: potato
64
+ rubygems_version: 1.5.2
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: A dAmn <=> IRC proxy.
68
+ test_files: []