├── .gitignore ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── app.rb ├── config.rb ├── config.ru ├── logbot.god ├── logbot.rb.example ├── public ├── applications.js └── images │ └── img_dropdown_violet.svg ├── sass ├── _solarized.sass ├── screen.sass └── widget.sass ├── screenshot.png ├── utils └── migrate_db.rb └── views ├── channel.erb └── widget.erb /.gitignore: -------------------------------------------------------------------------------- 1 | .swp 2 | /public/*.css 3 | .sass-cache 4 | .DS_Store 5 | /fire_app_log.txt 6 | *.rdb 7 | /logbot.rb 8 | /tmp 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | from base:latest 2 | run echo "deb http://ppa.launchpad.net/brightbox/ruby-ng/ubuntu precise main" >> /etc/apt/sources.list 3 | run echo "deb http://ppa.launchpad.net/chris-lea/redis-server/ubuntu precise main" >> /etc/apt/sources.list 4 | run apt-get update 5 | run apt-get install --force-yes -y ruby1.9.1 rubygems redis-server 6 | add . / 7 | run gem install bundler 8 | run apt-get install --force-yes -y ruby1.9.1-dev 9 | run bundle install 10 | run compass compile 11 | run cp logbot.rb.example logbot.rb 12 | expose 6379 13 | expose :5000 14 | env LOGBOT_NICK logbot_ 15 | env LOGBOT_SERVER irc.freenode.net 16 | env LOGBOT_CHANNELS #test56 17 | cmd ["sh", "-c", "/usr/bin/redis-server | foreman start"] 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'rake' 4 | gem 'foreman' 5 | gem 'sinatra' 6 | gem 'async_sinatra' 7 | gem 'eventmachine' 8 | gem 'shotgun' 9 | gem 'compass' 10 | gem 'haml' 11 | gem 'sass' 12 | gem 'redis' 13 | gem 'cinch' 14 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | async_sinatra (1.0.0) 5 | rack (>= 1.4.1) 6 | sinatra (>= 1.3.2) 7 | chunky_png (1.2.7) 8 | cinch (2.0.3) 9 | compass (0.12.2) 10 | chunky_png (~> 1.2) 11 | fssm (>= 0.2.7) 12 | sass (~> 3.1) 13 | eventmachine (1.0.0) 14 | foreman (0.61.0) 15 | thor (>= 0.13.6) 16 | fssm (0.2.9) 17 | haml (3.1.7) 18 | rack (1.4.4) 19 | rack-protection (1.3.2) 20 | rack 21 | rake (10.0.3) 22 | redis (3.0.2) 23 | sass (3.2.5) 24 | shotgun (0.9) 25 | rack (>= 1.0) 26 | sinatra (1.3.3) 27 | rack (~> 1.3, >= 1.3.6) 28 | rack-protection (~> 1.2) 29 | tilt (~> 1.3, >= 1.3.3) 30 | thor (0.16.0) 31 | tilt (1.3.3) 32 | 33 | PLATFORMS 34 | ruby 35 | 36 | DEPENDENCIES 37 | async_sinatra 38 | cinch 39 | compass 40 | eventmachine 41 | foreman 42 | haml 43 | rake 44 | redis 45 | sass 46 | shotgun 47 | sinatra 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Shao-Chung Chen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: shotgun -o 0.0.0.0 -p 5000 config.ru 2 | logbot: ruby logbot.rb 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Logbot 2 | ====== 3 | Logbot is a simple IRC logger with realtime web-based viewer. 4 | 5 | 6 | Screenshot 7 | ---------- 8 |  9 | 10 | 11 | How to Deploy 12 | ------------- 13 | * Use Docker 14 | 1. Install [Docker](https://www.docker.com/) 15 | 2. Run `docker run -e LOGBOT_NICK=xxxx -e LOGBOT_CHANNELS=#x,#y,#z -e LOGBOT_SERVER=168.95.1.1 dannvix/logbot` 16 | 3. Visit [http://localhost:5000](http://localhost:5000) 17 | 18 | * Manual installation 19 | 1. Ruby (1.9.3+) and Redis server must be installed 20 | 2. Run `bundle install` to install required Ruby gems 21 | 3. Run `compass compile` to compile Sass files 22 | 4. Fire up your `redis-server` 23 | 5. Specify target channels in `logbot.rb` 24 | 6. Run `foreman start` to launch web server (WEBrick) and Logbot agent 25 | 7. Visit [http://localhost:5000](http://localhost:5000). 26 | 27 | 28 | How to Contribute 29 | ----------------- 30 | Just hack it and send me pull requests ;) 31 | 32 | 33 | Resource 34 | -------- 35 | * See [g0v/Logbot](https://github.com/g0v/Logbot) for many bugfixes and enhancements. 36 | 37 | 38 | License 39 | ------- 40 | Licensed under the [MIT license](http://opensource.org/licenses/mit-license.php). 41 | 42 | Copyright (c) 2013 Shao-Chung Chen 43 | 44 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 45 | 46 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 47 | 48 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 49 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | Encoding.default_internal = "utf-8" 3 | Encoding.default_external = "utf-8" 4 | 5 | require "json" 6 | require "time" 7 | require "date" 8 | require "cgi" 9 | require "sinatra/base" 10 | require "sinatra/async" 11 | require "redis" 12 | require "compass" 13 | require "eventmachine" 14 | 15 | $redis = Redis.new(:thread_safe => true) 16 | 17 | module IRC_Log 18 | class App < Sinatra::Base 19 | configure do 20 | set :protection, :except => :frame_options 21 | end 22 | 23 | get "/" do 24 | redirect "/channel/g0v.tw/today" 25 | end 26 | 27 | get "/channel/:channel" do |channel| 28 | redirect "/channel/#{channel}/today" 29 | end 30 | 31 | get "/channel/:channel/:date" do |channel, date| 32 | case date 33 | when "today" 34 | @date = Time.now.strftime("%F") 35 | when "yesterday" 36 | @date = (Time.now - 86400).strftime("%F") 37 | else 38 | # date in "%Y-%m-%d" format (e.g. 2013-01-01) 39 | @date = date 40 | end 41 | 42 | @channel = channel 43 | 44 | @msgs = $redis.lrange("irclog:channel:##{channel}:#{@date}", 0, -1) 45 | @msgs = @msgs.map {|msg| 46 | msg = JSON.parse(msg) 47 | msg["msg"] = CGI.escapeHTML(msg["msg"]) 48 | if msg["msg"] =~ /^\u0001ACTION (.*)\u0001$/ 49 | msg["msg"].gsub!(/^\u0001ACTION (.*)\u0001$/, "#{msg["nick"]} \\1") 50 | msg["nick"] = "*" 51 | end 52 | msg 53 | } 54 | 55 | erb :channel 56 | end 57 | 58 | get "/widget/:channel" do |channel| 59 | @channel = channel 60 | today = Time.now.strftime("%Y-%m-%d") 61 | @msgs = $redis.lrange("irclog:channel:##{channel}:#{today}", -25, -1) 62 | @msgs = $redis.lrange("irclog:channel:##{channel}:#{today}", -25, -1) 63 | @msgs = @msgs.map {|msg| 64 | ret = JSON.parse(msg) 65 | ret["msg"] = CGI.escape(ret["msg"]) 66 | ret 67 | }.reverse 68 | 69 | erb :widget 70 | end 71 | end 72 | end 73 | 74 | 75 | module Comet 76 | class App < Sinatra::Base 77 | register Sinatra::Async 78 | 79 | get %r{/poll/(.*)/([\d\.]+)/updates.json} do |channel, time| 80 | date = Time.at(time.to_f).strftime("%Y-%m-%d") 81 | msgs = $redis.lrange("irclog:channel:##{channel}:#{date}", -10, -1).map{|msg| 82 | ret = ::JSON.parse(msg) 83 | ret["msg"] = CGI.escapeHTML(ret["msg"]) 84 | ret 85 | } 86 | if (not msgs.empty?) && msgs[-1]["time"] > time 87 | return msgs.select{|msg| msg["time"] > time }.to_json 88 | end 89 | 90 | EventMachine.run do 91 | n, timer = 0, EventMachine::PeriodicTimer.new(0.5) do 92 | msgs = $redis.lrange("irclog:channel:##{channel}:#{date}", -10, -1).map{|msg| 93 | ret = ::JSON.parse(msg) 94 | ret["msg"] = CGI.escapeHTML(ret["msg"]) 95 | ret 96 | } 97 | if (not msgs.empty?) && msgs[-1]["time"] > time || n > 120 98 | timer.cancel 99 | return msgs.select{|msg| msg["time"] > time }.to_json 100 | end 101 | n += 1 102 | end 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /config.rb: -------------------------------------------------------------------------------- 1 | http_path = "/" 2 | css_dir = "public" 3 | sass_dir = "sass" 4 | output_style = :compressed 5 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # Process.setrlimit(Process::RLIMIT_NOFILE, 4096, 65536) 2 | require File.join(File.dirname(__FILE__), "app") 3 | 4 | run Rack::URLMap.new \ 5 | "/" => IRC_Log::App.new, 6 | "/comet" => Comet::App.new, 7 | "/assets" => Rack::Directory.new("public") 8 | -------------------------------------------------------------------------------- /logbot.god: -------------------------------------------------------------------------------- 1 | God.watch do |w| 2 | w.name = "Logbot agent" 3 | w.start = "ruby /home/rails/logbot/logbot.rb" 4 | w.keepalive 5 | end 6 | -------------------------------------------------------------------------------- /logbot.rb.example: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "cinch" 3 | require "redis" 4 | 5 | channels = (ENV['LOGBOT_CHANNELS'] || '#test56').split /[\s,]+/ 6 | redis = Redis.new(:thread_safe => true) 7 | 8 | channels.each do |chan| 9 | redis.sadd("irclog:channels", "#{chan}") 10 | end 11 | 12 | bot = Cinch::Bot.new do 13 | configure do |conf| 14 | conf.server = (ENV['LOGBOT_SERVER'] || "irc.freenode.net") 15 | conf.nick = (ENV['LOGBOT_NICK'] || "logbot_") 16 | conf.channels = channels 17 | end 18 | 19 | on :message do |msg| 20 | if not msg.channel.nil? 21 | date = msg.time.strftime("%Y-%m-%d") 22 | key = "irclog:channel:#{msg.channel.name}:#{date}" 23 | redis.rpush(key, { 24 | :time => "#{msg.time.strftime("%s.%L")}", 25 | :nick => "#{msg.user.nick}", 26 | :msg => "#{msg.message}" 27 | }.to_json) 28 | end 29 | end 30 | end 31 | bot.start 32 | -------------------------------------------------------------------------------- /public/applications.js: -------------------------------------------------------------------------------- 1 | var strftime = function(date) { 2 | var hour = date.getHours(), 3 | min = date.getMinutes(), 4 | sec = date.getSeconds(); 5 | 6 | if (hour < 10) { hour = "0" + hour; } 7 | if ( min < 10) { min = "0" + min; } 8 | if ( sec < 10) { sec = "0" + sec; } 9 | 10 | return hour + ":" + min + ":" + sec; 11 | }; 12 | 13 | 14 | var lastTimestamp = undefined; 15 | var seenTimestamp = {}; 16 | var pollNewMsg = function(isWidget) { 17 | var isWidget = (isWidget == null) ? false : isWidget; 18 | var time = lastTimestamp || (new Date()).getTime() / 1000.0; 19 | $.ajax({ 20 | url: "/comet/poll/" + channel + "/" + time + "/updates.json", 21 | type: "get", 22 | async: true, 23 | cache: false, 24 | timeout: 60000, 25 | 26 | success: function (data) { 27 | var msgs = JSON.parse(data); 28 | for (var i = 0; i < msgs.length; i++) { 29 | var msg = msgs[i]; 30 | if (seenTimestamp[msg.time]) { continue; } 31 | seenTimestamp[msg.time] = true; 32 | var date = new Date(parseFloat(msg["time"]) * 1000); 33 | var linkedMsg = msg["msg"].replace(/(http[s]*:\/\/[^\s]+)/, '$1'); 34 | var msgElement = $("