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 +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: []
|