From e8fd3963e22493001aea19edd3d4b3995ccafa66 Mon Sep 17 00:00:00 2001 From: Vincent Batts Date: Wed, 17 Jul 2013 22:26:55 -0400 Subject: [PATCH] it works! --- README.md | 22 +++ noti/main.go | 78 ++++++++++ zmq_notify.rb | 402 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 502 insertions(+) create mode 100644 noti/main.go create mode 100644 zmq_notify.rb diff --git a/README.md b/README.md index d1f20ac..b9b96fa 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,25 @@ flaming-happiness ================= Golang + ZeroMQ + libnotify tool to connect to a zmq_notify.rb weechat script + + +Install +------- + + go get github.com/vbatts/flaming-happiness/noti + +Running +------- + +First you'll need to add the zmq_notify.rb file to ~/.weechat/ruby/ +and load it from weechat (you can symlink it in the ./autoload/ directory +to have this script loaded when weechat launches) + + /ruby load zmq_notify.rb + +Then run `noti` against your site + + noti tcp://example.com:2428 + +Be sure you've allowed for any firewalling between noti and the zmq_notify + diff --git a/noti/main.go b/noti/main.go new file mode 100644 index 0000000..fedd313 --- /dev/null +++ b/noti/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "bytes" + "encoding/json" + notify "github.com/mqu/go-notify" + zmq "github.com/pebbe/zmq3" + "log" + "os" + "os/signal" + "time" +) + +type IrcNotify struct { + Highlight bool `json:"highlight"` + Type string `json:"type"` + Channel string `json:"channel"` + Message string `json:"message"` + Server string `json:"server"` + Date string `json:"date"` + Tags []string `json:"tags"` +} + +func init() { + notify.Init("IRC-noti") + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + for sig := range c { + // sig is a ^C, handle it + log.Printf("captured %v, stopping profiler and exiting..", sig) + notify.UnInit() + os.Exit(0) + } + }() +} + +const ( + DELAY = 3000 +) + +func main() { + subscriber, _ := zmq.NewSocket(zmq.SUB) + if len(os.Args) == 2 { + subscriber.Connect(os.Args[1]) + log.Printf("Connected to [%s]", os.Args[1]) + } else { + log.Fatalf("provide the zmq_notify publisher! like tcp://example.com:2428") + } + subscriber.SetSubscribe("") + for { + msg, err := subscriber.RecvMessage(0) + if err != nil { + break + } + go func() { + noti_msg := IrcNotify{} + json.Unmarshal(bytes.NewBufferString(msg[0]).Bytes(), ¬i_msg) + log.Printf("%#v", noti_msg) + hello := notify.NotificationNew(noti_msg.Server + "," + noti_msg.Channel, + noti_msg.Message, + "") + + if hello == nil { + log.Println("ERROR: Unable to create a new notification") + return + } + notify.NotificationSetTimeout(hello, DELAY) + + if e := notify.NotificationShow(hello); e != nil && len(e.Message()) > 0 { + log.Printf("ERROR: %s", e.Message()) + return + } + time.Sleep(DELAY * time.Second) + notify.NotificationClose(hello) + }() + } +} diff --git a/zmq_notify.rb b/zmq_notify.rb new file mode 100644 index 0000000..68f2b20 --- /dev/null +++ b/zmq_notify.rb @@ -0,0 +1,402 @@ +# vim: set noet nosta sw=4 ts=4 : +# +# Mahlon E. Smith +# http://www.martini.nu/ +# (See below for LICENSE.) +# +# JSON msg option added by vbatts@hashbangbash.com +# +# ZMQ Notify +# ---------- +# +# Catch private messages and highlights, relaying them onward to a +# ZeroMQ publisher, for subscriber consumption. +# +# Writing a client to pull messages off the queue and send them to +# growl/libnotify/dzen/sms/email/your-tv/whatever-your-heart-desires +# is left as an exercise to the reader, but it should be as trivial as +# receiving the message and unwrapping YAML. Fun! +# +# ctx = ZMQ::Context.new +# zmq = ctx.socket( ZMQ::SUB ) +# zmq.connect( "tcp://example.com:2428" ) +# zmq.setsockopt( ZMQ::SUBSCRIBE, '' ) +# +# loop do +# pp YAML.load( zmq.recv ) +# end +# +# +# +# Install instructions: +# --------------------- +# +# This script requires the "zmq" ruby module, available +# from rubygems, and of course, your Weechat to be built with +# ruby. +# +# Load into Weechat like any other plugin, after putting it into +# your ~/.weechat/ruby directory: +# +# /ruby load zmq_notify.rb +# +# Options: +# -------- +# +# plugins.var.ruby.zmq_notify.endpoint +# +# The ZMQ connection endpoint. The socket type is always PUB. +# Default: tcp://*:2428 +# +# plugins.var.ruby.zmq_notify.ignore_tags +# +# A comma separated list of message types to ignore +# completely, regardless of away state. +# Default: "irc_quit" +# +# plugins.var.ruby.zmq_notify.enabled +# +# A global on/off toggle. +# Default: "off" +# +# plugins.var.ruby.zmq_notify.only_when_away +# +# Only relay messages to the ZMQ socket if you are set to /away. +# Default: "on" +# +# plugins.var.ruby.zmq_notify.use_json +# +# Send the messages as JSON instead of YAML +# Default: "on" +# +# +# ZMQ message payload +# ------------------- +# +# Highlighted message: +# +# {:type=>"channel", +# :highlight=>true, +# :message=>"Something said in #ruby-lang on my highlight list!", +# :away=>false, +# :channel=>"#ruby-lang", +# :server=>"freenode", +# :date=>"1294733587", +# :tags=>["irc_privmsg", "notify_message", "log1"]} +# +# Private message: +# +# {:type=>"private", +# :highlight=>false, +# :message=>"Here we go, yo. So what's the scenario?", +# :away=>false, +# :channel=>"grangeromatic", +# :server=>"bitlbee", +# :date=>"1294733597", +# :tags=>["irc_privmsg", "notify_private", "log1"]} +# + + +### Convenience 'truth' module for Weechat config strings, because: +### +### self.enabled.true? +### +### reads a whole lot nicer than: +### +### Weechat.config_string_to_boolean(Weechat.config_get_plugin('enabled')) == "1" +### +### I resist the temptation to monkeypatch all of String during my +### time with Weechat. Heh. +### +module Truthy + def true? + return Weechat.config_string_to_boolean( self.to_s ).to_i.zero? ? false : true + end +end + + +### The actual Weechat plugin. +### +class ZMQNotify + include Weechat + + DEBUG = false + + SIGNATURE = [ + 'zmq_notify', + 'Mahlon E. Smith', + '0.1', + 'BSD', + 'Send private messages and highlights to a ZMQ socket.', + 'weechat_unload', + 'UTF-8' + ] + + DEFAULT_OPTIONS = { + :endpoint => 'tcp://*:2428', + :ignore_tags => 'irc_quit', + :enabled => 'off', + :only_when_away => 'on', + :use_json => 'on' + } + + + ### Prepare configuration and bind a ZMQ endpoint. + ### + def initialize + + @zmq = @ctx = nil + + DEFAULT_OPTIONS.each_pair do |option, value| + + # install default options if needed. + # + if Weechat.config_is_set_plugin( option.to_s ).zero? + self.print_info "Setting value '%s' to %p" % [ option, value ] if DEBUG + Weechat.config_set_plugin( option.to_s, value.to_s ) + end + + # read in existing config values, attaching + # them to instance variables. + # + val = Weechat.config_get_plugin( option.to_s ) + val.extend( Truthy ) + instance_variable_set( "@#{option}".to_sym, val ) + self.class.send( :attr, option.to_sym, true ) + end + + self.bind + self.print_info "Initalized!" + end + + # The ZMQ socket and context. + # + attr :zmq, true + attr :ctx, true + + + ######################################################################## + ### W E E C H A T H O O K S + ######################################################################## + + ### Validate values for config changes, and take appropriate action + ### on any changes that immediately require it. + ### + def config_changed( data, option, new_value ) + option = option.match( /\.(\w+)$/ )[1] + bounce_connection = false + new_value.extend( Truthy ) + + case option + + # reset the connection if needed + # + when 'endpoint' + instance_variable_set( "@#{option}".to_sym, new_value ) + bounce_connection = true + + # Disconnect/reconnect to endpoint + # + when 'enabled' + self.enabled = new_value + new_value.true? ? self.bind : self.unbind + + # ... just change the setting, no validation/action needed. + else + instance_variable_set( "@#{option}".to_sym, new_value ) + end + + # Refresh the endpoint connection. + # + if bounce_connection + self.unbind + self.bind + end + + return WEECHAT_RC_OK + end + + + ### Process all incoming messages, filtering out anything we're not + ### interested in seeing. + ### + def notify_msg( data, buffer, date, tags, visible, highlight, prefix, message ) + + return WEECHAT_RC_OK unless self.enabled.true? + + # Grab the channel metadata. + data = {} + %w[ away type channel server ].each do |meta| + data[ meta.to_sym ] = Weechat.buffer_get_string( buffer, "localvar_#{meta}" ); + end + data[ :away ] = data[ :away ].empty? ? false : true + + # Are we currently marked as away? + return WEECHAT_RC_OK if self.only_when_away.true? && ! data[ :away ] + + # Only bother with the message if it is a highlight, or a private message. + return WEECHAT_RC_OK if highlight.to_i.zero? && data[ :type ] != 'private' + + # Are we specifically ignoring this message tag type? + # + ignored = self.ignore_tags.split( ',' ) + tags = tags.split( ',' ) + return WEECHAT_RC_OK unless ( ignored & tags ).empty? + + notify = { + :highlight => ! highlight.to_i.zero?, + :type => data[ :type ], + :channel => data[ :channel ], + :away => data[ :away ], + :server => data[ :server ], + :date => date, + :tags => tags, + :message => message + } + + # Ship it off. + # + self.print_info "Message notification: %p" % [ notify ] if DEBUG + if self.use_json.true? + self.zmq.send( notify.to_json ) + else + self.zmq.send( notify.to_yaml ) + end + + return WEECHAT_RC_OK + + rescue => err + self.disable "%s, %s" % [ err.class.name, err.message ] + return WEECHAT_RC_OK + end + + + ######################################################################## + ### I N S T A N C E M E T H O D S + ######################################################################## + + ### Instantiate a ZMQ endpoint. + ### + def bind + return unless self.enabled.true? + + self.print_info "Setting up endpoint at %s" % [ self.endpoint ] + + self.ctx = ZMQ::Context.new if self.ctx.nil? + self.zmq = self.ctx.socket( ZMQ::PUB ) + self.zmq.bind( self.endpoint ) + + rescue => err + self.print_info "Unable to create endpoint: %s, %s" % [ err.class.name, err.message ] + self.zmq = nil + end + + + ### Tear down the ZMQ endpoint. + ### + def unbind + return if self.zmq.nil? + self.zmq.close + end + + + ### Disable the plugin on repeated error. + ### TODO: Set a timer to attempt a re-connect? + ### + def disable( reason ) + self.print_info "Disabling plugin due to error: %s" % [ reason ] + Weechat.config_set_plugin( 'enabled', 'off' ) + end + + + + ######### + protected + ######### + + ### Quick wrapper for sending info messages to the weechat main buffer. + ### + def print_info( msg ) + Weechat.print '', "%sZMQ\t%s" % [ + Weechat.color('yellow'), + msg + ] + end +end + + + +### Weechat entry point. +### +def weechat_init + require 'rubygems' + require 'zmq' + require 'yaml' + require 'json' + + Weechat::register *ZMQNotify::SIGNATURE + $zmq = ZMQNotify.new + Weechat.hook_print( '', '', '', 1, 'notify_msg', '' ) + Weechat.hook_config( 'plugins.var.ruby.zmq_notify.*', 'config_changed', '' ) + + return Weechat::WEECHAT_RC_OK + +rescue LoadError => err + Weechat.print '', "zmq_notify: %s, %s\n$LOAD_PATH: %p" % [ + err.class.name, + err.message, + $LOAD_PATH + ] + Weechat.print '', 'zmq_notify: Unable to initialize due to missing dependencies.' + return Weechat::WEECHAT_RC_ERROR +end + + +### Hook for manually unloading this script. +### +def weechat_unload + $zmq.unbind + return Weechat::WEECHAT_RC_OK +end + + +### Allow Weechat namespace callbacks to forward to the ZMQNotify object. +### +require 'forwardable' +extend Forwardable +def_delegators :$zmq, :notify_msg, :config_changed + + +__END__ +__LICENSE__ + +Copyright (c) 2011, Mahlon E. Smith + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + * Neither the name of the author, nor the names of contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +