degesch: add the first Lua plugin to distribution
This required separate plugin directories for both pluginized executables.
This commit is contained in:
128
plugins/zyklonb/coin
Executable file
128
plugins/zyklonb/coin
Executable file
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env tclsh
|
||||
#
|
||||
# ZyklonB coin plugin, random number-based utilities
|
||||
#
|
||||
# Copyright 2012, 2014 Přemysl Janouch
|
||||
# See the file LICENSE for licensing information.
|
||||
#
|
||||
|
||||
# This is a terrible excuse for a programming language and I feel dirty.
|
||||
|
||||
proc parse {line} {
|
||||
global msg
|
||||
unset -nocomplain msg
|
||||
|
||||
if [regexp {^:([^ ]*) *(.*)} $line -> prefix rest] {
|
||||
set msg(prefix) $prefix
|
||||
set line $rest
|
||||
}
|
||||
if [regexp {^([^ ]*) *(.*)} $line -> command rest] {
|
||||
set msg(command) $command
|
||||
set line $rest
|
||||
}
|
||||
while {1} {
|
||||
set line [string trimleft $line " "]
|
||||
set i [string first " " $line]
|
||||
if {$i == -1} { set i [string length $line] }
|
||||
if {$i == 0} { break }
|
||||
|
||||
if {[string index $line 0] == ":"} {
|
||||
lappend msg(param) [string range $line 1 end]
|
||||
break
|
||||
}
|
||||
lappend msg(param) [string range $line 0 [expr $i - 1]]
|
||||
set line [string range $line $i end]
|
||||
}
|
||||
}
|
||||
|
||||
proc get_config {key} {
|
||||
global msg
|
||||
puts "ZYKLONB get_config :$key"
|
||||
gets stdin line
|
||||
parse $line
|
||||
return [lindex $msg(param) 0]
|
||||
}
|
||||
|
||||
proc pmrespond {text} {
|
||||
global ctx
|
||||
global ctx_quote
|
||||
puts "PRIVMSG $ctx :$ctx_quote$text"
|
||||
}
|
||||
|
||||
fconfigure stdin -translation crlf -encoding iso8859-1
|
||||
fconfigure stdout -translation crlf -encoding iso8859-1
|
||||
|
||||
set prefix [get_config prefix]
|
||||
puts "ZYKLONB register"
|
||||
|
||||
set eightball [list \
|
||||
"It is certain" \
|
||||
"It is decidedly so" \
|
||||
"Without a doubt" \
|
||||
"Yes - definitely" \
|
||||
"You may rely on it" \
|
||||
"As I see it, yes" \
|
||||
"Most likely" \
|
||||
"Outlook good" \
|
||||
"Yes" \
|
||||
"Signs point to yes" \
|
||||
"Reply hazy, try again" \
|
||||
"Ask again later" \
|
||||
"Better not tell you now" \
|
||||
"Cannot predict now" \
|
||||
"Concentrate and ask again" \
|
||||
"Don't count on it" \
|
||||
"My reply is no" \
|
||||
"My sources say no" \
|
||||
"Outlook not so good" \
|
||||
"Very doubtful"]
|
||||
|
||||
while {[gets stdin line] != -1} {
|
||||
parse $line
|
||||
|
||||
if {! [info exists msg(prefix)] || ! [info exists msg(command)]
|
||||
|| $msg(command) != "PRIVMSG" || ! [info exists msg(param)]
|
||||
|| [llength $msg(param)] < 2} { continue }
|
||||
|
||||
regexp {^[^!]*} $msg(prefix) ctx
|
||||
if [regexp {^[#&+!]} [lindex $msg(param) 0]] {
|
||||
set ctx_quote "$ctx: "
|
||||
set ctx [lindex $msg(param) 0]
|
||||
} else { set ctx_quote "" }
|
||||
|
||||
set input [lindex $msg(param) 1]
|
||||
set first_chars [string range $input 0 \
|
||||
[expr [string length $prefix] - 1]]
|
||||
if {$first_chars != $prefix} { continue }
|
||||
set input [string range $input [string length $prefix] end]
|
||||
|
||||
if {$input == "coin"} {
|
||||
if {rand() < 0.5} {
|
||||
pmrespond "Heads."
|
||||
} else {
|
||||
pmrespond "Tails."
|
||||
}
|
||||
} elseif {[regexp {^dice( +|$)(.*)} $input -> _ args]} {
|
||||
if {! [string is integer -strict $args] || $args <= 0} {
|
||||
pmrespond "Invalid or missing number."
|
||||
} else {
|
||||
pmrespond [expr {int($args * rand()) + 1}]
|
||||
}
|
||||
} elseif {[regexp {^(choose|\?)( +|$)(.*)} $input -> _ _ args]} {
|
||||
if {$args == ""} {
|
||||
pmrespond "Nothing to choose from."
|
||||
} else {
|
||||
set c [split $args ",|"]
|
||||
pmrespond [string trim [lindex $c \
|
||||
[expr {int([llength $c] * rand())}]]]
|
||||
}
|
||||
} elseif {[regexp {^eightball( +|$)(.*)} $input -> _ args]} {
|
||||
if {$args == ""} {
|
||||
pmrespond "You should, you know, ask something."
|
||||
} else {
|
||||
pmrespond [lindex $eightball \
|
||||
[expr {int([llength $eightball] * rand())}]].
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
312
plugins/zyklonb/eval
Executable file
312
plugins/zyklonb/eval
Executable file
@@ -0,0 +1,312 @@
|
||||
#!/usr/bin/awk -f
|
||||
#
|
||||
# ZyklonB eval plugin, LISP-like expression evaluator
|
||||
#
|
||||
# Copyright 2013, 2014 Přemysl Janouch
|
||||
# See the file LICENSE for licensing information.
|
||||
#
|
||||
|
||||
BEGIN \
|
||||
{
|
||||
RS = "\r"
|
||||
ORS = "\r\n"
|
||||
IGNORECASE = 1
|
||||
srand()
|
||||
|
||||
prefix = get_config("prefix")
|
||||
|
||||
print "ZYKLONB register"
|
||||
fflush("")
|
||||
|
||||
# All functions have to be in this particular array
|
||||
min_args["int"] = 1
|
||||
min_args["+"] = 1
|
||||
min_args["-"] = 1
|
||||
min_args["*"] = 1
|
||||
min_args["/"] = 1
|
||||
min_args["%"] = 1
|
||||
min_args["^"] = 1
|
||||
min_args["**"] = 1
|
||||
min_args["exp"] = 1
|
||||
min_args["sin"] = 1
|
||||
min_args["cos"] = 1
|
||||
min_args["atan2"] = 2
|
||||
min_args["log"] = 1
|
||||
min_args["rand"] = 0
|
||||
min_args["sqrt"] = 1
|
||||
|
||||
min_args["pi"] = 0
|
||||
min_args["e"] = 0
|
||||
|
||||
min_args["min"] = 1
|
||||
min_args["max"] = 1
|
||||
|
||||
# Whereas here their presence is only optional
|
||||
max_args["int"] = 1
|
||||
max_args["sin"] = 1
|
||||
max_args["cos"] = 1
|
||||
max_args["atan2"] = 2
|
||||
max_args["log"] = 1
|
||||
max_args["rand"] = 0
|
||||
max_args["sqrt"] = 1
|
||||
|
||||
max_args["pi"] = 0
|
||||
max_args["e"] = 0
|
||||
}
|
||||
|
||||
{
|
||||
parse($0)
|
||||
}
|
||||
|
||||
msg_command == "PRIVMSG" \
|
||||
{
|
||||
# Context = either channel or user nickname
|
||||
match(msg_prefix, /^[^!]+/)
|
||||
ctx = substr(msg_prefix, RSTART, RLENGTH)
|
||||
if (msg_param[0] ~ /^[#&!+]/)
|
||||
{
|
||||
ctx_quote = ctx ": "
|
||||
ctx = msg_param[0]
|
||||
}
|
||||
else
|
||||
ctx_quote = ""
|
||||
|
||||
|
||||
if (substr(msg_param[1], 1, length(prefix)) == prefix)
|
||||
{
|
||||
keyword = "eval"
|
||||
text = substr(msg_param[1], 1 + length(prefix))
|
||||
if (match(text, "^" keyword "([^A-Za-z0-9].*|$)"))
|
||||
process_request(substr(text, 1 + length(keyword)))
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
fflush("")
|
||||
}
|
||||
|
||||
function pmrespond (text)
|
||||
{
|
||||
print "PRIVMSG " ctx " :" ctx_quote text
|
||||
}
|
||||
|
||||
function process_request (input, res, x)
|
||||
{
|
||||
delete funs
|
||||
delete accumulator
|
||||
delete n_args
|
||||
|
||||
res = ""
|
||||
fun_top = 0
|
||||
funs[0] = ""
|
||||
accumulator[0] = 0
|
||||
n_args[0] = 0
|
||||
|
||||
if (match(input, "^[ \t]*"))
|
||||
input = substr(input, RLENGTH + 1)
|
||||
if (input == "")
|
||||
res = "expression missing"
|
||||
|
||||
while (res == "" && input != "") {
|
||||
if (match(input, "^-?[0-9]+\\.?[0-9]*")) {
|
||||
x = substr(input, RSTART, RLENGTH)
|
||||
input = substr(input, RLENGTH + 1)
|
||||
|
||||
match(input, "^ *")
|
||||
input = substr(input, RLENGTH + 1)
|
||||
|
||||
res = process_argument(x)
|
||||
} else if (match(input, "^[(]([^ ()]+)")) {
|
||||
x = substr(input, RSTART + 1, RLENGTH - 1)
|
||||
input = substr(input, RLENGTH + 1)
|
||||
|
||||
match(input, "^ *")
|
||||
input = substr(input, RLENGTH + 1)
|
||||
|
||||
if (!(x in min_args)) {
|
||||
res = "undefined function '" x "'"
|
||||
} else {
|
||||
fun_top++
|
||||
funs[fun_top] = x
|
||||
accumulator[fun_top] = 636363
|
||||
n_args[fun_top] = 0
|
||||
}
|
||||
} else if (match(input, "^[)] *")) {
|
||||
input = substr(input, RLENGTH + 1)
|
||||
res = process_end()
|
||||
} else
|
||||
res = "invalid input at '" substr(input, 1, 10) "...'"
|
||||
}
|
||||
|
||||
if (res == "") {
|
||||
if (fun_top != 0)
|
||||
res = "unclosed '" funs[fun_top] "'"
|
||||
else if (n_args[0] != 1)
|
||||
res = "internal error, expected one result" \
|
||||
", got " n_args[0] " instead"
|
||||
}
|
||||
|
||||
if (res == "")
|
||||
pmrespond(accumulator[0])
|
||||
else
|
||||
pmrespond(res)
|
||||
}
|
||||
|
||||
function process_argument (arg)
|
||||
{
|
||||
if (fun_top == 0) {
|
||||
if (n_args[0]++ != 0)
|
||||
return "too many results, I only expect one"
|
||||
|
||||
accumulator[0] = arg
|
||||
return ""
|
||||
}
|
||||
|
||||
fun = funs[fun_top]
|
||||
if (fun in max_args && max_args[fun] <= n_args[fun_top])
|
||||
return "too many operands for " fun
|
||||
|
||||
if (fun == "int") {
|
||||
accumulator[fun_top] = int(arg)
|
||||
} else if (fun == "+") {
|
||||
if (n_args[fun_top] == 0)
|
||||
accumulator[fun_top] = arg
|
||||
else
|
||||
accumulator[fun_top] += arg
|
||||
} else if (fun == "-") {
|
||||
if (n_args[fun_top] == 0)
|
||||
accumulator[fun_top] = arg
|
||||
else
|
||||
accumulator[fun_top] -= arg
|
||||
} else if (fun == "*") {
|
||||
if (n_args[fun_top] == 0)
|
||||
accumulator[fun_top] = arg
|
||||
else
|
||||
accumulator[fun_top] *= arg
|
||||
} else if (fun == "/") {
|
||||
if (n_args[fun_top] == 0)
|
||||
accumulator[fun_top] = arg
|
||||
else if (arg == 0)
|
||||
return "division by zero"
|
||||
else
|
||||
accumulator[fun_top] /= arg
|
||||
} else if (fun == "%") {
|
||||
if (n_args[fun_top] == 0)
|
||||
accumulator[fun_top] = arg
|
||||
else if (arg == 0)
|
||||
return "division by zero"
|
||||
else
|
||||
accumulator[fun_top] %= arg
|
||||
} else if (fun == "^" || fun == "**" || fun == "exp") {
|
||||
if (n_args[fun_top] == 0)
|
||||
accumulator[fun_top] = arg
|
||||
else
|
||||
accumulator[fun_top] ^= arg
|
||||
} else if (fun == "sin") {
|
||||
accumulator[fun_top] = sin(arg)
|
||||
} else if (fun == "cos") {
|
||||
accumulator[fun_top] = cos(arg)
|
||||
} else if (fun == "atan2") {
|
||||
if (n_args[fun_top] == 0)
|
||||
accumulator[fun_top] = arg
|
||||
else
|
||||
accumulator[fun_top] = atan2(accumulator[fun_top], arg)
|
||||
} else if (fun == "log") {
|
||||
accumulator[fun_top] = log(arg)
|
||||
} else if (fun == "rand") {
|
||||
# Just for completeness, execution never gets here
|
||||
} else if (fun == "sqrt") {
|
||||
accumulator[fun_top] = sqrt(arg)
|
||||
} else if (fun == "min") {
|
||||
if (n_args[fun_top] == 0)
|
||||
accumulator[fun_top] = arg
|
||||
else if (accumulator[fun_top] > arg)
|
||||
accumulator[fun_top] = arg
|
||||
} else if (fun == "max") {
|
||||
if (n_args[fun_top] == 0)
|
||||
accumulator[fun_top] = arg
|
||||
else if (accumulator[fun_top] < arg)
|
||||
accumulator[fun_top] = arg
|
||||
} else
|
||||
return "internal error, unhandled operands for " fun
|
||||
|
||||
n_args[fun_top]++
|
||||
return ""
|
||||
}
|
||||
|
||||
function process_end ()
|
||||
{
|
||||
if (fun_top <= 0)
|
||||
return "extraneous ')'"
|
||||
|
||||
fun = funs[fun_top]
|
||||
if (!(fun in min_args))
|
||||
return "internal error, unhandled ')' for '" fun "'"
|
||||
if (min_args[fun] > n_args[fun_top])
|
||||
return "not enough operands for '" fun "'"
|
||||
|
||||
# There's no 'init' function to do it in
|
||||
if (fun == "rand")
|
||||
accumulator[fun_top] = rand()
|
||||
else if (fun == "pi")
|
||||
accumulator[fun_top] = 3.141592653589793
|
||||
else if (fun == "e")
|
||||
accumulator[fun_top] = 2.718281828459045
|
||||
|
||||
return process_argument(accumulator[fun_top--])
|
||||
}
|
||||
|
||||
function get_config (key)
|
||||
{
|
||||
print "ZYKLONB get_config :" key
|
||||
fflush("")
|
||||
|
||||
getline
|
||||
parse($0)
|
||||
return msg_param[0]
|
||||
}
|
||||
|
||||
function parse (line, s, n, id, token)
|
||||
{
|
||||
s = 1
|
||||
id = 0
|
||||
|
||||
# NAWK only uses the first character of RS
|
||||
if (line ~ /^\n/)
|
||||
line = substr(line, 2)
|
||||
|
||||
msg_prefix = ""
|
||||
msg_command = ""
|
||||
delete msg_param
|
||||
|
||||
n = match(substr(line, s), / |$/)
|
||||
while (n)
|
||||
{
|
||||
token = substr(line, s, n - 1)
|
||||
if (token ~ /^:/)
|
||||
{
|
||||
if (s == 1)
|
||||
msg_prefix = substr(token, 2)
|
||||
else
|
||||
{
|
||||
msg_param[id] = substr(line, s + 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
else if (!msg_command)
|
||||
msg_command = toupper(token)
|
||||
else
|
||||
msg_param[id++] = token
|
||||
|
||||
s = s + n
|
||||
n = index(substr(line, s), " ")
|
||||
|
||||
if (!n)
|
||||
{
|
||||
n = length(substr(line, s)) + 1
|
||||
if (n == 1)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
502
plugins/zyklonb/pomodoro
Executable file
502
plugins/zyklonb/pomodoro
Executable file
@@ -0,0 +1,502 @@
|
||||
#!/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 <nick> | 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 <nick>"
|
||||
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
|
||||
2310
plugins/zyklonb/script
Executable file
2310
plugins/zyklonb/script
Executable file
File diff suppressed because it is too large
Load Diff
111
plugins/zyklonb/youtube
Executable file
111
plugins/zyklonb/youtube
Executable file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# ZyklonB YouTube plugin, displaying info about YouTube links
|
||||
#
|
||||
# Copyright 2014 - 2015, Přemysl Janouch <p.janouch@gmail.com>
|
||||
# See the file LICENSE for licensing information.
|
||||
#
|
||||
|
||||
import sys
|
||||
import io
|
||||
import re
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
class Plugin:
|
||||
re_msg = re.compile ('(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?'
|
||||
'([^ ]+)(?: +(.*))?\r\n$')
|
||||
re_args = re.compile (':?((?<=:).*|[^ ]+) *')
|
||||
|
||||
def parse (self, line):
|
||||
m = self.re_msg.match (line)
|
||||
if m is None:
|
||||
return None
|
||||
|
||||
(nick, user, host, command, args) = m.groups ()
|
||||
args = [] if args is None else self.re_args.findall (args)
|
||||
return (nick, user, host, command, args)
|
||||
|
||||
def get_config (self, key):
|
||||
print ("ZYKLONB get_config :%s" % key)
|
||||
(_, _, _, _, args) = self.parse (sys.stdin.readline ())
|
||||
return args[0]
|
||||
|
||||
def bot_print (self, what):
|
||||
print ('ZYKLONB print :%s' % what)
|
||||
|
||||
class YouTube (Plugin):
|
||||
re_videos = [re.compile (x) for x in [
|
||||
r'youtube\.[a-z]+/[^ ]*[&?]v=([-\w]+)',
|
||||
r'youtube\.[a-z]+/v/([-\w]+)',
|
||||
r'youtu\.be/([-\w]+)'
|
||||
]]
|
||||
re_playlists = [re.compile (x) for x in [
|
||||
r'youtube\.[a-z]+/playlist[&?][^ ]*(?<=&|\?)list=([-\w]+)',
|
||||
]]
|
||||
|
||||
def print_info (self, channel, url, cb):
|
||||
try:
|
||||
data = json.loads (urllib.request.urlopen
|
||||
(url, None, 30).read ().decode ('utf-8'))
|
||||
|
||||
for line in map (lambda x: "YouTube: " + cb (x), data['items']):
|
||||
print ("PRIVMSG %s :%s" % (channel,
|
||||
line.encode ('utf-8').decode ('iso8859-1')))
|
||||
|
||||
except Exception as err:
|
||||
self.bot_print ('youtube: %s' % (err))
|
||||
|
||||
def print_video_info (self, channel, video_id):
|
||||
url = 'https://www.googleapis.com/youtube/v3/' \
|
||||
+ 'videos?id=%s&key=%s&part=snippet,contentDetails,statistics' \
|
||||
% (video_id, self.youtube_api_key)
|
||||
self.print_info (channel, url, lambda x: "%s | %s | %sx" % (
|
||||
x['snippet']['title'],
|
||||
x['contentDetails']['duration'][2:].lower (),
|
||||
x['statistics']['viewCount']))
|
||||
|
||||
def print_playlist_info (self, channel, playlist_id):
|
||||
url = 'https://www.googleapis.com/youtube/v3/' \
|
||||
+ 'playlists?id=%s&key=%s&part=snippet,contentDetails' \
|
||||
% (playlist_id, self.youtube_api_key)
|
||||
self.print_info (channel, url, lambda x: "%s | %d videos" % (
|
||||
x['snippet']['title'],
|
||||
x['contentDetails']['itemCount']))
|
||||
|
||||
def process_line (self, line):
|
||||
msg = self.parse (line)
|
||||
if msg is None:
|
||||
return
|
||||
|
||||
(nick, user, host, command, args) = msg
|
||||
if command != 'PRIVMSG' or len (args) < 2:
|
||||
return
|
||||
|
||||
ctx = args[0]
|
||||
if not ctx.startswith (('#', '+', '&', '!')):
|
||||
ctx = nick
|
||||
|
||||
for regex in self.re_videos:
|
||||
for i in regex.findall (args[1]):
|
||||
self.print_video_info (ctx, i)
|
||||
for regex in self.re_playlists:
|
||||
for i in regex.findall (args[1]):
|
||||
self.print_playlist_info (ctx, i)
|
||||
|
||||
def run (self):
|
||||
self.youtube_api_key = self.get_config ('youtube_api_key')
|
||||
if self.youtube_api_key == "":
|
||||
self.bot_print ("youtube: missing `youtube_api_key'")
|
||||
|
||||
print ("ZYKLONB register")
|
||||
|
||||
for line in sys.stdin:
|
||||
self.process_line (line)
|
||||
|
||||
sys.stdin = io.TextIOWrapper (sys.__stdin__.buffer,
|
||||
encoding = 'iso8859-1', newline = '\r\n', line_buffering = True)
|
||||
sys.stdout = io.TextIOWrapper (sys.__stdout__.buffer,
|
||||
encoding = 'iso8859-1', newline = '\r\n', line_buffering = True)
|
||||
|
||||
YouTube ().run ()
|
||||
Reference in New Issue
Block a user