protonbot 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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rubocop.yml +38 -0
  4. data/CODE_OF_CONDUCT.md +74 -0
  5. data/CONFIG.md +45 -0
  6. data/CORE_PLUGIN.md +128 -0
  7. data/Gemfile +7 -0
  8. data/LICENSE.txt +21 -0
  9. data/PLUGIN.md +45 -0
  10. data/README.md +81 -0
  11. data/Rakefile +2 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/exe/protonbot-gencert +7 -0
  15. data/lib/protonbot/bot.rb +64 -0
  16. data/lib/protonbot/bot_conf.rb +48 -0
  17. data/lib/protonbot/bot_plugins.rb +110 -0
  18. data/lib/protonbot/core_plugin/apis/help.rb +54 -0
  19. data/lib/protonbot/core_plugin/apis/index.rb +2 -0
  20. data/lib/protonbot/core_plugin/apis/perms.rb +118 -0
  21. data/lib/protonbot/core_plugin/codes/index.rb +3 -0
  22. data/lib/protonbot/core_plugin/codes/names.rb +21 -0
  23. data/lib/protonbot/core_plugin/codes/welcome.rb +7 -0
  24. data/lib/protonbot/core_plugin/codes/whois.rb +9 -0
  25. data/lib/protonbot/core_plugin/commands/admin.rb +107 -0
  26. data/lib/protonbot/core_plugin/commands/basic.rb +9 -0
  27. data/lib/protonbot/core_plugin/commands/cross.rb +40 -0
  28. data/lib/protonbot/core_plugin/commands/help.rb +25 -0
  29. data/lib/protonbot/core_plugin/commands/index.rb +6 -0
  30. data/lib/protonbot/core_plugin/commands/joinpart.rb +25 -0
  31. data/lib/protonbot/core_plugin/commands/perms.rb +50 -0
  32. data/lib/protonbot/core_plugin/hooks/ctcp.rb +9 -0
  33. data/lib/protonbot/core_plugin/hooks/index.rb +7 -0
  34. data/lib/protonbot/core_plugin/hooks/joinpart.rb +42 -0
  35. data/lib/protonbot/core_plugin/hooks/kick.rb +17 -0
  36. data/lib/protonbot/core_plugin/hooks/nick.rb +10 -0
  37. data/lib/protonbot/core_plugin/hooks/ping.rb +3 -0
  38. data/lib/protonbot/core_plugin/hooks/privmsg.rb +62 -0
  39. data/lib/protonbot/core_plugin/hooks/raw.rb +32 -0
  40. data/lib/protonbot/core_plugin/plugin.rb +10 -0
  41. data/lib/protonbot/event_lock.rb +25 -0
  42. data/lib/protonbot/hook.rb +22 -0
  43. data/lib/protonbot/log.rb +182 -0
  44. data/lib/protonbot/log_wrapper.rb +52 -0
  45. data/lib/protonbot/numeric.rb +140 -0
  46. data/lib/protonbot/plug.rb +118 -0
  47. data/lib/protonbot/plug_events.rb +51 -0
  48. data/lib/protonbot/plug_io.rb +99 -0
  49. data/lib/protonbot/plug_utils.rb +242 -0
  50. data/lib/protonbot/plugin.rb +101 -0
  51. data/lib/protonbot/version.rb +4 -0
  52. data/lib/protonbot.rb +38 -0
  53. data/protonbot.gemspec +31 -0
  54. metadata +181 -0
