From 71f3532e04e5c76327363a3fd36b506f54e5043d Mon Sep 17 00:00:00 2001 From: Přemysl Janouch Date: Sat, 21 Nov 2015 22:47:52 +0100 Subject: degesch: add the first Lua plugin to distribution This required separate plugin directories for both pluginized executables. --- plugins/pomodoro | 502 ------------------------------------------------------- 1 file changed, 502 deletions(-) delete mode 100755 plugins/pomodoro (limited to 'plugins/pomodoro') diff --git a/plugins/pomodoro b/plugins/pomodoro deleted file mode 100755 index 8f30327..0000000 --- a/plugins/pomodoro +++ /dev/null @@ -1,502 +0,0 @@ -#!/usr/bin/env ruby -# coding: utf-8 -# -# ZyklonB pomodoro plugin -# -# Copyright 2015 Přemysl Janouch -# See the file LICENSE for licensing information. -# - -# --- Simple event loop -------------------------------------------------------- - -# This is more or less a straight-forward port of my C event loop. It's a bit -# unfortunate that I really have to implement all this in order to get some -# basic asynchronicity but at least I get to exercise my Ruby. - -class TimerEvent - attr_accessor :index, :when, :callback - - def initialize (callback) - raise ArgumentError unless callback.is_a? Proc - - @index = nil - @when = nil - @callback = callback - end - - def active? - @index != nil - end - - def until - return @when - Time.new - end -end - -class IOEvent - READ = 1 << 0 - WRITE = 1 << 1 - - attr_accessor :read_index, :write_index, :io, :callback - - def initialize (io, callback) - raise ArgumentError unless callback.is_a? Proc - - @read_index = nil - @write_index = nil - @io = io - @callback = callback - end -end - -class EventLoop - def initialize - @running = false - @timers = [] - @readers = [] - @writers = [] - @io_to_event = {} - end - - def set_timer (timer, timeout) - raise ArgumentError unless timer.is_a? TimerEvent - - timer.when = Time.now + timeout - if timer.index - heapify_down timer.index - heapify_up timer.index - else - timer.index = @timers.size - @timers.push timer - heapify_up timer.index - end - end - - def reset_timer (timer) - raise ArgumentError unless timer.is_a? TimerEvent - remove_timer_at timer.index if timer.index - end - - def set_io (io_event, events) - raise ArgumentError unless io_event.is_a? IOEvent - raise ArgumentError unless events.is_a? Numeric - - reset_io io_event - - @io_to_event[io_event.io] = io_event - if events & IOEvent::READ - io_event.read_index = @readers.size - @readers.push io_event.io - end - if events & IOEvent::WRITE - io_event.read_index = @writers.size - @writers.push io_event.io - end - end - - def reset_io (io_event) - raise ArgumentError unless io_event.is_a? IOEvent - - @readers.delete_at io_event.read_index if io_event.read_index - @writers.delete_at io_event.write_index if io_event.write_index - - io_event.read_index = nil - io_event.write_index = nil - - @io_to_event.delete io_event.io - end - - def run - @running = true - while @running do one_iteration end - end - - def quit - @running = false - end - -private - def one_iteration - rs, ws, = IO.select @readers, @writers, [], nearest_timeout - dispatch_timers - (Array(rs) | Array(ws)).each do |io| - @io_to_event[io].callback.call io - end - end - - def dispatch_timers - now = Time.new - while not @timers.empty? and @timers[0].when <= now do - @timers[0].callback.call - remove_timer_at 0 - end - end - - def nearest_timeout - return nil if @timers.empty? - timeout = @timers[0].until - if timeout < 0 then 0 else timeout end - end - - def remove_timer_at (index) - @timers[index].index = nil - moved = @timers.pop - return if index == @timers.size - - @timers[index] = moved - @timers[index].index = index - heapify_down index - end - - def swap_timers (a, b) - @timers[a], @timers[b] = @timers[b], @timers[a] - @timers[a].index = a - @timers[b].index = b - end - - def heapify_up (index) - while index != 0 do - parent = (index - 1) / 2 - break if @timers[parent].when <= @timers[index].when - swap_timers index, parent - index = parent - end - end - - def heapify_down (index) - loop do - parent = index - left = 2 * index + 1 - right = 2 * index + 2 - - lowest = parent - lowest = left if left < @timers.size and - @timers[left] .when < @timers[lowest].when - lowest = right if right < @timers.size and - @timers[right].when < @timers[lowest].when - break if parent == lowest - - swap_timers lowest, parent - index = lowest - end - end -end - -# --- IRC protocol ------------------------------------------------------------- - -$stdin.set_encoding 'ASCII-8BIT' -$stdout.set_encoding 'ASCII-8BIT' - -$stdin.sync = true -$stdout.sync = true - -$/ = "\r\n" -$\ = "\r\n" - -RE_MSG = /(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(?: +(.*))?$/ -RE_ARGS = /:?((?<=:).*|[^ ]+) */ - -def parse (line) - m = line.match RE_MSG - return nil if not m - - nick, user, host, command, args = *m.captures - args = if args then args.scan(RE_ARGS).flatten else [] end - [nick, user, host, command, args] -end - -def bot_print (what) - print "ZYKLONB print :#{what}" -end - -# --- Initialization ----------------------------------------------------------- - -# We can only read in configuration from here so far -# To read it from anywhere else, it has to be done asynchronously -$config = {} -[:prefix].each do |name| - print "ZYKLONB get_config :#{name}" - _, _, _, _, args = *parse($stdin.gets.chomp) - $config[name] = args[0] -end - -print "ZYKLONB register" - -# --- Plugin logic ------------------------------------------------------------- - -# FIXME: this needs a major refactor as it doesn't make much sense at all - -class MessageMeta < Struct.new(:nick, :user, :host, :channel, :ctx, :quote) - def respond (message) - print "PRIVMSG #{ctx} :#{quote}#{message}" - end -end - -class Context - attr_accessor :nick, :ctx - - def initialize (meta) - @nick = meta.nick - @ctx = meta.ctx - end - - def == (other) - self.class == other.class \ - and other.nick == @nick \ - and other.ctx == @ctx - end - - alias eql? == - - def hash - @nick.hash ^ @ctx.hash - end -end - -class PomodoroTimer - def initialize (context) - @ctx = context.ctx - @nicks = [context.nick] - - @timer_work = TimerEvent.new(lambda { on_work }) - @timer_rest = TimerEvent.new(lambda { on_rest }) - - on_work - end - - def inform (message) - # FIXME: it tells the nick even in PM's - quote = "#{@nicks.join(" ")}: " - print "PRIVMSG #{@ctx} :#{quote}#{message}" - end - - def on_work - inform "work now!" - $loop.set_timer @timer_rest, 25 * 60 - end - - def on_rest - inform "rest now!" - $loop.set_timer @timer_work, 5 * 60 - end - - def join (meta) - return if @nicks.include? meta.nick - - meta.respond "you have joined their pomodoro" - @nicks |= [meta.nick] - end - - def part (meta, requested) - return if not @nicks.include? meta.nick - - if requested - meta.respond "you have stopped your pomodoro" - end - - @nicks -= [meta.nick] - if @nicks.empty? - $loop.reset_timer @timer_work - $loop.reset_timer @timer_rest - end - end - - def status (meta) - return if not @nicks.include? meta.nick - - if @timer_rest.active? - till = @timer_rest.until - meta.respond "working, #{(till / 60).to_i} minutes, " + - "#{(till % 60).to_i} seconds until rest" - end - if @timer_work.active? - till = @timer_work.until - meta.respond "resting, #{(till / 60).to_i} minutes, " + - "#{(till % 60).to_i} seconds until work" - end - end -end - -class Pomodoro - KEYWORD = "pomodoro" - - def initialize - @timers = {} - end - - def on_help (meta, args) - meta.respond "usage: #{KEYWORD} { start | stop | join | status }" - end - - def on_start (meta, args) - if args.size != 0 - meta.respond "usage: #{KEYWORD} start" - return - end - - context = Context.new meta - if @timers[context] - meta.respond "you already have a timer running here" - else - @timers[context] = PomodoroTimer.new meta - end - end - - def on_join (meta, args) - if args.size != 1 - meta.respond "usage: #{KEYWORD} join " - return - end - - context = Context.new meta - if @timers[context] - meta.respond "you already have a timer running here" - return - end - - joined_context = Context.new meta - joined_context.nick = args.shift - timer = @timers[joined_context] - if not timer - meta.respond "that person doesn't have a timer here" - else - timer.join meta - @timers[context] = timer - end - end - - def on_stop (meta, args) - if args.size != 0 - meta.respond "usage: #{KEYWORD} stop" - return - end - - context = Context.new meta - timer = @timers[context] - if not timer - meta.respond "you don't have a timer running here" - else - timer.part meta, true - @timers.delete context - end - end - - def on_status (meta, args) - if args.size != 0 - meta.respond "usage: #{KEYWORD} status" - return - end - - timer = @timers[Context.new meta] - if not timer - meta.respond "you don't have a timer running here" - else - timer.status meta - end - end - - def process_command (meta, msg) - args = msg.split - return if args.shift != KEYWORD - - method = "on_#{args.shift}" - send method, meta, args if respond_to? method - end - - def on_server_nick (meta, command, args) - # TODO: either handle this properly... - happened = false - @timers.keys.each do |key| - next if key.nick != meta.nick - @timers[key].part meta, false - @timers.delete key - happened = true - end - if happened - # TODO: ...or at least inform the user via his new nick - end - end - - def on_server_part (meta, command, args) - # TODO: instead of cancelling the user's pomodoros, either redirect - # them to PM's and later upon rejoining undo the redirection... - context = Context.new(meta) - context.ctx = meta.channel - if @timers.include? context - # TODO: ...or at least inform the user about the cancellation - @timers[context].part meta, false - @timers.delete context - end - end - - def on_server_quit (meta, command, args) - @timers.keys.each do |key| - next if key.nick != meta.nick - @timers[key].part meta, false - @timers.delete key - end - end - - def process (meta, command, args) - method = "on_server_#{command.downcase}" - send method, meta, command, args if respond_to? method - end -end - -# --- IRC message processing --------------------------------------------------- - -$handlers = [Pomodoro.new] -def process_line (line) - msg = parse line - return if not msg - - nick, user, host, command, args = *msg - - context = nick - quote = "" - channel = nil - - if args.size >= 1 and args[0].start_with? ?#, ?+, ?&, ?! - case command - when "PRIVMSG", "NOTICE", "JOIN" - context = args[0] - quote = "#{nick}: " - channel = args[0] - when "PART" - channel = args[0] - end - end - - # Handle any IRC message - meta = MessageMeta.new(nick, user, host, channel, context, quote).freeze - $handlers.each do |handler| - handler.process meta, command, args - end - - # Handle pre-processed bot commands - if command == 'PRIVMSG' and args.size >= 2 - msg = args[1] - return unless msg.start_with? $config[:prefix] - $handlers.each do |handler| - handler.process_command meta, msg[$config[:prefix].size..-1] - end - end -end - -buffer = "" -stdin_io = IOEvent.new($stdin, lambda do |io| - begin - buffer << io.read_nonblock(4096) - lines = buffer.split $/, -1 - buffer = lines.pop - lines.each { |line| process_line line } - rescue EOFError - $loop.quit - rescue IO::WaitReadable - # Ignore - end -end) - -$loop = EventLoop.new -$loop.set_io stdin_io, IOEvent::READ -$loop.run -- cgit v1.2.3-70-g09d2