daioikachan 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8c10cbfb0cfbfbc731033be5a02a9f8ea843f4f5
4
+ data.tar.gz: fe2a499b9da5d66a57458234d5de4f475034b734
5
+ SHA512:
6
+ metadata.gz: 585730388e9aff3c91ef30e36060957d3363be9cc6b376d9f23c843d1b617112383466a5166d212d4319be2083529989c9bff9d706a29ee4fd72d85f1d3d7f89
7
+ data.tar.gz: 4910660d2dbd34642730b438362b7b9b799efb8946f3cd5abb7bbbd3a6beebce1589e83a61d2029fb50bd21b5763ff5ca69ac3da70f2c3e13fbdebdca2d33f7b
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ /*.gem
2
+ ~*
3
+ #*
4
+ *~
5
+ .bundle
6
+ Gemfile.lock
7
+ .rbenv-version
8
+ vendor
9
+ doc/*
10
+ tmp/*
11
+ coverage
12
+ .yardoc
13
+ .ruby-version
14
+ pkg/*
15
+ .tags
16
+ .env
17
+ daioikachan.conf
data/.travis.yml ADDED
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1
6
+ - 2.2
7
+
8
+ branches:
9
+ only:
10
+ - master
11
+
12
+ script: bundle exec rake test
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # 0.0.1 (2015/03/22)
2
+
3
+ Initial version
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Naotoshi Seo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # Daioikachan
2
+
3
+ [Ikachan](https://github.com/yappo/p5-App-Ikachan) compatible interface with multiple backends (IRC, Slack, etc).
4
+
5
+ ## Requirements
6
+
7
+ * Ruby
8
+
9
+ ## Installation
10
+
11
+ Write Gemfile:
12
+
13
+ ```
14
+ source "https://rubygems.org"
15
+
16
+ gem 'daioikachan'
17
+ ```
18
+
19
+ Run:
20
+
21
+ ```
22
+ $ bundle
23
+ ```
24
+
25
+ IRC, Slack backends are bundled as default.
26
+
27
+ Daioikachan supports plugin architecture. You may add your favirite backends as gems. See Plugin section for details.
28
+
29
+ ## How to Run
30
+
31
+ This is an example to post messages for both IRC and Slack.
32
+
33
+ ### Start
34
+
35
+ Generate a sample [daioikachan.conf](./examples/example.conf):
36
+
37
+ ```
38
+ $ bundle exec daioikachan -g daioikachan.conf
39
+ ```
40
+
41
+ Create `.env` and configure your IRC server and Slack token:
42
+
43
+ ```
44
+ IRC_SERVER=XX.XX.XX.XX
45
+ SLACK_API_TOKEN=XXX-XXXXX-XXXXXX-XXXXX
46
+ ````
47
+
48
+ Start `daioikachan` as
49
+
50
+ ```
51
+ $ bundle exec daioikachan
52
+ ```
53
+
54
+ ### Test
55
+
56
+ Test to post a message to `#daioikachan` channel of both IRC and Slack via `daioikachan` like:
57
+
58
+ ```
59
+ $ curl -d "channel=#daioikachan&message=test daioikachan" http://localhost:4979/notice
60
+ ```
61
+
62
+ Look whether posting to IRC, and Slack succeeds.
63
+
64
+ ## Configuration
65
+
66
+ See [example.conf](./example.conf) or [multi_slack.conf](./examples/multi_slack.conf) as examples.
67
+
68
+ You might've noticed that the config file is similar with Fluentd.
69
+ Yes, `daioikachan` is created based on Fluentd.
70
+ So, you can use `routing` functions which Fluentd has (`tag` and `label`).
71
+
72
+ See [config-file](http://docs.fluentd.org/articles/config-file) documentation of Fluentd for details.
73
+
74
+ See following pages for built-in plugins.
75
+
76
+ * [in_daioikachan](./README/in_daioikachan.md)
77
+ * [fluent-plugin-irc](./README/out_irc.md)
78
+ * [fluent-plugin-slack](https://github.com/sowawa/fluent-plugin-slack)
79
+
80
+ If you need other backends, search other fluentd output plugins and add them. Enjoy!
81
+
82
+ ## Plugin
83
+
84
+ You can create and add your own backends for daioikachan as a Fluentd Plugin.
85
+
86
+ See http://www.fluentd.org/plugins#notifications for available plugins.
87
+
88
+ ## API
89
+
90
+ ### /notice
91
+
92
+ Send `notice` message.
93
+
94
+ ```
95
+ $ curl -d "channel=#channel&message=test message" http://localhost:4979/notice
96
+ ```
97
+
98
+ ### /privmsg
99
+
100
+ Send `privmsg` message.
101
+
102
+ ```
103
+ $ curl -d "channel=#channel&message=test message" http://localhost:4979/privmsg
104
+ ```
105
+
106
+ ### /join
107
+
108
+ The server always returns 200, and ignore.
109
+
110
+ IRC client (`fluent-plugin-irc`) automatically joins to a channel on sending message.
111
+ Slack client does not require to join to a channel.
112
+
113
+ ### /leave
114
+
115
+ The server always returns 200, and ignores.
116
+
117
+ ## ChangeLog
118
+
119
+ See [CHANGELOG.md](CHANGELOG.md) for details.
120
+
121
+ ## Contributing
122
+
123
+ 1. Fork it
124
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
125
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
126
+ 4. Push to the branch (`git push origin my-new-feature`)
127
+ 5. Create new [Pull Request](../../pull/new/master)
128
+
129
+ ## Copyright
130
+
131
+ ### daioikachan, and fluent-plugin-daioikachan
132
+
133
+ Copyright (c) 2015 Naotoshi Seo. See [LICENSE](LICENSE) for details.
134
+
135
+ ### fluent-plugin-irc
136
+
137
+ See https://github.com/choplin/fluent-plugin-irc
138
+
139
+ ### fluent-plugin-slack
140
+
141
+ See https://github.com/sowawa/fluent-plugin-slack
@@ -0,0 +1,63 @@
1
+ # Daioikachan plugin for Fluentd
2
+
3
+ This plugin provides [ikachan](https://github.com/yappo/p5-App-Ikachan) compatible interface by Fluentd.
4
+
5
+ ## Installation
6
+
7
+ Bundled with `daioikachan` gem.
8
+
9
+ ## Configuration
10
+
11
+ ```apache
12
+ <source>
13
+ type daioikachan
14
+ bind 127.0.0.1
15
+ port 4979
16
+ backlog 2048
17
+
18
+ # optional Puma parameters
19
+ min_threads 0
20
+ max_threads 4
21
+ </source>
22
+ ```
23
+
24
+ Receiving API post like below,
25
+
26
+ ```
27
+ $ curl -d "channel=#channel&message=test message" http://localhost:4979/notice
28
+ ```
29
+
30
+ emits an event as
31
+
32
+ ```
33
+ notice.channel {"command":"notice","channel":"channel","message":"test message"}
34
+ ```
35
+
36
+ ## API
37
+
38
+ ### /notice
39
+
40
+ Send `notice` message.
41
+
42
+ ```
43
+ $ curl -d "channel=#channel&message=test message" http://localhost:4979/notice
44
+ ```
45
+
46
+ ### /privmsg
47
+
48
+ Send `privmsg` message.
49
+
50
+ ```
51
+ $ curl -d "channel=#channel&message=test message" http://localhost:4979/privmsg
52
+ ```
53
+
54
+ ### /join
55
+
56
+ The server always returns 200, and ignored.
57
+
58
+ IRC client should automatically join to a channel on sending message.
59
+ Slack client does not require to join to a channel.
60
+
61
+ ### /leave
62
+
63
+ The server always returns 200, and ignored.
data/README/out_irc.md ADDED
@@ -0,0 +1,58 @@
1
+ # Fluent::Plugin::Irc, a plugin for [Fluentd](http://fluentd.org)
2
+
3
+ [![Build Status](https://travis-ci.org/choplin/fluent-plugin-irc.svg)](https://travis-ci.org/choplin/fluent-plugin-irc)
4
+
5
+ Fluent plugin to send messages to IRC server
6
+
7
+ ## Installation
8
+
9
+ `$ fluent-gem install fluent-plugin-irc`
10
+
11
+ ## Configuration
12
+
13
+ ### Example
14
+
15
+ ```
16
+ <match **>
17
+ type irc
18
+ host localhost
19
+ port 6667
20
+ channel fluentd
21
+ nick fluentd
22
+ user fluentd
23
+ real fluentd
24
+ message notice: %s [%s] %s
25
+ out_keys tag,time,message
26
+ time_key time
27
+ time_format %Y/%m/%d %H:%M:%S
28
+ tag_key tag
29
+ </match>
30
+ ```
31
+
32
+ ### Parameter
33
+
34
+ |parameter|description|default|
35
+ |---|---|---|
36
+ |host|IRC server host|localhost|
37
+ |port|IRC server port number|6667|
38
+ |channel|channel to send messages (without first '#')||
39
+ |channel_keys|keys used to format channel. %s will be replaced with value specified by channel_keys if this option is used|nil|
40
+ |nick|nickname registerd of IRC|fluentd|
41
+ |user|user name registerd of IRC|fluentd|
42
+ |real|real name registerd of IRC|fluentd|
43
+ |message|message format. %s will be replaced with value specified by out_keys||
44
+ |out_keys|keys used to format messages||
45
+ |time_key|key name for time|time|
46
+ |time_format|time format. This will be formatted with Time#strftime.|%Y/%m/%d %H:%M:%S|
47
+ |tag_key|key name for tag|tag|
48
+ |command|irc command. `privmsg` or `notice`|privmsg|
49
+ |command_keys|keys used to format command. %s will be replaced with value specified by command_keys if this option is used|nil|
50
+ |send_interval|interval (sec) to send message. defence Excess Flood|2|
51
+ |max_send_queue|maximum size of send message queue|100|
52
+
53
+ ## Copyright
54
+
55
+ <table>
56
+ <tr><td>Copyright</td><td>Copyright (c) 2015 OKUNO Akihiro</td></tr>
57
+ <tr><td>License</td><td>Apache License, Version 2.0</td></tr>
58
+ </table>
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require 'bundler'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ require 'rake/testtask'
7
+
8
+ Rake::TestTask.new(:test) do |test|
9
+ test.libs << 'lib' << 'test'
10
+ test.test_files = (Dir["test/plugin/test_*.rb"] - ["helper.rb"]).sort
11
+ test.verbose = true
12
+ end
13
+
14
+ task :default => [:test]
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bin/daioikachan ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+ require 'rubygems' unless defined?(gem)
4
+ ROOT = File.expand_path('..', File.dirname(__FILE__))
5
+ $LOAD_PATH << File.expand_path(File.join(ROOT, 'lib'))
6
+
7
+ require 'dotenv'
8
+ Dotenv.load
9
+
10
+ # tweak default_options
11
+ require 'fluent/supervisor'
12
+ class Fluent::Supervisor
13
+ class << self
14
+ alias :fluentd_default_options :default_options
15
+ end
16
+ def self.default_options
17
+ fluentd_default_options.merge({
18
+ :config_path => 'daioikachan.conf',
19
+ :plugin_dirs => [File.join(ROOT, 'lib/fluent/plugin')],
20
+ })
21
+ end
22
+ end
23
+
24
+ # handle argv to generate conf
25
+ require 'optparse'
26
+ opts = {}
27
+ opt = OptionParser.new
28
+ opt.on('-g', "--generate PATH", "generate a sample configuration file") {|s|
29
+ opts[:generate_path] = s
30
+ }
31
+ begin
32
+ opt.parse!(ARGV)
33
+ rescue OptionParser::InvalidOption => e
34
+ argv = ARGV # suppress warning to constant
35
+ argv.unshift(*(e.args)) # restore and ignore unknown options to pass to Fluentd
36
+ end
37
+
38
+ if generate_path = opts[:generate_path]
39
+ require 'fileutils'
40
+ FileUtils.mkdir_p File.dirname(generate_path)
41
+ if File.exist?(generate_path)
42
+ puts "#{generate_path} already exists."
43
+ else
44
+ File.open(generate_path, "w") {|f|
45
+ conf = File.read File.join(ROOT, 'examples/example.conf')
46
+ f.write conf
47
+ }
48
+ puts "Installed #{generate_path}."
49
+ end
50
+ exit 0
51
+ end
52
+
53
+ require 'fluent/command/fluentd'
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "daioikachan"
6
+ gem.description = "Ikachan compatible interface with multiple backends (IRC, Slack, etc)"
7
+ gem.homepage = "https://github.com/sonots/daioikachan"
8
+ gem.summary = gem.description
9
+ gem.version = File.read("VERSION").strip
10
+ gem.authors = ["Naotoshi Seo"]
11
+ gem.email = "sonots@gmail.com"
12
+ gem.has_rdoc = false
13
+ gem.license = 'MIT License'
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ gem.require_paths = ['lib']
18
+
19
+ gem.add_dependency "fluentd", "~> 0.12.0"
20
+ gem.add_dependency "dotenv"
21
+ gem.add_dependency "puma"
22
+ # gem.add_dependency "fluent-plugin-irc"
23
+ gem.add_dependency "irc_parser"
24
+ gem.add_dependency "fluent-plugin-slack", ">= 0.5.0"
25
+ gem.add_development_dependency "rake"
26
+ end
@@ -0,0 +1,57 @@
1
+ <source>
2
+ type daioikachan
3
+ bind 0.0.0.0
4
+ port 4979
5
+ min_threads 0
6
+ max_threads 4
7
+ backlog 1024
8
+ @label @raw
9
+ </source>
10
+
11
+ <label @raw>
12
+ <match **>
13
+ type copy
14
+ <store>
15
+ type stdout
16
+ </store>
17
+ <store>
18
+ type relabel
19
+ @label @slack
20
+ </store>
21
+ <store>
22
+ type relabel
23
+ @label @irc
24
+ </store>
25
+ </match>
26
+ </label>
27
+
28
+ <label @irc>
29
+ <match **>
30
+ type irc
31
+ host "#{ENV['IRC_SERVER']}"
32
+ port 6667
33
+ nick daioikachan
34
+ user daioikachan
35
+ real daioikachan
36
+ command %s
37
+ command_keys command
38
+ channel %s
39
+ channel_keys channel
40
+ message %s
41
+ out_keys message
42
+ send_interval 2s # IRC has Excess Flood limit, this is the default value taken from ikachan
43
+ </match>
44
+ </label>
45
+
46
+ <label @slack>
47
+ <match **>
48
+ type slack
49
+ token "#{ENV['SLACK_API_TOKEN']}"
50
+ username daioikachan
51
+ channel %s
52
+ channel_keys channel
53
+ color good
54
+ icon_emoji :ghost:
55
+ flush_interval 1s # slack API has limit as a post / sec
56
+ </match>
57
+ </label>
@@ -0,0 +1,46 @@
1
+ <source>
2
+ type daioikachan
3
+ bind 0.0.0.0
4
+ port 4979
5
+ min_threads 0
6
+ max_threads 4
7
+ backlog 1024
8
+ @label @raw
9
+ </source>
10
+
11
+ <label @raw>
12
+ <match **>
13
+ type copy
14
+ <store>
15
+ type stdout
16
+ </store>
17
+ <store>
18
+ type relabel
19
+ @label @slack
20
+ </store>
21
+ </match>
22
+ </label>
23
+
24
+ <label @slack>
25
+ # #fluentd_warn => team1.slack.com#general
26
+ <match *.fluentd_warn>
27
+ type slack
28
+ token "#{ENV['TEAM1_TOKEN']}"
29
+ username daioikachan
30
+ channel general
31
+ color bad
32
+ icon_emoji :ghost:
33
+ flush_interval 1s # slack API has limit as a post / sec
34
+ </match>
35
+ # other channels => default_team.slack.com#${channel}
36
+ <match **>
37
+ type slack
38
+ token "#{ENV['DEFAULT_TEAM_TOKEN']}"
39
+ username daioikachan
40
+ channel %s
41
+ channel_keys channel
42
+ color bad
43
+ icon_emoji :ghost:
44
+ flush_interval 1s # slack API has limit as a post / sec
45
+ </match>
46
+ </label>
@@ -0,0 +1,160 @@
1
+ module Fluent
2
+ class DaioikachanInput < Input
3
+ Plugin.register_input('daioikachan', self)
4
+
5
+ def initialize
6
+ require 'puma'
7
+ require 'uri'
8
+ super
9
+ end
10
+
11
+ config_param :port, :integer, :default => 4979
12
+ config_param :bind, :string, :default => '0.0.0.0'
13
+ config_param :min_threads, :integer, :default => 0
14
+ config_param :max_threads, :integer, :default => 4
15
+ config_param :backlog, :integer, :default => nil
16
+
17
+ def configure(conf)
18
+ super
19
+ end
20
+
21
+ def start
22
+ super
23
+
24
+ # Refer puma's Runner and Rack handler for puma server setup
25
+ @server = ::Puma::Server.new(method(:on_request))
26
+ @server.min_threads = @min_threads
27
+ @server.max_threads = @max_threads
28
+ @server.leak_stack_on_error = false
29
+ setup_http
30
+
31
+ @app = App.new(self)
32
+
33
+ @thread = Thread.new(&method(:run))
34
+ end
35
+
36
+ def shutdown
37
+ @server.stop(true)
38
+ @thread.join
39
+ end
40
+
41
+ def run
42
+ @server.run(false)
43
+ rescue => e
44
+ log.error "unexpected error", :error => e.to_s
45
+ log.error_backtrace e.backtrace
46
+ end
47
+
48
+ def on_request(env)
49
+ @app.run(env)
50
+ end
51
+
52
+ def setup_http
53
+ log.info "listening http on #{@bind}:#{@port}"
54
+
55
+ opts = [@bind, @port, true]
56
+ opts << @backlog if @backlog
57
+ @server.add_tcp_listener(*opts)
58
+ end
59
+
60
+ class App
61
+ class BadRequest < StandardError; end
62
+ class InternalServerError < StandardError; end
63
+ class NotFound < StandardError; end
64
+
65
+ attr_reader :router, :log
66
+
67
+ def initialize(plugin)
68
+ @router = plugin.router
69
+ @log = plugin.log
70
+ end
71
+
72
+ def run(env)
73
+ # req = Rack::Request.new(env)
74
+ method = env['REQUEST_METHOD'.freeze] # req.method
75
+ path = URI.parse(env['REQUEST_URI'.freeze]).path # req.path
76
+ body = env['rack.input'].read # req.body.read
77
+ params = Rack::Utils.parse_query(body)
78
+
79
+ begin
80
+ if method == 'POST'
81
+ case path
82
+ when '/notice'
83
+ notice(params)
84
+ when '/privmsg'
85
+ privmsg(params)
86
+ when '/join'
87
+ return ok
88
+ when '/leave'
89
+ return ok
90
+ else
91
+ return not_found
92
+ end
93
+ else
94
+ return not_found
95
+ end
96
+ rescue BadRequest => e
97
+ bad_request(e.message)
98
+ rescue NotFound => e
99
+ not_found(e.message)
100
+ rescue InternalServerError => e
101
+ internal_server_error(e.message)
102
+ rescue => e
103
+ internal_server_error("#{e.class} #{e.message} #{e.backtrace.first}")
104
+ else
105
+ ok
106
+ end
107
+ end
108
+
109
+ def notice(params)
110
+ channel, message = build_channel(params), build_message(params)
111
+ tag = "notice.#{channel}"
112
+ record = params.merge('command' => 'notice', 'channel' => channel, 'message' => message)
113
+ router.emit(tag, Fluent::Engine.now, record)
114
+ end
115
+
116
+ def privmsg(params)
117
+ channel, message = build_channel(params), build_message(params)
118
+ tag = "privmsg.#{channel}"
119
+ record = params.merge('command' => 'privmsg', 'channel' => channel, 'message' => message)
120
+ router.emit(tag, Fluent::Engine.now, record)
121
+ end
122
+
123
+ private
124
+
125
+ def build_channel(params)
126
+ unless channel = params.delete('channel')
127
+ raise BadRequest.new('`channel` parameter is mandatory')
128
+ end
129
+ if channel.start_with?('#')
130
+ channel[1..-1] # remove starting #
131
+ else
132
+ channel
133
+ end
134
+ end
135
+
136
+ def build_message(params)
137
+ unless message = params.delete('message')
138
+ raise BadRequest.new('`message` parameter is mandatory')
139
+ end
140
+ message # should I truncate message to max_length?
141
+ end
142
+
143
+ def ok(msg = nil)
144
+ [200, {'Content-type'=>'text/plain'}, ["OK\n#{msg}"]]
145
+ end
146
+
147
+ def bad_request(msg = nil)
148
+ [400, {'Content-type'=>'text/plain'}, ["Bad Request\n#{msg}"]]
149
+ end
150
+
151
+ def not_found(msg = nil)
152
+ [404, {'Content-type'=>'text/plain'}, ["Not Found\n#{msg}"]]
153
+ end
154
+
155
+ def internal_server_error(msg = nil)
156
+ [500, {'Content-type'=>'text/plain'}, ["Internal Server Error\n#{msg}"]]
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,283 @@
1
+ module Fluent
2
+ class IRCOutput < Fluent::Output
3
+ Fluent::Plugin.register_output('irc', self)
4
+
5
+ include SetTimeKeyMixin
6
+ include SetTagKeyMixin
7
+
8
+ config_set_default :include_time_key, true
9
+ config_set_default :include_tag_key, true
10
+
11
+ config_param :host , :string , :default => 'localhost'
12
+ config_param :port , :integer , :default => 6667
13
+ config_param :channel , :string
14
+ config_param :channel_keys, :default => nil do |val|
15
+ val.split(',')
16
+ end
17
+ config_param :nick , :string , :default => 'fluentd'
18
+ config_param :user , :string , :default => 'fluentd'
19
+ config_param :real , :string , :default => 'fluentd'
20
+ config_param :password , :string , :default => nil
21
+ config_param :message , :string
22
+ config_param :out_keys do |val|
23
+ val.split(',')
24
+ end
25
+ config_param :time_key , :string , :default => 'time'
26
+ config_param :time_format , :string , :default => '%Y/%m/%d %H:%M:%S'
27
+ config_param :tag_key , :string , :default => 'tag'
28
+ config_param :command , :string , :default => 'privmsg'
29
+ config_param :command_keys, :default => nil do |val|
30
+ val.split(',')
31
+ end
32
+
33
+ config_param :blocking_timeout, :time, :default => 0.5
34
+ config_param :max_send_queue, :integer, :default => 100
35
+ config_param :send_interval, :time, :default => 2
36
+
37
+ COMMAND_MAP = {
38
+ 'priv_msg' => :priv_msg,
39
+ 'privmsg' => :priv_msg,
40
+ 'notice' => :notice,
41
+ }
42
+
43
+ # To support log_level option implemented by Fluentd v0.10.43
44
+ unless method_defined?(:log)
45
+ define_method("log") { $log }
46
+ end
47
+
48
+ attr_reader :conn # for test
49
+
50
+ def initialize
51
+ super
52
+ require 'irc_parser'
53
+ end
54
+
55
+ def configure(conf)
56
+ super
57
+
58
+ begin
59
+ @message % (['1'] * @out_keys.length)
60
+ rescue ArgumentError
61
+ raise Fluent::ConfigError, "string specifier '%s' and out_keys specification mismatch"
62
+ end
63
+
64
+ if @channel_keys
65
+ begin
66
+ @channel % (['1'] * @channel_keys.length)
67
+ rescue ArgumentError
68
+ raise Fluent::ConfigError, "string specifier '%s' and channel_keys specification mismatch"
69
+ end
70
+ end
71
+ @channel = '#'+@channel
72
+
73
+ if @command_keys
74
+ begin
75
+ @command % (['1'] * @command_keys.length)
76
+ rescue ArgumentError
77
+ raise Fluent::ConfigError, "string specifier '%s' and command_keys specification mismatch"
78
+ end
79
+ else
80
+ unless @command = COMMAND_MAP[@command]
81
+ raise Fluent::ConfigError, "command must be one of #{COMMAND_MAP.keys.join(', ')}"
82
+ end
83
+ end
84
+
85
+ @send_queue = []
86
+ end
87
+
88
+ def start
89
+ super
90
+
91
+ begin
92
+ @loop = Coolio::Loop.new
93
+ @conn = create_connection
94
+ @timer = TimerWatcher.new(@send_interval, true, log, &method(:on_timer))
95
+ @loop.attach(@timer)
96
+ @thread = Thread.new(&method(:run))
97
+ rescue => e
98
+ puts e
99
+ raise Fluent::ConfigError, "failed to connect IRC server #{@host}:#{@port}"
100
+ end
101
+ end
102
+
103
+ def shutdown
104
+ super
105
+ @loop.watchers.each { |w| w.detach }
106
+ @loop.stop
107
+ @conn.close
108
+ @thread.join
109
+ end
110
+
111
+ def run
112
+ @loop.run(@blocking_timeout)
113
+ rescue => e
114
+ log.error "unexpected error", :error => e, :error_class => e.class
115
+ log.error_backtrace
116
+ end
117
+
118
+ def emit(tag, es, chain)
119
+ chain.next
120
+
121
+ if @conn.closed?
122
+ log.warn "out_irc: connection is closed. try to reconnect"
123
+ @conn = create_connection
124
+ end
125
+
126
+ es.each do |time,record|
127
+ if @send_queue.size >= @max_send_queue
128
+ log.warn "out_irc: send queue size exceeded max_send_queue(#{@max_send_queue}), discards"
129
+ break
130
+ end
131
+
132
+ filter_record(tag, time, record)
133
+ command, channel, message = build_command(record), build_channel(record), build_message(record)
134
+ log.debug { "out_irc: push {command:\"#{command}\", channel:\"#{channel}\", message:\"#{message}\"}" }
135
+ @send_queue.push([command, channel, message])
136
+ end
137
+ end
138
+
139
+ def on_timer
140
+ return if @send_queue.empty?
141
+ command, channel, message = @send_queue.shift
142
+ log.info { "out_irc: send {command:\"#{command}\", channel:\"#{channel}\", message:\"#{message}\"}" }
143
+ @conn.send_message(command, channel, message)
144
+ end
145
+
146
+ private
147
+
148
+ def create_connection
149
+ conn = IRCConnection.connect(@host, @port)
150
+ conn.log = log
151
+ conn.nick = @nick
152
+ conn.user = @user
153
+ conn.real = @real
154
+ conn.password = @password
155
+ conn.attach(@loop)
156
+ conn
157
+ end
158
+
159
+ def build_message(record)
160
+ values = fetch_keys(record, @out_keys)
161
+ @message % values
162
+ end
163
+
164
+ def build_channel(record)
165
+ return @channel unless @channel_keys
166
+
167
+ values = fetch_keys(record, @channel_keys)
168
+ @channel % values
169
+ end
170
+
171
+ def build_command(record)
172
+ return @command unless @command_keys
173
+
174
+ values = fetch_keys(record, @command_keys)
175
+ unless command = COMMAND_MAP[@command % values]
176
+ log.warn "out_irc: command is not one of #{COMMAND_MAP.keys.join(', ')}, use privmsg"
177
+ end
178
+ command || :priv_msg
179
+ end
180
+
181
+ def fetch_keys(record, keys)
182
+ Array(keys).map do |key|
183
+ begin
184
+ record.fetch(key).to_s
185
+ rescue KeyError
186
+ log.warn "out_irc: the specified key '#{key}' not found in record. [#{record}]"
187
+ ''
188
+ end
189
+ end
190
+ end
191
+
192
+ class TimerWatcher < Coolio::TimerWatcher
193
+ def initialize(interval, repeat, log, &callback)
194
+ @callback = callback
195
+ @log = log
196
+ super(interval, repeat)
197
+ end
198
+
199
+ def on_timer
200
+ @callback.call
201
+ rescue
202
+ @log.error $!.to_s
203
+ @log.error_backtrace
204
+ end
205
+ end
206
+
207
+ class IRCConnection < Cool.io::TCPSocket
208
+ attr_reader :joined # for test
209
+ attr_accessor :log, :nick, :user, :real, :password
210
+
211
+ def initialize(*args)
212
+ super
213
+ @joined = {}
214
+ end
215
+
216
+ def on_connect
217
+ if @password
218
+ IRCParser.message(:pass) do |m|
219
+ m.password = @password
220
+ write m
221
+ end
222
+ end
223
+ IRCParser.message(:nick) do |m|
224
+ m.nick = @nick
225
+ write m
226
+ end
227
+ IRCParser.message(:user) do |m|
228
+ m.user = @user
229
+ m.postfix = @real
230
+ write m
231
+ end
232
+ end
233
+
234
+ def on_read(data)
235
+ data.each_line do |line|
236
+ begin
237
+ msg = IRCParser.parse(line)
238
+ log.debug { "out_irc: on_read :#{msg.class.to_sym}" }
239
+ case msg.class.to_sym
240
+ when :ping
241
+ IRCParser.message(:pong) do |m|
242
+ m.target = msg.target
243
+ m.body = msg.body
244
+ write m
245
+ end
246
+ when :join
247
+ log.info { "out_irc: joined to #{msg.channels.join(', ')}" }
248
+ msg.channels.each {|channel| @joined[channel] = true }
249
+ when :err_nick_name_in_use
250
+ log.warn "out_irc: nickname \"#{msg.error_nick}\" is already in use."
251
+ when :error
252
+ log.warn "out_irc: an error occured. \"#{msg.error_message}\""
253
+ end
254
+ rescue
255
+ #TODO
256
+ end
257
+ end
258
+ end
259
+
260
+ def joined?(channel)
261
+ @joined[channel]
262
+ end
263
+
264
+ def join(channel)
265
+ IRCParser.message(:join) do |m|
266
+ m.channels = channel
267
+ write m
268
+ end
269
+ log.debug { "out_irc: join to #{channel}" }
270
+ end
271
+
272
+ def send_message(command, channel, message)
273
+ join(channel) unless joined?(channel)
274
+ IRCParser.message(command) do |m|
275
+ m.target = channel
276
+ m.body = message
277
+ write m
278
+ end
279
+ channel # return channel for test
280
+ end
281
+ end
282
+ end
283
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'test/unit'
2
+ require 'fileutils'
3
+ require 'fluent/log'
4
+ require 'fluent/test'
5
+
6
+ unless defined?(Test::Unit::AssertionFailedError)
7
+ class Test::Unit::AssertionFailedError < StandardError
8
+ end
9
+ end
10
+
11
+ def unused_port
12
+ s = TCPServer.open(0)
13
+ port = s.addr[1]
14
+ s.close
15
+ port
16
+ end
17
+
18
+ def ipv6_enabled?
19
+ require 'socket'
20
+
21
+ begin
22
+ TCPServer.open("::1", 0)
23
+ true
24
+ rescue
25
+ false
26
+ end
27
+ end
28
+
29
+ $log = Fluent::Log.new(Fluent::Test::DummyLogDevice.new, Fluent::Log::LEVEL_WARN)
@@ -0,0 +1,105 @@
1
+ require_relative '../helper'
2
+ require 'fluent/plugin/in_daioikachan'
3
+ require 'net/https'
4
+
5
+ class DaioikachanInputTest < Test::Unit::TestCase
6
+ def post(path, params, header = {}, ssl = false)
7
+ http = Net::HTTP.new("127.0.0.1", PORT)
8
+ if ssl
9
+ http.use_ssl = true
10
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
11
+ end
12
+ req = Net::HTTP::Post.new(path, header)
13
+ if params.is_a?(String)
14
+ req.body = params
15
+ else
16
+ req.set_form_data(params)
17
+ end
18
+ http.request(req)
19
+ end
20
+
21
+ def setup
22
+ Fluent::Test.setup
23
+ end
24
+
25
+ PORT = unused_port
26
+ CONFIG = %[
27
+ port #{PORT}
28
+ ]
29
+
30
+ def create_driver(conf = CONFIG)
31
+ Fluent::Test::InputTestDriver.new(Fluent::DaioikachanInput).configure(conf, true)
32
+ end
33
+
34
+ def test_default_configure
35
+ d = create_driver(%[])
36
+ assert_equal 4979, d.instance.port
37
+ assert_equal '0.0.0.0', d.instance.bind
38
+ assert_equal 0, d.instance.min_threads
39
+ assert_equal 4, d.instance.max_threads
40
+ assert_equal nil, d.instance.backlog
41
+ end
42
+
43
+ def test_configure
44
+ d = create_driver(%[
45
+ port #{PORT}
46
+ bind 127.0.0.1
47
+ min_threads 0
48
+ max_threads 4
49
+ backlog 1024
50
+ ])
51
+ assert_equal PORT, d.instance.port
52
+ assert_equal '127.0.0.1', d.instance.bind
53
+ assert_equal 0, d.instance.min_threads
54
+ assert_equal 4, d.instance.max_threads
55
+ assert_equal 1024, d.instance.backlog
56
+ end
57
+
58
+ def test_notice
59
+ d = create_driver
60
+
61
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
62
+ Fluent::Engine.now = time
63
+
64
+ d.expect_emit "notice.channel", time, {"command" => "notice", "channel" => "channel", "message" => "message"}
65
+
66
+ d.run do
67
+ d.expected_emits.each {|tag, time, record|
68
+ res = post("/notice", {command: "notice", channel: "channel", message: "message"})
69
+ assert_equal "200", res.code
70
+ }
71
+ end
72
+ end
73
+
74
+ def test_privmsg
75
+ d = create_driver
76
+
77
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
78
+ Fluent::Engine.now = time
79
+
80
+ d.expect_emit "privmsg.channel", time, {"command" => "privmsg", "channel" => "channel", "message" => "message"}
81
+
82
+ d.run do
83
+ d.expected_emits.each {|tag, time, record|
84
+ res = post("/privmsg", {command: "privmsg", channel: "channel", message: "message"})
85
+ assert_equal "200", res.code
86
+ }
87
+ end
88
+ end
89
+
90
+ def test_join
91
+ d = create_driver
92
+ d.run do
93
+ res = post("/join", {})
94
+ assert_equal "200", res.code
95
+ end
96
+ end
97
+
98
+ def test_leave
99
+ d = create_driver
100
+ d.run do
101
+ res = post("/leave", {})
102
+ assert_equal "200", res.code
103
+ end
104
+ end
105
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: daioikachan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Naotoshi Seo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fluentd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.12.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.12.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: dotenv
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: puma
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: irc_parser
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: fluent-plugin-slack
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.5.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 0.5.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Ikachan compatible interface with multiple backends (IRC, Slack, etc)
98
+ email: sonots@gmail.com
99
+ executables:
100
+ - daioikachan
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".travis.yml"
106
+ - CHANGELOG.md
107
+ - Gemfile
108
+ - LICENSE
109
+ - README.md
110
+ - README/in_daioikachan.md
111
+ - README/out_irc.md
112
+ - Rakefile
113
+ - VERSION
114
+ - bin/daioikachan
115
+ - daioikachan.gemspec
116
+ - examples/example.conf
117
+ - examples/multi_slack.conf
118
+ - lib/fluent/plugin/in_daioikachan.rb
119
+ - lib/fluent/plugin/out_irc.rb
120
+ - test/helper.rb
121
+ - test/plugin/test_in_daioikachan.rb
122
+ homepage: https://github.com/sonots/daioikachan
123
+ licenses:
124
+ - MIT License
125
+ metadata: {}
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ required_rubygems_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: '0'
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 2.2.2
143
+ signing_key:
144
+ specification_version: 4
145
+ summary: Ikachan compatible interface with multiple backends (IRC, Slack, etc)
146
+ test_files:
147
+ - test/helper.rb
148
+ - test/plugin/test_in_daioikachan.rb