@@ -0,0 +1,51 @@
1
+ class ProtonBot::Plug
2
+ # Emits passed event - calls first matching hook from each plugin
3
+ # @param dat [Hash] Event hash
4
+ # @return [Plug] self
5
+ def emit(dat = {})
6
+ bot.plugins.each do |_, p|
7
+ worked = []
8
+ p.hooks.each do |h|
9
+ next unless dat >= h.pattern && !worked.include?(h.object_id) && worked.empty?
10
+ canrun = true
11
+ h.chain.each do |l|
12
+ next unless canrun
13
+ canrun = l.call(dat, h)
14
+ end
15
+ worked << h.object_id
16
+ h.block.call(dat) if canrun
17
+ end
18
+ worked = []
19
+ end
20
+ event_locks.each_with_index do |el, k|
21
+ if dat >= el.pattern
22
+ event_locks.delete_at(k)
23
+ el.unlock
24
+ end
25
+ end
26
+ self
27
+ end
28
+
29
+ # Emits passed event in new thread
30
+ # @param dat [Hash] Event hash
31
+ # @return [Plug] self
32
+ def emitt(dat = {})
33
+ d = dat.clone
34
+ Thread.new do
35
+ begin
36
+ emit(d)
37
+ rescue => e
38
+ log_err(e)
39
+ end
40
+ end
41
+ self
42
+ end
43
+
44
+ # Creates EventLock with given pattern.
45
+ # @param pattern [Hash]
46
+ # @return [Plug] self
47
+ def wait_for(pattern)
48
+ ProtonBot::EventLock.new(self, pattern)
49
+ self
50
+ end
51
+ end
@@ -0,0 +1,99 @@
1
+ class ProtonBot::Plug
2
+ # Main read-loop. Reads data from socket and emits event `raw`
3
+ def readloop
4
+ while @running
5
+ if s = @sock.gets
6
+ s = s.force_encoding(@conf['encoding'])
7
+ s = s[0..-3]
8
+ log.info "R > #{s}"
9
+ begin
10
+ emit(type: :raw, raw_data: s.clone, plug: self, db: db, bot: bot, core: bot.plugins['core'])
11
+ rescue => e
12
+ log_err(e)
13
+ end
14
+ else
15
+ @running = false
16
+ end
17
+ end
18
+ log.info 'Connection closed.'
19
+ end
20
+
21
+ # Main write-loop. Reads data from queue and sends it to server
22
+ def writeloop
23
+ while @running || !@wqueue.empty?
24
+ s = @queue.pop
25
+ write_(s)
26
+ sleep(@conf['queue_delay'])
27
+ end
28
+ end
29
+
30
+ # Adds given message to the queue
31
+ # @param s [String] Message
32
+ # @return [Plug] self
33
+ def write(s)
34
+ @queue << s
35
+ self
36
+ end
37
+
38
+ # Sends message to server without using queue.
39
+ # @param s [String] Message
40
+ # @return [Plug] self
41
+ def write_(s)
42
+ s = s.encode(@conf['encoding'], s.encoding)
43
+ @sock.puts s
44
+ log.info "W > #{s}"
45
+ end
46
+
47
+ # Sends credentials to server (PASS, NICK, USER).
48
+ def introduce
49
+ write_("PASS #{@conf['pass']}") if @conf['pass']
50
+ write_("NICK #{@conf['nick']}")
51
+ write_("USER #{@conf['user']} 0 * :#{@conf['rnam']}")
52
+ end
53
+
54
+ # Splits given string each 399 bytes and on newlines
55
+ # @param string [String]
56
+ # @return [String] Output
57
+ def self.ssplit(string)
58
+ out = []
59
+ arr = string.split("\n\r")
60
+ arr.each do |i|
61
+ items = i.scan(/.{,399}/)
62
+ items.delete('')
63
+ items.each do |i2|
64
+ out << i2
65
+ end
66
+ end
67
+ out
68
+ end
69
+
70
+ # Colorizes given string.
71
+ # @param string [String]
72
+ # @return [String] Output
73
+ def self.process_colors(string)
74
+ string
75
+ .gsub("%C%", "%C?")
76
+ .gsub(",%", ",?")
77
+ .gsub("%C", "\x03")
78
+ .gsub("%B", "\x02")
79
+ .gsub("%I", "\x10")
80
+ .gsub("%U", "\x1F")
81
+ .gsub("%N", "\x0F")
82
+ .gsub("?WHITE", "0")
83
+ .gsub("?BLACK", "1")
84
+ .gsub("?BLUE", "2")
85
+ .gsub("?GREEN", "3")
86
+ .gsub("?RED", "4")
87
+ .gsub("?BROWN", "5")
88
+ .gsub("?PURPLE", "6")
89
+ .gsub("?ORANGE", "7")
90
+ .gsub("?YELLOW", "8")
91
+ .gsub("?LGREEN", "9")
92
+ .gsub("?CYAN" , "10")
93
+ .gsub("?LCYAN", "11")
94
+ .gsub("?LBLUE", "12")
95
+ .gsub("?PINK", "13")
96
+ .gsub("?GREY", "14")
97
+ .gsub("?LGREY", "15")
98
+ end
99
+ end
@@ -0,0 +1,242 @@
1
+ class ProtonBot::Plug
2
+ # Sends WHOIS message to the server
3
+ # @param nick [String]
4
+ # @return [Plug] self
5
+ def whois(nick)
6
+ write("WHOIS #{nick}")
7
+ self
8
+ end
9
+
10
+ # Gets hostname for given nickname using WHOIS and event lock
11
+ # @param nick [String]
12
+ # @return [String] host
13
+ def gethost(nick)
14
+ if @users[nick] and @users[nick][:host]
15
+ @users[nick][:host]
16
+ else
17
+ Thread.new do
18
+ sleep(1)
19
+ whois(nick)
20
+ end
21
+ wait_for(type: :code_whoisuser, nick: nick)
22
+ end
23
+ @users[nick][:host]
24
+ end
25
+
26
+ # Gets username for given nickname using WHOIS and event lock
27
+ # @param nick [String]
28
+ # @return [String] user
29
+ def getuser(nick)
30
+ if @users[nick] and @users[nick][:user]
31
+ @users[nick][:user]
32
+ else
33
+ Thread.new do
34
+ sleep(1)
35
+ whois(nick)
36
+ end
37
+ wait_for(type: :code, code: ProtonBot::Numeric::WHOISUSER)
38
+ @users[nick][:user]
39
+ end
40
+ end
41
+
42
+ # Changes current nick to given. If successful, `unick` event will be emitted
43
+ # @param to [String]
44
+ # @return [Plug] self
45
+ def change_nick(to)
46
+ write("NICK #{to}")
47
+ self
48
+ end
49
+
50
+ # Joins given channel and uses password if it's provided
51
+ # @param chan [String]
52
+ # @param pass [String] Password
53
+ # @return [Plug] self
54
+ def join(chan, pass = '')
55
+ write("JOIN #{chan} #{pass}")
56
+ self
57
+ end
58
+
59
+ # Parts given channel with given reason
60
+ # @param chan [String]
61
+ # @param reason [String]
62
+ def part(chan, reason = 'Parting...')
63
+ write("PART #{chan} :#{reason}")
64
+ self
65
+ end
66
+
67
+ # Sends message to given target. Splits it each 399 bytes.
68
+ # @param target [String]
69
+ # @param msg [String]
70
+ # @return [Plug] self
71
+ def privmsg(target, msg)
72
+ self.class.ssplit(self.class.process_colors(msg)).each do |m|
73
+ write("PRIVMSG #{target} :\u200B#{m}")
74
+ end
75
+ self
76
+ end
77
+
78
+ # Sends notice to given target. Splits it each 399 bytes.
79
+ # @param target [String]
80
+ # @param msg [String]
81
+ # @return [Plug] self
82
+ def notice(target, msg)
83
+ self.class.ssplit(self.class.process_colors(msg)).each do |m|
84
+ write("NOTICE #{target} :\u200B#{m}")
85
+ end
86
+ self
87
+ end
88
+
89
+ # Sends CTCP to given target.
90
+ # @param target [String]
91
+ # @param msg [String]
92
+ # @return [Plug] self
93
+ def ctcp(target, msg)
94
+ write("PRIVMSG #{target} :\x01#{msg}\x01")
95
+ self
96
+ end
97
+
98
+ # Sends NCTCP to given target.
99
+ # @param target [String]
100
+ # @param msg [String]
101
+ # @return [Plug] self
102
+ def nctcp(target, msg)
103
+ write("NOTICE #{target} :\x01#{msg}\x01")
104
+ self
105
+ end
106
+
107
+ # Invites user to given channel
108
+ # @param chan [String]
109
+ # @param nick [String]
110
+ # @return [Plug] self
111
+ def invite(chan, nick)
112
+ write("INVITE #{nick} #{chan}")
113
+ self
114
+ end
115
+
116
+ # Sets mode on given user at given channel
117
+ # @param chan [String]
118
+ # @param nick [String]
119
+ # @param mode [String]
120
+ # @return [Plug] self
121
+ def usermode(chan, nick, mode)
122
+ write("MODE #{chan} #{mode} #{nick}")
123
+ self
124
+ end
125
+
126
+ # Sets mode on given channel
127
+ # @param chan [String]
128
+ # @param mode [String]
129
+ # @return [Plug] self
130
+ def chanmode(chan, mode)
131
+ write("MODE #{chan} #{mode}")
132
+ self
133
+ end
134
+
135
+ # Sets quiet on given user at given channel
136
+ # @param chan [String]
137
+ # @param nick [String]
138
+ # @return [Plug] self
139
+ def quiet(chan, nick)
140
+ usermode(chan, nick, '+q')
141
+ self
142
+ end
143
+
144
+ # Removes quiet on given user at given channel
145
+ # @param chan [String]
146
+ # @param nick [String]
147
+ # @return [Plug] self
148
+ def unquiet(chan, nick)
149
+ usermode(chan, nick, '-q')
150
+ self
151
+ end
152
+
153
+ # Sets ban on given user at given channel
154
+ # @param chan [String]
155
+ # @param nick [String]
156
+ # @return [Plug] self
157
+ def ban(chan, nick)
158
+ usermode(chan, nick, '+b')
159
+ self
160
+ end
161
+
162
+ # Removes ban on given user at given channel
163
+ # @param chan [String]
164
+ # @param nick [String]
165
+ # @return [Plug] self
166
+ def unban(chan, nick)
167
+ usermode(chan, nick, '-b')
168
+ self
169
+ end
170
+
171
+ # Sets excempt on given user at given channel
172
+ # @param chan [String]
173
+ # @param nick [String]
174
+ # @return [Plug] self
175
+ def excempt(chan, nick)
176
+ usermode(chan, nick, '+e')
177
+ self
178
+ end
179
+
180
+ # Removes excempt on given user at given channel
181
+ # @param chan [String]
182
+ # @param nick [String]
183
+ # @return [Plug] self
184
+ def unexcempt(chan, nick)
185
+ usermode(chan, nick, '-e')
186
+ self
187
+ end
188
+
189
+ # Ops given user at given channel
190
+ # @param chan [String]
191
+ # @param nick [String]
192
+ # @return [Plug] self
193
+ def op(chan, nick)
194
+ usermode(chan, nick, '+o')
195
+ self
196
+ end
197
+
198
+ # Deops given user at given channel
199
+ # @param chan [String]
200
+ # @param nick [String]
201
+ # @return [Plug] self
202
+ def deop(chan, nick)
203
+ usermode(chan, nick, '-o')
204
+ self
205
+ end
206
+
207
+ # Voices given user at given channel
208
+ # @param chan [String]
209
+ # @param nick [String]
210
+ # @return [Plug] self
211
+ def voice(chan, nick)
212
+ usermode(chan, nick, '+v')
213
+ self
214
+ end
215
+
216
+ # Devoices on given user at given channel
217
+ # @param chan [String]
218
+ # @param nick [String]
219
+ # @return [Plug] self
220
+ def devoice(chan, nick)
221
+ usermode(chan, nick, '-v')
222
+ self
223
+ end
224
+
225
+ # Kicks given user from given channel
226
+ # @param chan [String]
227
+ # @param nick [String]
228
+ # @return [Plug] self
229
+ def kick(chan, nick, reason = 'Default Kick Reason')
230
+ write("KICK #{chan} #{nick} :#{reason}")
231
+ self
232
+ end
233
+
234
+ # Removes given user from given channel
235
+ # @param chan [String]
236
+ # @param nick [String]
237
+ # @return [Plug] self
238
+ def remove(chan, nick, reason = 'Default Remove Reason')
239
+ write("REMOVE #{chan} #{nick} :#{reason}")
240
+ self
241
+ end
242
+ end
@@ -0,0 +1,101 @@
1
+ # Plugin object. Use it to alter bot's behavior
2
+ # @see http://www.rubydoc.info/gems/protonbot/file/plugin-demo.md
3
+ # @!attribute [r] name
4
+ # @return [String] Plugin's name
5
+ # @!attribute [r] version
6
+ # @return [String] Plugin's version
7
+ # @!attribute [r] description
8
+ # @return [String] Plugin's description
9
+ # @!attribute [r] name
10
+ # @return [String] Plugin's name
11
+ # @!attribute [r] hooks
12
+ # @return [Array<Hook>] Plugin's hooks
13
+ # @!attribute [rw] bot
14
+ # @return [Bot] Bot
15
+ # @!attribute [rw] Log
16
+ # @return [LogWrapper] Log
17
+ # @!attribute [rw] path
18
+ # @return [String] Path
19
+ # @!attribute [r] core
20
+ # @return [Plugin] Core plugin
21
+ class ProtonBot::Plugin
22
+ attr_reader :name, :version, :description, :plugins, :hooks, :core
23
+ attr_accessor :bot, :log, :path
24
+
25
+ # @param block [Proc]
26
+ def initialize(&block)
27
+ @hooks = []
28
+ @block = block
29
+ @numeric = ProtonBot::Numeric
30
+ end
31
+
32
+ # Starts plugin
33
+ # @return [Plugin] self
34
+ def launch
35
+ @plugins = @bot.plugins
36
+ @core = @bot.plugins['core']
37
+ instance_exec(&@block)
38
+ raise ProtonBot::PluginError, 'Plugin-name is not set!' unless
39
+ @name
40
+ raise ProtonBot::PluginError, 'Plugin-description is not set!' unless
41
+ @description
42
+ raise ProtonBot::PluginError, 'Plugin-description is not set!' unless
43
+ @description
44
+ log.info("Started plugin `#{@name} v#{@version}` successfully!")
45
+ end
46
+
47
+ # Creates hook and returns it
48
+ # @param pattern [Hash<Symbol>]
49
+ # @param block [Proc]
50
+ # @return [Hook] hook
51
+ def hook(pattern = {}, &block)
52
+ h = ProtonBot::Hook.new(pattern, &block)
53
+ @hooks << h
54
+ h
55
+ end
56
+
57
+ # Shortcut for `hook(type: :command, ...) {}`
58
+ # @param pattern [Hash<Symbol>]
59
+ # @param block [Proc]
60
+ # @return [Hook] hook
61
+ def cmd(pattern = {}, &block)
62
+ hook(pattern.merge(type: :command), &block)
63
+ end
64
+
65
+ # Emits passed event
66
+ # @param dat [Hash<Symbol>]
67
+ def emit(dat = {})
68
+ dat[:plug].emit(dat) if dat[:plug]
69
+ end
70
+
71
+ # Emits passed event in other thread
72
+ # @param dat [Hash<Symbol>]
73
+ def emitt(dat = {})
74
+ dat[:plug].emitt(dat) if dat[:plug]
75
+ end
76
+
77
+ # Runs given file (.rb is appended automatically)
78
+ # @param fname [String]
79
+ def run(fname)
80
+ if @name == 'Core'
81
+ instance_eval File.read("#{Gem.loaded_specs['protonbot'].lib_dirs_glob}/" \
82
+ "protonbot/core_plugin/#{fname}.rb"), "#{name}/#{fname}.rb"
83
+ else
84
+ instance_exec do
85
+ instance_eval File.read(File.expand_path("#{path}/#{fname}.rb")), "#{name}/#{fname}.rb"
86
+ end
87
+ end
88
+ end
89
+
90
+ # Shortcut for `define_singleton_method(...)`
91
+ # @param name [String,Symbol]
92
+ # @param block [Proc]
93
+ def fun(name, &block)
94
+ define_singleton_method(name, &block)
95
+ end
96
+
97
+ # @return [String] Output
98
+ def inspect
99
+ %(<#ProtonBot::Plugin:#{object_id.to_s(16)} @name=#{name} @version=#{version}>)
100
+ end
101
+ end
@@ -0,0 +1,4 @@
1
+ module ProtonBot
2
+ # ProtonBot's version
3
+ VERSION = '0.1.0'.freeze
4
+ end
data/lib/protonbot.rb ADDED
@@ -0,0 +1,38 @@
1
+ require 'English'
2
+
3
+ require 'date'
4
+
5
+ require 'pastel'
6
+ require 'tty'
7
+ require 'irb'
8
+
9
+ require 'heliodor'
10
+
11
+ require 'open3'
12
+
13
+ require 'socket'
14
+ require 'openssl'
15
+
16
+ module ProtonBot
17
+ end
18
+
19
+ require 'protonbot/version'
20
+
21
+ require 'protonbot/numeric'
22
+
23
+ require 'protonbot/log'
24
+ require 'protonbot/log_wrapper'
25
+
26
+ require 'protonbot/bot'
27
+ require 'protonbot/bot_conf'
28
+ require 'protonbot/bot_plugins'
29
+
30
+ require 'protonbot/event_lock'
31
+
32
+ require 'protonbot/plug'
33
+ require 'protonbot/plug_io'
34
+ require 'protonbot/plug_utils'
35
+ require 'protonbot/plug_events'
36
+
37
+ require 'protonbot/plugin'
38
+ require 'protonbot/hook'
data/protonbot.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'protonbot/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'protonbot'
8
+ spec.version = ProtonBot::VERSION
9
+ spec.authors = ['Nickolay']
10
+ spec.email = ['nickolay02@inbox.ru']
11
+
12
+ spec.summary = 'An IRC bot library'
13
+ spec.description = 'Library for writing cross-server IRC bots'
14
+ spec.homepage = 'https://github.com/handicraftsman/ProtonBot'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
18
+ f.match(%r{^(test|spec|features)/})
19
+ end
20
+ spec.bindir = 'exe'
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.require_paths = ['lib']
23
+
24
+ spec.add_development_dependency 'bundler', '~> 1.13'
25
+ spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'rubocop', '~> 0.47'
27
+
28
+ spec.add_runtime_dependency 'pastel', '~> 0.7.1'
29
+ spec.add_runtime_dependency 'tty', '~> 0.6.1'
30
+ spec.add_runtime_dependency 'heliodor', '~> 0.2.0'
31
+ end