tinyirc 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +44 -0
- data/LICENSE.txt +21 -0
- data/README.md +90 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/tinyirc +24 -0
- data/lib/tinyirc/app.rb +77 -0
- data/lib/tinyirc/bot.rb +363 -0
- data/lib/tinyirc/command.rb +109 -0
- data/lib/tinyirc/event.rb +12 -0
- data/lib/tinyirc/ircsocket.rb +231 -0
- data/lib/tinyirc/perms.rb +45 -0
- data/lib/tinyirc/plugin.rb +95 -0
- data/lib/tinyirc/plugins/admin-utils.rb +501 -0
- data/lib/tinyirc/plugins/cookies.rb +77 -0
- data/lib/tinyirc/plugins/core.rb +429 -0
- data/lib/tinyirc/usercache.rb +24 -0
- data/lib/tinyirc/version.rb +3 -0
- data/lib/tinyirc/views/404.erb +4 -0
- data/lib/tinyirc/views/500.erb +5 -0
- data/lib/tinyirc/views/index.erb +31 -0
- data/lib/tinyirc/views/layout.erb +62 -0
- data/lib/tinyirc/views/plugin.erb +42 -0
- data/lib/tinyirc.rb +26 -0
- data/tinyirc.gemspec +32 -0
- data/tinyirc.sample.yaml +40 -0
- metadata +172 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
class TinyIRC::Command
|
2
|
+
class << self
|
3
|
+
attr_reader :log
|
4
|
+
end
|
5
|
+
|
6
|
+
@log = ParticleLog.new('commands', ParticleLog::INFO)
|
7
|
+
|
8
|
+
class Branch
|
9
|
+
attr_reader :cmd, :id, :definition, :handler, :last_uses
|
10
|
+
attr_accessor :cooldown, :description
|
11
|
+
|
12
|
+
def initialize(cmd, id, definition, handler)
|
13
|
+
@cmd = cmd
|
14
|
+
@cooldown = 0
|
15
|
+
@id = id
|
16
|
+
@definition = definition
|
17
|
+
@description = ''
|
18
|
+
@handler = handler
|
19
|
+
@last_uses = {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :plugin, :name, :branches
|
24
|
+
|
25
|
+
def initialize(plugin, name)
|
26
|
+
@plugin = plugin
|
27
|
+
@name = name
|
28
|
+
@branches = {}
|
29
|
+
TinyIRC::Command.log.info "commands += #{@plugin.name}/#{name}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def branch(id, definition, &handler)
|
33
|
+
@branches[id] = Branch.new(self, id, ParticleCMD::Definition.from_string(@name, definition), handler)
|
34
|
+
TinyIRC::Command.log.info "#{@plugin.name}/#{@name} += #{id}: #{@branches[id].definition.command_signature(true)}"
|
35
|
+
@branches[id]
|
36
|
+
end
|
37
|
+
|
38
|
+
def handle_command(h, cmd_info)
|
39
|
+
pname = h[:cmd_info][:plugin]
|
40
|
+
pname = @plugin.name if pname == :any
|
41
|
+
cname = h[:cmd_info][:command]
|
42
|
+
bname = h[:cmd_info][:branch]
|
43
|
+
|
44
|
+
def checkperm(h, fname)
|
45
|
+
h[:socket].has_perm(h[:host], fname)
|
46
|
+
end
|
47
|
+
|
48
|
+
def checkcd(h, branch)
|
49
|
+
return true if h[:socket].has_group(h[:host], 'admin')
|
50
|
+
unless branch.last_uses[h[:socket].name]
|
51
|
+
branch.last_uses[h[:socket].name] = {}
|
52
|
+
return true
|
53
|
+
end
|
54
|
+
current = Time.now.to_i
|
55
|
+
last = branch.last_uses[h[:socket].name][h[:nick]].to_i
|
56
|
+
diff = current - last
|
57
|
+
if diff >= branch.cooldown || diff < 0
|
58
|
+
branch.last_uses[h[:socket].name][h[:nick]] = current
|
59
|
+
true
|
60
|
+
else
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if (bname != :any)
|
66
|
+
if @branches.include? bname
|
67
|
+
branch = @branches[bname]
|
68
|
+
res = branch.definition.match cmd_info
|
69
|
+
if res
|
70
|
+
bname = branch.id
|
71
|
+
fname = pname + '/' + cname + '/' + bname
|
72
|
+
unless checkperm(h, fname)
|
73
|
+
h.nreply "Sorry, but you cannot execute this command (#{fname})"
|
74
|
+
return :denied
|
75
|
+
end
|
76
|
+
unless checkcd(h, branch)
|
77
|
+
h.nreply "Sorry, but you cannot execute this command now (cooldown - #{branch.cooldown}s)"
|
78
|
+
return :cooldown
|
79
|
+
end
|
80
|
+
branch.handler.(h, res)
|
81
|
+
true
|
82
|
+
else
|
83
|
+
false
|
84
|
+
end
|
85
|
+
else
|
86
|
+
false
|
87
|
+
end
|
88
|
+
else
|
89
|
+
@branches.each_pair do |id, branch|
|
90
|
+
res = branch.definition.match cmd_info
|
91
|
+
if res
|
92
|
+
bname = branch.id
|
93
|
+
fname = pname + '/' + cname + '/' + bname
|
94
|
+
unless checkperm(h, fname)
|
95
|
+
h.nreply "Sorry, but you cannot execute this command (#{fname})"
|
96
|
+
return :denied
|
97
|
+
end
|
98
|
+
unless checkcd(h, branch)
|
99
|
+
h.nreply "Sorry, but you cannot execute this command now (cooldown - #{branch.cooldown}s)"
|
100
|
+
return :cooldown
|
101
|
+
end
|
102
|
+
branch.handler.(h, res)
|
103
|
+
return true
|
104
|
+
end
|
105
|
+
end
|
106
|
+
false
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module TinyIRC
|
2
|
+
def self.define_event_methods(h)
|
3
|
+
h.define_singleton_method :reply do |msg|
|
4
|
+
raise RuntimeError, ':reply action is not supported' unless self[:reply_to]
|
5
|
+
self[:socket].privmsg(self[:reply_to], msg)
|
6
|
+
end
|
7
|
+
h.define_singleton_method :nreply do |msg|
|
8
|
+
raise RuntimeError, ':nreply action is not supported' unless self[:nick]
|
9
|
+
self[:socket].notice(self[:nick], msg)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
class TinyIRC::IRCSocket
|
2
|
+
attr_accessor :name, :host, :port, :nick, :user, :pass, :rnam, :autojoin, :prefix
|
3
|
+
attr_accessor :bot, :reconnects, :running, :last_write
|
4
|
+
attr_reader :mtx, :log, :sock, :queue, :usercache
|
5
|
+
|
6
|
+
def initialize(bot, name, opts)
|
7
|
+
@bot = bot
|
8
|
+
@name = name
|
9
|
+
@host = opts['host'] || '127.0.0.1'
|
10
|
+
@port = opts['port'] || 6667
|
11
|
+
@nick = opts['nick'] || 'TinyBot'
|
12
|
+
@user = opts['user'] || @nick
|
13
|
+
@pass = opts['pass'] || nil
|
14
|
+
@rnam = opts['rnam'] || 'An IRC bot in Ruby'
|
15
|
+
@autojoin = opts['autojoin'] || []
|
16
|
+
@prefix = opts['prefix'] || bot.prefix || '!'
|
17
|
+
|
18
|
+
@reconnects = 0
|
19
|
+
@running = false
|
20
|
+
@last_write = Time.now
|
21
|
+
|
22
|
+
@mtx = Mutex.new
|
23
|
+
@log = ParticleLog.new("!#{name}", ParticleLog::INFO)
|
24
|
+
@log.level_table[ParticleLog::IO] = 'IRC'
|
25
|
+
@log.important "Hello, #{name}!"
|
26
|
+
|
27
|
+
@sock = nil
|
28
|
+
@queue = Queue.new
|
29
|
+
@usercache = TinyIRC::UserCache.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def connect(reconnect=false)
|
33
|
+
@log.important 'Connecting...'
|
34
|
+
@mtx.synchronize do
|
35
|
+
begin
|
36
|
+
@sock = TCPSocket.new @host, @port
|
37
|
+
@running = true
|
38
|
+
@bot.handle_event type: :connect, socket: self
|
39
|
+
Thread.new do
|
40
|
+
authenticate
|
41
|
+
end
|
42
|
+
rescue => e
|
43
|
+
@log.error "#{e.class.name} - #{e.message}"
|
44
|
+
@bot.handle_event type: :disconnect, socket: self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def disconnect
|
50
|
+
@mtx.synchronize do
|
51
|
+
@running = false
|
52
|
+
sock.close
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def authenticate
|
57
|
+
direct_write "PASS #{@pass}" if @pass
|
58
|
+
direct_write "NICK #{@nick}"
|
59
|
+
direct_write "USER #{@user} 0 * :#{@rnam}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def direct_write(msg)
|
63
|
+
log.io "W> #{msg}"
|
64
|
+
@sock.write "#{msg}\r\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
def write(msg)
|
68
|
+
@queue.push msg
|
69
|
+
end
|
70
|
+
|
71
|
+
def inspect
|
72
|
+
"#<TinyIRC::IRCSocket @name=#{@name.inspect}>"
|
73
|
+
end
|
74
|
+
|
75
|
+
def has_perm(host, name)
|
76
|
+
if (@bot.db.execute(
|
77
|
+
'SELECT EXISTS(SELECT 1 FROM groupinfo WHERE server=? AND host=? AND name="admin" LIMIT 1)',
|
78
|
+
[@name, host]
|
79
|
+
).flatten[0] == 1)
|
80
|
+
return true
|
81
|
+
end
|
82
|
+
|
83
|
+
groups = @bot.db.execute("SELECT name FROM groupinfo WHERE server=? AND host=?", [@name, host])
|
84
|
+
groups = groups.flatten.to_set
|
85
|
+
groups << 'world'
|
86
|
+
|
87
|
+
def where(gname)
|
88
|
+
s = gname.split('/', 2)
|
89
|
+
if s.length == 1
|
90
|
+
@bot.groups[gname]
|
91
|
+
elsif s.length == 2
|
92
|
+
return nil unless @bot.plugins.include? s[0]
|
93
|
+
@bot.plugins[s[0]].groups[gname]
|
94
|
+
else
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
groups.each do |gname|
|
101
|
+
w = where(gname)
|
102
|
+
next unless w
|
103
|
+
w.perms.each do |perm|
|
104
|
+
if perm == TinyIRC::Permission.new(*TinyIRC::Permission.parse(name))
|
105
|
+
return true
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
false
|
111
|
+
end
|
112
|
+
|
113
|
+
def set_group(host, name)
|
114
|
+
res = @bot.db.execute(
|
115
|
+
'SELECT EXISTS(SELECT 1 FROM groupinfo WHERE server=? AND host=? AND name=? LIMIT 1)',
|
116
|
+
[@name, host, name]
|
117
|
+
).flatten[0]
|
118
|
+
if res == 0
|
119
|
+
@bot.db.execute('INSERT INTO groupinfo (server, host, name) VALUES (?, ?, ?)', [@name, host, name])
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def del_group(host, name)
|
124
|
+
@bot.db.execute('DELETE FROM groupinfo WHERE server=? AND host=? AND name=?', [@name, host, name])
|
125
|
+
end
|
126
|
+
|
127
|
+
def has_group(host, name)
|
128
|
+
if (@bot.db.execute(
|
129
|
+
'SELECT EXISTS(SELECT 1 FROM groupinfo WHERE server=? AND host=? AND name="admin" LIMIT 1)',
|
130
|
+
[@name, host]
|
131
|
+
).flatten[0] == 0) then
|
132
|
+
@bot.db.execute(
|
133
|
+
'SELECT EXISTS(SELECT 1 FROM groupinfo WHERE server=? AND host=? AND name=? LIMIT 1)',
|
134
|
+
[@name, host, name]
|
135
|
+
).flatten[0] == 1
|
136
|
+
else
|
137
|
+
true
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def list_groups(host)
|
142
|
+
@bot.db.execute(
|
143
|
+
'SELECT name FROM groupinfo WHERE server=? AND host=?',
|
144
|
+
[@name, host]
|
145
|
+
).flatten
|
146
|
+
end
|
147
|
+
|
148
|
+
def join(chan, key = nil)
|
149
|
+
if key
|
150
|
+
write "JOIN #{chan} #{key}"
|
151
|
+
else
|
152
|
+
write "JOIN #{chan}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def part(chan, reason = 'Bye')
|
157
|
+
write "PART #{chan} :#{reason}"
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.ssplit(string)
|
161
|
+
out = []
|
162
|
+
arr = string.split("\n\r")
|
163
|
+
arr.each do |i|
|
164
|
+
items = i.scan(/.{,399}/)
|
165
|
+
items.delete('')
|
166
|
+
items.each do |i2|
|
167
|
+
out << i2
|
168
|
+
end
|
169
|
+
end
|
170
|
+
out
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.process_colors(string)
|
174
|
+
string
|
175
|
+
.gsub("%C%", "%C?")
|
176
|
+
.gsub(",%", ",?")
|
177
|
+
.gsub("%C", "\x03")
|
178
|
+
.gsub("%B", "\x02")
|
179
|
+
.gsub("%I", "\x10")
|
180
|
+
.gsub("%U", "\x1F")
|
181
|
+
.gsub("%N", "\x0F")
|
182
|
+
.gsub("?WHITE", "0")
|
183
|
+
.gsub("?BLACK", "1")
|
184
|
+
.gsub("?BLUE", "2")
|
185
|
+
.gsub("?GREEN", "3")
|
186
|
+
.gsub("?RED", "4")
|
187
|
+
.gsub("?BROWN", "5")
|
188
|
+
.gsub("?PURPLE", "6")
|
189
|
+
.gsub("?ORANGE", "7")
|
190
|
+
.gsub("?YELLOW", "8")
|
191
|
+
.gsub("?LGREEN", "9")
|
192
|
+
.gsub("?CYAN" , "10")
|
193
|
+
.gsub("?LCYAN", "11")
|
194
|
+
.gsub("?LBLUE", "12")
|
195
|
+
.gsub("?PINK", "13")
|
196
|
+
.gsub("?GREY", "14")
|
197
|
+
.gsub("?LGREY", "15")
|
198
|
+
end
|
199
|
+
|
200
|
+
def privmsg(target, message)
|
201
|
+
self.class.ssplit(self.class.process_colors(message)).each do |m|
|
202
|
+
write("PRIVMSG #{target} :\u200B#{m}")
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def notice(target, message)
|
207
|
+
self.class.ssplit(self.class.process_colors(message)).each do |m|
|
208
|
+
write("NOTICE #{target} :\u200B#{m}")
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def ctcp(target, message)
|
213
|
+
write("PRIVMSG #{target} :\x01#{self.class.process_colors(message)}\x01")
|
214
|
+
end
|
215
|
+
|
216
|
+
def nctcp(target, message)
|
217
|
+
write("NOTICE #{target} :\x01#{self.class.process_colors(message)}\x01")
|
218
|
+
end
|
219
|
+
|
220
|
+
def mode(channel, params)
|
221
|
+
write("MODE #{channel} #{params}")
|
222
|
+
end
|
223
|
+
|
224
|
+
def kick(channel, target, reason = 'Bye!')
|
225
|
+
write("KICK #{channel} #{target} :#{reason}")
|
226
|
+
end
|
227
|
+
|
228
|
+
def remove(channel, target, reason = 'Bye!')
|
229
|
+
write("REMOVE #{channel} #{target} :#{reason}")
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class TinyIRC::Permission
|
2
|
+
attr_reader :plugin, :command, :branch
|
3
|
+
|
4
|
+
def self.parse(pstr)
|
5
|
+
a = pstr.split('/', 3)
|
6
|
+
raise RuntimeError, "Invalid permission: #{pstr}" unless a[0] || !a[1] && a[2]
|
7
|
+
[a[0], a[1] || :all, a[2] || :all]
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(plugin, command, branch)
|
11
|
+
@plugin = plugin
|
12
|
+
@command = command
|
13
|
+
@branch = branch
|
14
|
+
end
|
15
|
+
|
16
|
+
def ==(other)
|
17
|
+
@plugin == other.plugin && @command == other.command && @branch == other.branch
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
"#{@plugin}/#{@command}/#{@branch}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class TinyIRC::Group
|
26
|
+
class << self
|
27
|
+
attr_reader :log
|
28
|
+
end
|
29
|
+
|
30
|
+
@log = ParticleLog.new('groups', ParticleLog::INFO)
|
31
|
+
|
32
|
+
attr_reader :name, :perms
|
33
|
+
|
34
|
+
def initialize(name)
|
35
|
+
@name = name
|
36
|
+
@perms = Set.new
|
37
|
+
TinyIRC::Group::log.write "groups += #{name}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def perm(plugin, command, branch)
|
41
|
+
if @perms.add?(TinyIRC::Permission.new(plugin, command, branch))
|
42
|
+
TinyIRC::Group::log.write "#{@name} += #{plugin}/#{command}/#{branch}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
class TinyIRC::Plugin
|
2
|
+
attr_reader :commands, :event_handlers, :fullname, :groups, :loaded, :log, :name
|
3
|
+
|
4
|
+
def initialize(bot, name)
|
5
|
+
@loaded = false
|
6
|
+
|
7
|
+
@l_init = lambda {}
|
8
|
+
@l_postinit = lambda {}
|
9
|
+
@l_config = lambda {|cfg|}
|
10
|
+
|
11
|
+
@bot = bot
|
12
|
+
@fullname = name
|
13
|
+
@name = File.basename(name)
|
14
|
+
|
15
|
+
@commands = {}
|
16
|
+
@event_handlers = {}
|
17
|
+
@groups = {}
|
18
|
+
|
19
|
+
@log = ParticleLog.new('?' + @name, ParticleLog::INFO)
|
20
|
+
@log.info 'Loading...'
|
21
|
+
|
22
|
+
lp = "./#{name}.rb"
|
23
|
+
ok = false
|
24
|
+
if File.exists? lp
|
25
|
+
instance_eval(File.read(lp), lp)
|
26
|
+
ok = true
|
27
|
+
else
|
28
|
+
$LOAD_PATH.each do |f|
|
29
|
+
lp = File.join(f, name + '.rb')
|
30
|
+
if File.exists? lp
|
31
|
+
instance_eval(File.read(lp), lp)
|
32
|
+
ok = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
raise RuntimeError, "Cannot find the `#{name}` plugin" unless ok
|
38
|
+
|
39
|
+
_l_init
|
40
|
+
end
|
41
|
+
|
42
|
+
def init(&b) @l_init = b end
|
43
|
+
def _l_init()
|
44
|
+
@log.info 'Initializing...'
|
45
|
+
@l_init.()
|
46
|
+
end
|
47
|
+
|
48
|
+
def postinit(&b) @l_postinit = b end
|
49
|
+
def _l_postinit()
|
50
|
+
@log.info 'Post-Initializing...'
|
51
|
+
@l_postinit.()
|
52
|
+
@loaded = true
|
53
|
+
@log.important "Hello, bot!"
|
54
|
+
end
|
55
|
+
|
56
|
+
def configure(&b) @l_config = b end
|
57
|
+
def _l_config(cfg)
|
58
|
+
@log.info 'Configuring...'
|
59
|
+
@l_config.(cfg)
|
60
|
+
end
|
61
|
+
|
62
|
+
def on(type, pattern, &block)
|
63
|
+
pattern[:type] = type
|
64
|
+
pattern[:_block] = block
|
65
|
+
@event_handlers[type] = pattern
|
66
|
+
end
|
67
|
+
|
68
|
+
def on(type, **pattern, &handler)
|
69
|
+
@event_handlers[type] ||= []
|
70
|
+
@event_handlers[type] << {
|
71
|
+
pattern: pattern,
|
72
|
+
handler: handler
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def cmd(name)
|
77
|
+
@commands[name] = TinyIRC::Command.new(self, name)
|
78
|
+
@commands[name]
|
79
|
+
end
|
80
|
+
|
81
|
+
def handle_command(h, cmd_info)
|
82
|
+
return false if h[:cmd_info][:plugin] != @name && h[:cmd_info][:plugin] != :any
|
83
|
+
x = if @commands.include? h[:cmd_info][:command]
|
84
|
+
@commands[h[:cmd_info][:command]].handle_command(h, cmd_info)
|
85
|
+
else
|
86
|
+
false
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def group(name)
|
91
|
+
n = "#{@name}/#{name}"
|
92
|
+
@groups[n] = TinyIRC::Group.new n
|
93
|
+
@groups[n]
|
94
|
+
end
|
95
|
+
end
|