potato 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []