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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f53a83ec7e535488a81897bb2d3d5cfa50aa0f61ecc975e102602be4d8dfe1ff
4
+ data.tar.gz: 631e9d17e0f40eedfb0613814f732ca80b9244c6432a739a59764d35a0bc3006
5
+ SHA512:
6
+ metadata.gz: ccc01076e7c3751b070b231adb11fb1adcd0cc2715e503617c7eb058cebd8c6233daf4149b46e69e6dbcc9ba5614e50330e726278a630b902099781bb88dc88c
7
+ data.tar.gz: 55ded6b6debcdaa5b29763a01ad72ee4ba3e7703525a874e8a70061b706ece5a8a6ea757ef934925fd162c698da6a5bc4e13a7e6064c0b3a5c802fd3758e4c18
data/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /tinyirc.yaml
10
+ /tinyirc.db
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in tinyirc.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,44 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ tinyirc (0.1.0)
5
+ particlecmd (~> 0.1)
6
+ particlelog (~> 0.1)
7
+ sinatra (~> 2.0)
8
+ sqlite3 (~> 1.3)
9
+ thin (~> 1.2)
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ daemons (1.2.6)
15
+ eventmachine (1.2.5)
16
+ mustermann (1.0.2)
17
+ particlecmd (0.1.4)
18
+ particlelog (0.1.1)
19
+ rack (2.0.4)
20
+ rack-protection (2.0.1)
21
+ rack
22
+ rake (10.5.0)
23
+ sinatra (2.0.1)
24
+ mustermann (~> 1.0)
25
+ rack (~> 2.0)
26
+ rack-protection (= 2.0.1)
27
+ tilt (~> 2.0)
28
+ sqlite3 (1.3.13)
29
+ thin (1.7.2)
30
+ daemons (~> 1.0, >= 1.0.9)
31
+ eventmachine (~> 1.0, >= 1.0.4)
32
+ rack (>= 1, < 3)
33
+ tilt (2.0.8)
34
+
35
+ PLATFORMS
36
+ ruby
37
+
38
+ DEPENDENCIES
39
+ bundler (~> 1.16)
40
+ rake (~> 10.0)
41
+ tinyirc!
42
+
43
+ BUNDLED WITH
44
+ 1.16.1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Nickolay Ilyushin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # Tinyirc
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'particlecmd'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ ```bash
14
+ $ bundle
15
+ ```
16
+
17
+ Or install it yourself as:
18
+
19
+ ```bash
20
+ $ gem install particlecmd
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ Sample config file:
26
+
27
+ ```yaml
28
+ plugins:
29
+ path/to/plugin: # The same path as in the `require` function
30
+ # This table can be empty if you want to load the plugin without configuring it
31
+ # Plugins may require some values from this table though
32
+ # You can always load plugins from installed gems (if you are using bundler)
33
+ foo: bar
34
+
35
+ groups:
36
+ world: # All users belong to this group
37
+ include: # Includes permissions from listed groups into the current one
38
+ - plugin/world
39
+ perms:
40
+ - core/flushq
41
+ admin: # Admins have this group
42
+ include:
43
+ - world
44
+ - plugin/admin
45
+ owner: # Owners have this group
46
+ include:
47
+ - admin
48
+ - plugin/owner
49
+
50
+ cooldowns:
51
+ plugin: 30 # Sets cooldown for plugin/*/* commands to 30 seconds
52
+ plugin/command: 20 # Sets cooldown for plugin/command/* commands to 20 seconds
53
+ plugin/command/branch: 10 # Sets cooldown for plugin/command/branch command to 10 seconds
54
+
55
+ servers:
56
+ freenode:
57
+ host: irc.freenode.net
58
+ port: 6667
59
+
60
+ nick: YourNicknameHere
61
+ user: YourUsernameHere
62
+ pass: YourPasswordHere
63
+ rnam: YourRealnameHere
64
+
65
+ prefix: '@'
66
+
67
+ autojoin:
68
+ - '#botters-test'
69
+ ```
70
+
71
+ To start the bot, simply execute:
72
+
73
+ ```bash
74
+ $ bundler exec tinyirc -c /path/to/config.yaml -d /path/to/db.db # both options are optional
75
+ ```
76
+
77
+ ## Development
78
+
79
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
80
+
81
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
82
+
83
+ ## Contributing
84
+
85
+ Bug reports and pull requests are welcome on GitHub at https://github.com/handicraftsman/tinyirc.
86
+
87
+ ## License
88
+
89
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
90
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "tinyirc"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/tinyirc ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'tinyirc'
4
+ require 'optparse'
5
+
6
+ options = {
7
+ :cfg => './tinyirc.yaml',
8
+ :db => './tinyirc.db'
9
+ }
10
+
11
+ OptionParser.new do |o|
12
+ o.banner = 'Usage: tinyirc [OPTIONS]'
13
+
14
+ o.on '-cFILE', '--config=FILE', 'Path to the config list' do |p|
15
+ options[:cfg] = p
16
+ end
17
+
18
+ o.on '-dFILE', '--db=FILE', 'Path to the database file' do |p|
19
+ options[:db] = p
20
+ end
21
+ end.parse!
22
+
23
+ bot = TinyIRC::Bot.new(options[:cfg], options[:db])
24
+ bot.start
@@ -0,0 +1,77 @@
1
+ class TinyIRC::AppLogger
2
+ class << self
3
+ attr_accessor :log
4
+ end
5
+
6
+ @log = ParticleLog.new('web', ParticleLog::IO)
7
+ @log.level_table[ParticleLog::IO] = 'REQ'
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ end
12
+
13
+ def call(env)
14
+ @start = Time.now
15
+ @status, @headers, @body = @app.call(env)
16
+ @duration = ((Time.now - @start).to_f * 1000).round(2)
17
+
18
+ TinyIRC::AppLogger.log.io "#{env['REMOTE_ADDR']} --- #{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} --- #{@duration} ms"
19
+
20
+ [@status, @headers, @body]
21
+ end
22
+ end
23
+
24
+ class TinyIRC::App < Sinatra::Application
25
+ class << self
26
+ attr_accessor :bot
27
+ end
28
+
29
+ use TinyIRC::AppLogger
30
+
31
+ def self.cfg(bot)
32
+ TinyIRC::App.bot = bot
33
+ configure do
34
+ disable :show_exceptions
35
+
36
+ set :clean_trace, true
37
+
38
+ set :public_folder, File.dirname(__FILE__) + '/public'
39
+ set :views, File.dirname(__FILE__) + '/views'
40
+
41
+ set :bind, ENV['HOST'] || '0.0.0.0'
42
+ set :port, (ENV['PORT'] || 8080).to_i
43
+
44
+ disable :logging
45
+ end
46
+ end
47
+
48
+ helpers do
49
+ def h(text)
50
+ Rack::Utils.escape_html(text)
51
+ end
52
+ end
53
+
54
+ get '/' do
55
+ @pagename = 'Groups'
56
+ erb :index, layout: :layout
57
+ end
58
+
59
+ get '/favicon.ico' do 404 end
60
+
61
+ get '/:plugin' do
62
+ @pagename = params['plugin']
63
+ @plugin = TinyIRC::App.bot.plugins[params['plugin']]
64
+ raise RuntimeError, "Cannot find plugin `#{params['plugin']}`" unless @plugin
65
+ erb :plugin, layout: :layout
66
+ end
67
+
68
+ error 404 do
69
+ @page = '404'
70
+ erb :'404', layout: :layout
71
+ end
72
+
73
+ error do
74
+ @page = '500'
75
+ erb :'500', layout: :layout
76
+ end
77
+ end
@@ -0,0 +1,363 @@
1
+ class TinyIRC::Bot
2
+ class << self
3
+ attr_accessor :log
4
+ end
5
+
6
+ attr_accessor(
7
+ :config_file,
8
+ :config,
9
+ :sockets,
10
+ :plugins,
11
+ :groups,
12
+ :prefix,
13
+ :db,
14
+ :config_mtx,
15
+ :log
16
+ )
17
+
18
+ def initialize(config_file, db_file)
19
+ @config_file = config_file
20
+
21
+ @sockets = {}
22
+ @plugins = {}
23
+ @groups = {}
24
+
25
+ @log = ParticleLog.new 'bot', ParticleLog::INFO
26
+ @log.important 'Hello, IRC!'
27
+
28
+ @db = SQLite3::Database.open db_file
29
+ @db.execute <<~SQL
30
+ CREATE TABLE IF NOT EXISTS groupinfo (
31
+ server VARCHAR(32),
32
+ host VARCHAR(64),
33
+ name VARCHAR(32)
34
+ );
35
+ SQL
36
+ @log.info "db = #{db_file}"
37
+
38
+ @config_mtx = Mutex.new
39
+
40
+ load_config
41
+ end
42
+
43
+ def load_config
44
+ @config_mtx.synchronize do
45
+ @config = YAML.parse_file(@config_file).to_ruby
46
+ @prefix = @config['prefix'] || '!'
47
+ @log.info "prefix = #{@prefix}"
48
+
49
+ load_plugin_config
50
+ load_group_config
51
+ load_cooldown_config
52
+ load_server_config
53
+ end
54
+ end
55
+
56
+ def reload
57
+ @config_mtx.synchronize do
58
+ @plugins = {}
59
+ @config = YAML.parse_file(@config_file).to_ruby
60
+ @prefix = @config['prefix'] || '!'
61
+ @log.info "prefix = #{@prefix}"
62
+
63
+ load_plugin_config
64
+ load_group_config
65
+ load_cooldown_config
66
+ load_server_config
67
+ end
68
+ end
69
+
70
+ def load_plugin_config
71
+ plugin_config = { "tinyirc/plugins/core" => nil }.merge(@config['plugins'] || {})
72
+
73
+ def add(full, name)
74
+ n = File.basename(name)
75
+ if @plugins.include? n
76
+ @log.error "Cannot have multiple plugins with same basename (#{n})"
77
+ else
78
+ @plugins[n] = TinyIRC::Plugin.new self, full
79
+ end
80
+ end
81
+
82
+ def remove(name)
83
+ @plugins.delete name
84
+ end
85
+
86
+ pkeys = {}
87
+ plugin_config.keys.each do |k|
88
+ pkeys[File.basename(k)] = k
89
+ end
90
+
91
+ (@plugins.keys - pkeys.keys).each do |k|
92
+ @plugins[k].log.info 'Destroying...'
93
+ remove k
94
+ end
95
+
96
+ (pkeys.keys - @plugins.keys).each do |k|
97
+ add pkeys[k], k
98
+ end
99
+
100
+ @plugins.each_pair do |k, v|
101
+ cfg = plugin_config[k] || {}
102
+ raise RuntimeError, 'Invalid config type' if cfg.class != Hash
103
+ v._l_config(cfg)
104
+ unless v.loaded
105
+ v._l_postinit
106
+ end
107
+ end
108
+ end
109
+
110
+ def load_group_config
111
+ @groups = {}
112
+
113
+ group_config = {
114
+ "world" => {},
115
+ "admin" => {}
116
+ }.merge(@config['groups'] || {})
117
+
118
+ def add_group(name, group_config)
119
+ raise RuntimeError, 'User-defined group names cannot contain slashes!' if name.index '/'
120
+ g = TinyIRC::Group.new name
121
+ (group_config[name]['include'] || []).each do |g2|
122
+ if g2.index '/'
123
+ g2p, _ = *g2.split('/', 2)
124
+ unless @plugins.include? g2p
125
+ @log.error "There's no plugin called `#{g2p}`"
126
+ next
127
+ end
128
+ unless @plugins[g2p].groups.include? g2
129
+ @log.error "Plugin `#{g2p}` does not have group `#{g2}`"
130
+ next
131
+ end
132
+ @plugins[g2p].groups[g2].perms.each do |perm|
133
+ g.perm(perm.plugin, perm.command, perm.branch)
134
+ end
135
+ else
136
+ unless @groups.include? g2
137
+ @log.error "There's no group called `#{g2}`"
138
+ next
139
+ end
140
+ @groups[g2].perms.each do |perm|
141
+ g.perm(perm.plugin, perm.command, perm.branch)
142
+ end
143
+ end
144
+ end
145
+ (group_config[name]['perms'] || []).each do |perm|
146
+ g.perm *TinyIRC::Permission.parse(perm)
147
+ end
148
+ @groups[name] = g
149
+ end
150
+
151
+ def add(name, group_config)
152
+ add_group(name, group_config)
153
+ end
154
+
155
+ def remove(name)
156
+ @groups.delete name
157
+ end
158
+
159
+ (@groups.keys & group_config.keys).each do |k|
160
+ add_group(k, group_config)
161
+ end
162
+
163
+ (@groups.keys - group_config.keys).each do |k|
164
+ remove k
165
+ end
166
+
167
+ (group_config.keys - @groups.keys).each do |k|
168
+ add(k, group_config)
169
+ end
170
+ end
171
+
172
+ def load_cooldown_config
173
+ cooldown_config = @config['cooldowns'] || {}
174
+
175
+ l = ParticleLog.new('cooldowns', ParticleLog::INFO)
176
+
177
+ cooldown_config.each_pair do |cmd, cld|
178
+ cld = cld.to_i
179
+ a = cmd.split('/', 3)
180
+ if !a[0] || !a[1] && a[2]
181
+ l.error "Invalid command entry: #{cld}"
182
+ next
183
+ end
184
+ pname = a[0]
185
+ command = a[1] || :all
186
+ branch = a[2] || :all
187
+ unless @plugins.include? pname
188
+ l.error "There's no plugin called `#{pname}`"
189
+ next
190
+ end
191
+ plugin = @plugins[pname]
192
+ upd = lambda do |command|
193
+ if branch == :all
194
+ command.branches.each_pair do |_, b|
195
+ b.cooldown = cld
196
+ l.info "#{pname}/#{command.name}/#{b.id} <- #{cld}s"
197
+ end
198
+ else
199
+ unless command.branches.include? branch
200
+ l.error "`#{pname}/#{command.name}` command does not have branch called `#{branch}`"
201
+ next
202
+ end
203
+ command.branches[branch].cooldown = cld
204
+ l.info "#{pname}/#{command.name}/#{branch} <- #{cld}s"
205
+ end
206
+ end
207
+ if command == :all
208
+ plugin.commands.each_pair do |_, cmd|
209
+ upd.(cmd)
210
+ end
211
+ else
212
+ unless plugin.commands.include? command
213
+ l.error "`#{pname}/#{command}` command does not have branch called `#{branch}`"
214
+ next
215
+ end
216
+ upd.(plugin.commands[command])
217
+ end
218
+ end
219
+ end
220
+
221
+ def load_server_config
222
+ server_config = @config['servers'] || {}
223
+
224
+ def add(name, cfg)
225
+ s = TinyIRC::IRCSocket.new(self, name, cfg)
226
+ Thread.new { s.connect }
227
+ @sockets[name] = s
228
+ end
229
+
230
+ def remove(name)
231
+ @sockets[name].mtx.synchronize do
232
+ @sockets.delete(name).disconnect
233
+ end
234
+ end
235
+
236
+ (@sockets.keys & server_config.keys).each do |k|
237
+ s = @sockets[k]
238
+ c = server_config[k]
239
+ Thread.new do
240
+ #s.mtx.synchronize do
241
+ should_restart = false
242
+ should_restart = true if s.host != c['host']
243
+ should_restart = true if s.port != c['port']
244
+ should_restart = true if s.user != c['user']
245
+ should_restart = true if s.rnam != c['rnam']
246
+
247
+ s.disconnect if should_restart
248
+
249
+ s.host = c['host']
250
+ s.port = c['port']
251
+ s.nick = c['nick']
252
+ s.user = c['user']
253
+ s.pass = c['pass']
254
+ s.rnam = c['rnam']
255
+
256
+ s.connect if should_restart
257
+ end
258
+ end
259
+
260
+ (@sockets.keys - server_config.keys).each do |k|
261
+ remove k
262
+ end
263
+
264
+ (server_config.keys - @sockets.keys).each do |k|
265
+ add(k, server_config[k])
266
+ end
267
+ end
268
+
269
+ def handle_command(h, cmd_info)
270
+ @plugins.each_pair do |name, plugin|
271
+ return true if plugin.handle_command(h, cmd_info)
272
+ end
273
+ false
274
+ end
275
+
276
+ def handle_event(e)
277
+ TinyIRC.define_event_methods(e)
278
+ Thread.new do
279
+ @plugins['core'].handle_event(e)
280
+ end
281
+ end
282
+
283
+ def start
284
+ Thread.new do
285
+ ioloop
286
+ end
287
+
288
+ TinyIRC::App.cfg self
289
+ TinyIRC::App.run!
290
+ end
291
+
292
+ def ioloop
293
+ def _read_line(socket)
294
+ res = IO.select([socket.sock], nil, nil, 0.001)
295
+ return nil unless res
296
+ socket.mtx.synchronize do
297
+ begin
298
+ return socket.sock.readline("\r\n", chomp: true).force_encoding("UTF-8")
299
+ rescue => e
300
+ socket.log.error "#{e.class.name} - #{e.message}"
301
+ socket.running = false
302
+ handle_event(type: :disconnect, socket: socket)
303
+ return nil
304
+ end
305
+ end
306
+ end
307
+
308
+ def read_line(socket)
309
+ return unless socket.running
310
+ msg = _read_line(socket)
311
+ return unless msg
312
+ socket.log.io "R> #{msg}"
313
+ handle_event(type: :raw, raw_data: msg, socket: socket, bot: self)
314
+ end
315
+
316
+ def write_line(socket)
317
+ return unless socket.running
318
+
319
+ previous = socket.last_write
320
+ current = Time.now
321
+ diff = current - previous
322
+ return if diff < 0.7 && diff > 0
323
+ # todo - implement bursts
324
+
325
+ msg = begin
326
+ socket.queue.pop true
327
+ rescue ThreadError
328
+ nil
329
+ end
330
+
331
+ return unless msg
332
+ socket.mtx.synchronize do
333
+ begin
334
+ socket.direct_write msg.force_encoding("UTF-8")
335
+ rescue => e
336
+ socket.log.error "#{e.class.name} - #{e.message}"
337
+ socket.running = false
338
+ handle_event(type: :disconnect, socket: socket)
339
+ return nil
340
+ end
341
+ end
342
+
343
+ socket.last_write = current
344
+ end
345
+
346
+ while true do
347
+ sleep 0.001
348
+
349
+ @sockets.each_pair do |sname, socket|
350
+ sleep 0.001
351
+
352
+ next unless socket.running
353
+
354
+ read_line(socket)
355
+ write_line(socket)
356
+ end
357
+ end
358
+ end
359
+
360
+ def inspect
361
+ '#<TinyIRC::Bot>'
362
+ end
363
+ end