potato 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.yardopts +1 -0
- data/README.md +18 -0
- data/Rakefile +2 -0
- data/bin/potato +4 -0
- data/lib/potato.rb +39 -0
- data/lib/potato/damn/client.rb +139 -0
- data/lib/potato/damn/events.rb +82 -0
- data/lib/potato/damn/packet.rb +112 -0
- data/lib/potato/damn/token.rb +53 -0
- data/lib/potato/helpers/string.rb +28 -0
- data/lib/potato/irc/client.rb +370 -0
- data/lib/potato/irc/events.rb +138 -0
- data/lib/potato/irc/packet.rb +41 -0
- data/lib/potato/irc/server.rb +115 -0
- data/lib/potato/irc/user.rb +10 -0
- data/lib/potato/version.rb +4 -0
- data/potato.gemspec +21 -0
- metadata +68 -0
data/.gitignore
ADDED
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
data/bin/potato
ADDED
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
|
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: []
|