Basic bot in a working state.

This commit is contained in:
Jinks 2015-10-21 07:05:29 +02:00
parent 47abf398a1
commit 6c4161f17a
15 changed files with 2710 additions and 1 deletions

View file

@ -1,2 +1,4 @@
# LuaMeat # LuaMeat
Lua IRC bot A simple, stupid IRC bot to provide some support functions for twitch channels.
Doesn't do much, yet.

20
config.lua.template Normal file
View file

@ -0,0 +1,20 @@
-- config file template - copy this to config.lua
-- IRC Config
network = "irc.twitch.tv" -- network to connect (my default for quakenet)
nick = "YourStupidBot"
defaulChannels = {"#YourStupidBot", "#otherchannel"} --channels to join on startup
username = "YourStupidBot"
realname = "Stupid Lua IRC bot for twitch"
password = "oauth:<oauth_key>"
-- Plugin configs
yt_api_key = "<APIKeyFromGoogle>"
---------------------------
-- Add plugins here!
---------------------------
require('plugin.twitch')
require('plugin.google')
require('plugin.calc')
require('plugin.youtube')

994
irc.lua Normal file
View file

@ -0,0 +1,994 @@
---
-- Implementation of the main LuaIRC module
-- initialization {{{
local base = _G
local constants = require 'irc.constants'
local ctcp = require 'irc.ctcp'
local c = ctcp._ctcp_quote
local irc_debug = require 'irc.debug'
local message = require 'irc.message'
local misc = require 'irc.misc'
local socket = require 'socket'
local os = require 'os'
local string = require 'string'
local table = require 'table'
-- }}}
---
-- LuaIRC - IRC framework written in Lua
-- @release 0.3
module 'irc'
-- constants {{{
_VERSION = 'LuaIRC 0.3'
-- }}}
-- classes {{{
local Channel = base.require 'irc.channel'
-- }}}
-- local variables {{{
local irc_sock = nil
local rsockets = {}
local wsockets = {}
local rcallbacks = {}
local wcallbacks = {}
local icallbacks = {
whois = {},
serverversion = {},
servertime = {},
ctcp_ping = {},
ctcp_time = {},
ctcp_version = {},
}
local requestinfo = {whois = {}}
local handlers = {}
local ctcp_handlers = {}
local user_handlers = {}
local serverinfo = {}
local ip = nil
-- }}}
-- defaults {{{
TIMEOUT = 60 -- connection timeout
NETWORK = "localhost" -- default network
PORT = 6667 -- default port
NICK = "luabot" -- default nick
USERNAME = "LuaIRC" -- default username
REALNAME = "LuaIRC" -- default realname
DEBUG = false -- whether we want extra debug information
OUTFILE = nil -- file to send debug output to - nil is stdout
-- }}}
-- private functions {{{
-- main_loop_iter {{{
local function main_loop_iter()
if #rsockets == 0 and #wsockets == 0 then return false end
local rready, wready, err = socket.select(rsockets, wsockets)
if err then irc_debug._err(err); return false; end
for _, sock in base.ipairs(rready) do
local cb = socket.protect(rcallbacks[sock])
local ret, err = cb(sock)
if not ret then
irc_debug._warn("socket error: " .. err)
_unregister_socket(sock, 'r')
end
end
for _, sock in base.ipairs(wready) do
local cb = socket.protect(wcallbacks[sock])
local ret, err = cb(sock)
if not ret then
irc_debug._warn("socket error: " .. err)
_unregister_socket(sock, 'w')
end
end
return true
end
-- }}}
-- begin_main_loop {{{
local function begin_main_loop()
while main_loop_iter() do end
end
-- }}}
-- incoming_message {{{
local function incoming_message(sock)
local raw_msg = socket.try(sock:receive())
irc_debug._message("RECV", raw_msg)
local msg = message._parse(raw_msg)
misc._try_call_warn("Unhandled server message: " .. msg.command,
handlers["on_" .. msg.command:lower()],
(misc._parse_user(msg.from)), base.unpack(msg.args))
return true
end
-- }}}
-- callback {{{
local function callback(name, ...)
return misc._try_call(user_handlers[name], ...)
end
-- }}}
-- }}}
-- internal message handlers {{{
-- command handlers {{{
-- on_nick {{{
function handlers.on_nick(from, new_nick)
for chan in channels() do
chan:_change_nick(from, new_nick)
end
callback("nick_change", new_nick, from)
end
-- }}}
-- on_join {{{
function handlers.on_join(from, chan)
base.assert(serverinfo.channels[chan],
"Received join message for unknown channel: " .. chan)
if serverinfo.channels[chan].join_complete then
serverinfo.channels[chan]:_add_user(from)
callback("join", serverinfo.channels[chan], from)
end
end
-- }}}
-- on_part {{{
function handlers.on_part(from, chan, part_msg)
-- don't assert on chan here, since we get part messages for ourselves
-- after we remove the channel from the channel list
if not serverinfo.channels[chan] then return end
if serverinfo.channels[chan].join_complete then
serverinfo.channels[chan]:_remove_user(from)
callback("part", serverinfo.channels[chan], from, part_msg)
end
end
-- }}}
-- on_mode {{{
function handlers.on_mode(from, to, mode_string, ...)
local dir = mode_string:sub(1, 1)
mode_string = mode_string:sub(2)
local args = {...}
if to:sub(1, 1) == "#" then
-- handle channel mode requests {{{
base.assert(serverinfo.channels[to],
"Received mode change for unknown channel: " .. to)
local chan = serverinfo.channels[to]
local ind = 1
for i = 1, mode_string:len() do
local mode = mode_string:sub(i, i)
local target = args[ind]
-- channel modes other than op/voice will be implemented as
-- information request commands
if mode == "o" then -- channel op {{{
chan:_change_status(target, dir == "+", "o")
callback(({["+"] = "op", ["-"] = "deop"})[dir],
chan, from, target)
ind = ind + 1
-- }}}
elseif mode == "v" then -- voice {{{
chan:_change_status(target, dir == "+", "v")
callback(({["+"] = "voice", ["-"] = "devoice"})[dir],
chan, from, target)
ind = ind + 1
-- }}}
end
end
-- }}}
elseif from == to then
-- handle user mode requests {{{
-- TODO: make users more easily accessible so this is actually
-- reasonably possible
for i = 1, mode_string:len() do
local mode = mode_string:sub(i, i)
if mode == "i" then -- invisible {{{
-- }}}
elseif mode == "s" then -- server messages {{{
-- }}}
elseif mode == "w" then -- wallops messages {{{
-- }}}
elseif mode == "o" then -- ircop {{{
-- }}}
end
end
-- }}}
end
end
-- }}}
-- on_topic {{{
function handlers.on_topic(from, chan, new_topic)
base.assert(serverinfo.channels[chan],
"Received topic message for unknown channel: " .. chan)
serverinfo.channels[chan]._topic.text = new_topic
serverinfo.channels[chan]._topic.user = from
serverinfo.channels[chan]._topic.time = os.time()
if serverinfo.channels[chan].join_complete then
callback("topic_change", serverinfo.channels[chan])
end
end
-- }}}
-- on_invite {{{
function handlers.on_invite(from, to, chan)
callback("invite", from, chan)
end
-- }}}
-- on_kick {{{
function handlers.on_kick(from, chan, to)
base.assert(serverinfo.channels[chan],
"Received kick message for unknown channel: " .. chan)
if serverinfo.channels[chan].join_complete then
serverinfo.channels[chan]:_remove_user(to)
callback("kick", serverinfo.channels[chan], to, from)
end
end
-- }}}
-- on_privmsg {{{
function handlers.on_privmsg(from, to, msg)
local msgs = ctcp._ctcp_split(msg)
for _, v in base.ipairs(msgs) do
local msg = v.str
if v.ctcp then
-- ctcp message {{{
local words = misc._split(msg)
local received_command = words[1]
local cb = "on_" .. received_command:lower()
table.remove(words, 1)
-- not using try_call here because the ctcp specification requires
-- an error response to nonexistant commands
if base.type(ctcp_handlers[cb]) == "function" then
ctcp_handlers[cb](from, to, table.concat(words, " "))
else
notice(from, c("ERRMSG", received_command, ":Unknown query"))
end
-- }}}
else
-- normal message {{{
if to:sub(1, 1) == "#" then
base.assert(serverinfo.channels[to],
"Received channel msg from unknown channel: " .. to)
callback("channel_msg", serverinfo.channels[to], from, msg)
else
callback("private_msg", from, msg)
end
-- }}}
end
end
end
-- }}}
-- on_notice {{{
function handlers.on_notice(from, to, msg)
local msgs = ctcp._ctcp_split(msg)
for _, v in base.ipairs(msgs) do
local msg = v.str
if v.ctcp then
-- ctcp message {{{
local words = misc._split(msg)
local command = words[1]:lower()
table.remove(words, 1)
misc._try_call_warn("Unknown CTCP message: " .. command,
ctcp_handlers["on_rpl_"..command], from, to,
table.concat(words, ' '))
-- }}}
else
-- normal message {{{
if to:sub(1, 1) == "#" then
base.assert(serverinfo.channels[to],
"Received channel msg from unknown channel: " .. to)
callback("channel_notice", serverinfo.channels[to], from, msg)
else
callback("private_notice", from, msg)
end
-- }}}
end
end
end
-- }}}
-- on_quit {{{
function handlers.on_quit(from, quit_msg)
for name, chan in base.pairs(serverinfo.channels) do
chan:_remove_user(from)
end
callback("quit", from, quit_msg)
end
-- }}}
-- on_ping {{{
-- respond to server pings to make sure it knows we are alive
function handlers.on_ping(from, respond_to)
send("PONG", respond_to)
end
-- }}}
-- }}}
-- server replies {{{
-- on_rpl_topic {{{
-- catch topic changes
function handlers.on_rpl_topic(from, chan, topic)
base.assert(serverinfo.channels[chan],
"Received topic information about unknown channel: " .. chan)
serverinfo.channels[chan]._topic.text = topic
end
-- }}}
-- on_rpl_notopic {{{
function handlers.on_rpl_notopic(from, chan)
base.assert(serverinfo.channels[chan],
"Received topic information about unknown channel: " .. chan)
serverinfo.channels[chan]._topic.text = ""
end
-- }}}
-- on_rpl_topicdate {{{
-- "topic was set by <user> at <time>"
function handlers.on_rpl_topicdate(from, chan, user, time)
base.assert(serverinfo.channels[chan],
"Received topic information about unknown channel: " .. chan)
serverinfo.channels[chan]._topic.user = user
serverinfo.channels[chan]._topic.time = base.tonumber(time)
end
-- }}}
-- on_rpl_namreply {{{
-- handles a NAMES reply
function handlers.on_rpl_namreply(from, chanmode, chan, userlist)
base.assert(serverinfo.channels[chan],
"Received user information about unknown channel: " .. chan)
serverinfo.channels[chan]._chanmode = constants.chanmodes[chanmode]
local users = misc._split(userlist)
for k,v in base.ipairs(users) do
if v:sub(1, 1) == "@" or v:sub(1, 1) == "+" then
local nick = v:sub(2)
serverinfo.channels[chan]:_add_user(nick, v:sub(1, 1))
else
serverinfo.channels[chan]:_add_user(v)
end
end
end
-- }}}
-- on_rpl_endofnames {{{
-- when we get this message, the channel join has completed, so call the
-- external cb
function handlers.on_rpl_endofnames(from, chan)
base.assert(serverinfo.channels[chan],
"Received user information about unknown channel: " .. chan)
if not serverinfo.channels[chan].join_complete then
callback("me_join", serverinfo.channels[chan])
serverinfo.channels[chan].join_complete = true
end
end
-- }}}
-- on_rpl_welcome {{{
function handlers.on_rpl_welcome(from)
serverinfo = {
connected = false,
connecting = true,
channels = {}
}
end
-- }}}
-- on_rpl_yourhost {{{
function handlers.on_rpl_yourhost(from, msg)
serverinfo.host = from
end
-- }}}
-- on_rpl_motdstart {{{
function handlers.on_rpl_motdstart(from)
serverinfo.motd = ""
end
-- }}}
-- on_rpl_motd {{{
function handlers.on_rpl_motd(from, motd)
serverinfo.motd = (serverinfo.motd or "") .. motd .. "\n"
end
-- }}}
-- on_rpl_endofmotd {{{
function handlers.on_rpl_endofmotd(from)
if not serverinfo.connected then
serverinfo.connected = true
serverinfo.connecting = false
callback("connect")
end
end
-- }}}
-- on_rpl_whoisuser {{{
function handlers.on_rpl_whoisuser(from, nick, user, host, star, realname)
local lnick = nick:lower()
requestinfo.whois[lnick].nick = nick
requestinfo.whois[lnick].user = user
requestinfo.whois[lnick].host = host
requestinfo.whois[lnick].realname = realname
end
-- }}}
-- on_rpl_whoischannels {{{
function handlers.on_rpl_whoischannels(from, nick, channel_list)
nick = nick:lower()
if not requestinfo.whois[nick].channels then
requestinfo.whois[nick].channels = {}
end
for _, channel in base.ipairs(misc._split(channel_list)) do
table.insert(requestinfo.whois[nick].channels, channel)
end
end
-- }}}
-- on_rpl_whoisserver {{{
function handlers.on_rpl_whoisserver(from, nick, server, serverinfo)
nick = nick:lower()
requestinfo.whois[nick].server = server
requestinfo.whois[nick].serverinfo = serverinfo
end
-- }}}
-- on_rpl_away {{{
function handlers.on_rpl_away(from, nick, away_msg)
nick = nick:lower()
if requestinfo.whois[nick] then
requestinfo.whois[nick].away_msg = away_msg
end
end
-- }}}
-- on_rpl_whoisoperator {{{
function handlers.on_rpl_whoisoperator(from, nick)
requestinfo.whois[nick:lower()].is_oper = true
end
-- }}}
-- on_rpl_whoisidle {{{
function handlers.on_rpl_whoisidle(from, nick, idle_seconds)
requestinfo.whois[nick:lower()].idle_time = idle_seconds
end
-- }}}
-- on_rpl_endofwhois {{{
function handlers.on_rpl_endofwhois(from, nick)
nick = nick:lower()
local cb = table.remove(icallbacks.whois[nick], 1)
cb(requestinfo.whois[nick])
requestinfo.whois[nick] = nil
if #icallbacks.whois[nick] > 0 then send("WHOIS", nick)
else icallbacks.whois[nick] = nil
end
end
-- }}}
-- on_rpl_version {{{
function handlers.on_rpl_version(from, version, server, comments)
local cb = table.remove(icallbacks.serverversion[server], 1)
cb({version = version, server = server, comments = comments})
if #icallbacks.serverversion[server] > 0 then send("VERSION", server)
else icallbacks.serverversion[server] = nil
end
end
-- }}}
-- on_rpl_time {{{
function on_rpl_time(from, server, time)
local cb = table.remove(icallbacks.servertime[server], 1)
cb({time = time, server = server})
if #icallbacks.servertime[server] > 0 then send("TIME", server)
else icallbacks.servertime[server] = nil
end
end
-- }}}
-- }}}
-- ctcp handlers {{{
-- requests {{{
-- on_action {{{
function ctcp_handlers.on_action(from, to, message)
if to:sub(1, 1) == "#" then
base.assert(serverinfo.channels[to],
"Received channel msg from unknown channel: " .. to)
callback("channel_act", serverinfo.channels[to], from, message)
else
callback("private_act", from, message)
end
end
-- }}}
-- on_dcc {{{
-- TODO: can we not have this handler be registered unless the dcc module is
-- loaded?
function ctcp_handlers.on_dcc(from, to, message)
local type, argument, address, port, size = base.unpack(misc._split(message, " ", nil, '"', '"'))
address = misc._ip_int_to_str(address)
if type == "SEND" then
if callback("dcc_send", from, to, argument, address, port, size) then
dcc._accept(argument, address, port)
end
elseif type == "CHAT" then
-- TODO: implement this? do people ever use this?
end
end
-- }}}
-- on_version {{{
function ctcp_handlers.on_version(from, to)
notice(from, c("VERSION", _VERSION .. " running under " .. base._VERSION .. " with " .. socket._VERSION))
end
-- }}}
-- on_errmsg {{{
function ctcp_handlers.on_errmsg(from, to, message)
notice(from, c("ERRMSG", message, ":No error has occurred"))
end
-- }}}
-- on_ping {{{
function ctcp_handlers.on_ping(from, to, timestamp)
notice(from, c("PING", timestamp))
end
-- }}}
-- on_time {{{
function ctcp_handlers.on_time(from, to)
notice(from, c("TIME", os.date()))
end
-- }}}
-- }}}
-- responses {{{
-- on_rpl_action {{{
-- actions are handled the same, notice or not
ctcp_handlers.on_rpl_action = ctcp_handlers.on_action
-- }}}
-- on_rpl_version {{{
function ctcp_handlers.on_rpl_version(from, to, version)
local lfrom = from:lower()
local cb = table.remove(icallbacks.ctcp_version[lfrom], 1)
cb({version = version, nick = from})
if #icallbacks.ctcp_version[lfrom] > 0 then say(from, c("VERSION"))
else icallbacks.ctcp_version[lfrom] = nil
end
end
-- }}}
-- on_rpl_errmsg {{{
function ctcp_handlers.on_rpl_errmsg(from, to, message)
callback("ctcp_error", from, to, message)
end
-- }}}
-- on_rpl_ping {{{
function ctcp_handlers.on_rpl_ping(from, to, timestamp)
local lfrom = from:lower()
local cb = table.remove(icallbacks.ctcp_ping[lfrom], 1)
cb({time = os.time() - timestamp, nick = from})
if #icallbacks.ctcp_ping[lfrom] > 0 then say(from, c("PING", os.time()))
else icallbacks.ctcp_ping[lfrom] = nil
end
end
-- }}}
-- on_rpl_time {{{
function ctcp_handlers.on_rpl_time(from, to, time)
local lfrom = from:lower()
local cb = table.remove(icallbacks.ctcp_time[lfrom], 1)
cb({time = time, nick = from})
if #icallbacks.ctcp_time[lfrom] > 0 then say(from, c("TIME"))
else icallbacks.ctcp_time[lfrom] = nil
end
end
-- }}}
-- }}}
-- }}}
-- }}}
-- module functions {{{
-- socket handling functions {{{
-- _register_socket {{{
--
-- Register a socket to listen on.
-- @param sock LuaSocket socket object
-- @param mode 'r' if the socket is for reading, 'w' if for writing
-- @param cb Callback to call when the socket is ready for reading/writing.
-- It will be called with the socket as the single argument.
function _register_socket(sock, mode, cb)
local socks, cbs
if mode == 'r' then
socks = rsockets
cbs = rcallbacks
else
socks = wsockets
cbs = wcallbacks
end
base.assert(not cbs[sock], "socket already registered")
table.insert(socks, sock)
cbs[sock] = cb
end
-- }}}
-- _unregister_socket {{{
--
-- Remove a previously registered socket.
-- @param sock Socket to unregister
-- @param mode 'r' to unregister it for reading, 'w' for writing
function _unregister_socket(sock, mode)
local socks, cbs
if mode == 'r' then
socks = rsockets
cbs = rcallbacks
else
socks = wsockets
cbs = wcallbacks
end
for i, v in base.ipairs(socks) do
if v == sock then table.remove(socks, i); break; end
end
cbs[sock] = nil
end
-- }}}
-- }}}
-- }}}
-- public functions {{{
-- server commands {{{
-- connect {{{
---
-- Start a connection to the irc server.
-- @param args Table of named arguments containing connection parameters.
-- Defaults are the all-caps versions of these parameters given
-- at the top of the file, and are overridable by setting them
-- as well, i.e. <pre>irc.NETWORK = irc.freenode.net</pre>
-- Possible options are:
-- <ul>
-- <li><i>network:</i> address of the irc network to connect to
-- (default: 'localhost')</li>
-- <li><i>port:</i> port to connect to
-- (default: '6667')</li>
-- <li><i>pass:</i> irc server password
-- (default: don't send)</li>
-- <li><i>nick:</i> nickname to connect as
-- (default: 'luabot')</li>
-- <li><i>username:</i> username to connect with
-- (default: 'LuaIRC')</li>
-- <li><i>realname:</i> realname to connect with
-- (default: 'LuaIRC')</li>
-- <li><i>timeout:</i> amount of time in seconds to wait before
-- dropping an idle connection
-- (default: '60')</li>
-- </ul>
function connect(args)
local network = args.network or NETWORK
local port = args.port or PORT
local nick = args.nick or NICK
local username = args.username or USERNAME
local realname = args.realname or REALNAME
local timeout = args.timeout or TIMEOUT
serverinfo.connecting = true
if OUTFILE then irc_debug.set_output(OUTFILE) end
if DEBUG then irc_debug.enable() end
irc_sock = base.assert(socket.connect(network, port))
irc_sock:settimeout(timeout)
_register_socket(irc_sock, 'r', incoming_message)
if args.pass then send("PASS", args.pass) end
send("NICK", nick)
send("USER", username, get_ip(), network, realname)
begin_main_loop()
end
-- }}}
-- quit {{{
---
-- Close the connection to the irc server.
-- @param message Quit message (optional, defaults to 'Leaving')
function quit(message)
message = message or "Leaving"
send("QUIT", message)
serverinfo.connected = false
end
-- }}}
-- join {{{
---
-- Join a channel.
-- @param channel Channel to join
function join(channel)
if not channel then return end
serverinfo.channels[channel] = Channel.new(channel)
send("JOIN", channel)
end
-- }}}
-- part {{{
---
-- Leave a channel.
-- @param channel Channel to leave
function part(channel)
if not channel then return end
serverinfo.channels[channel] = nil
send("PART", channel)
end
-- }}}
-- say {{{
---
-- Send a message to a user or channel.
-- @param name User or channel to send the message to
-- @param message Message to send
function say(name, message)
if not name then return end
message = message or ""
send("PRIVMSG", name, message)
end
-- }}}
-- notice {{{
---
-- Send a notice to a user or channel.
-- @param name User or channel to send the notice to
-- @param message Message to send
function notice(name, message)
if not name then return end
message = message or ""
send("NOTICE", name, message)
end
-- }}}
-- act {{{
---
-- Perform a /me action.
-- @param name User or channel to send the action to
-- @param action Action to send
function act(name, action)
if not name then return end
action = action or ""
send("PRIVMSG", name, c("ACTION", action))
end
-- }}}
-- }}}
-- information requests {{{
-- server_version {{{
---
-- Request the version of the IRC server you are currently connected to.
-- @param cb Callback to call when the information is available. The single
-- table parameter to this callback will contain the fields:
-- <ul>
-- <li><i>server:</i> the server which responded to the request</li>
-- <li><i>version:</i> the server version</li>
-- <li><i>comments:</i> other data provided by the server</li>
-- </ul>
function server_version(cb)
-- apparently the optional server parameter isn't supported for servers
-- which you are not directly connected to (freenode specific?)
local server = serverinfo.host
if not icallbacks.serverversion[server] then
icallbacks.serverversion[server] = {cb}
send("VERSION", server)
else
table.insert(icallbacks.serverversion[server], cb)
end
end
-- }}}
-- whois {{{
-- TODO: allow server parameter (to get user idle time)
---
-- Request WHOIS information about a given user.
-- @param cb Callback to call when the information is available. The single
-- table parameter to this callback may contain any or all of the
-- fields:
-- <ul>
-- <li><i>nick:</i> the nick that was passed to this function
-- (this field will always be here)</li>
-- <li><i>user:</i> the IRC username of the user</li>
-- <li><i>host:</i> the user's hostname</li>
-- <li><i>realname:</i> the IRC realname of the user</li>
-- <li><i>server:</i> the IRC server the user is connected to</li>
-- <li><i>serverinfo:</i> arbitrary information about the above
-- server</li>
-- <li><i>awaymsg:</i> set to the user's away message if they are
-- away</li>
-- <li><i>is_oper:</i> true if the user is an IRCop</li>
-- <li><i>idle_time:</i> amount of time the user has been idle</li>
-- <li><i>channels:</i> array containing the channels the user has
-- joined</li>
-- </ul>
-- @param nick User to request WHOIS information about
function whois(cb, nick)
nick = nick:lower()
requestinfo.whois[nick] = {}
if not icallbacks.whois[nick] then
icallbacks.whois[nick] = {cb}
send("WHOIS", nick)
else
table.insert(icallbacks.whois[nick], cb)
end
end
-- }}}
-- server_time {{{
---
-- Request the current time of the server you are connected to.
-- @param cb Callback to call when the information is available. The single
-- table parameter to this callback will contain the fields:
-- <ul>
-- <li><i>server:</i> the server which responded to the request</li>
-- <li><i>time:</i> the time reported by the server</li>
-- </ul>
function server_time(cb)
-- apparently the optional server parameter isn't supported for servers
-- which you are not directly connected to (freenode specific?)
local server = serverinfo.host
if not icallbacks.servertime[server] then
icallbacks.servertime[server] = {cb}
send("TIME", server)
else
table.insert(icallbacks.servertime[server], cb)
end
end
-- }}}
-- }}}
-- ctcp commands {{{
-- ctcp_ping {{{
---
-- Send a CTCP ping request.
-- @param cb Callback to call when the information is available. The single
-- table parameter to this callback will contain the fields:
-- <ul>
-- <li><i>nick:</i> the nick which responded to the request</li>
-- <li><i>time:</i> the roundtrip ping time, in seconds</li>
-- </ul>
-- @param nick User to ping
function ctcp_ping(cb, nick)
nick = nick:lower()
if not icallbacks.ctcp_ping[nick] then
icallbacks.ctcp_ping[nick] = {cb}
say(nick, c("PING", os.time()))
else
table.insert(icallbacks.ctcp_ping[nick], cb)
end
end
-- }}}
-- ctcp_time {{{
---
-- Send a localtime request.
-- @param cb Callback to call when the information is available. The single
-- table parameter to this callback will contain the fields:
-- <ul>
-- <li><i>nick:</i> the nick which responded to the request</li>
-- <li><i>time:</i> the localtime reported by the remote client</li>
-- </ul>
-- @param nick User to request the localtime from
function ctcp_time(cb, nick)
nick = nick:lower()
if not icallbacks.ctcp_time[nick] then
icallbacks.ctcp_time[nick] = {cb}
say(nick, c("TIME"))
else
table.insert(icallbacks.ctcp_time[nick], cb)
end
end
-- }}}
-- ctcp_version {{{
---
-- Send a client version request.
-- @param cb Callback to call when the information is available. The single
-- table parameter to this callback will contain the fields:
-- <ul>
-- <li><i>nick:</i> the nick which responded to the request</li>
-- <li><i>version:</i> the version reported by the remote client</li>
-- </ul>
-- @param nick User to request the client version from
function ctcp_version(cb, nick)
nick = nick:lower()
if not icallbacks.ctcp_version[nick] then
icallbacks.ctcp_version[nick] = {cb}
say(nick, c("VERSION"))
else
table.insert(icallbacks.ctcp_version[nick], cb)
end
end
-- }}}
-- }}}
-- callback functions {{{
-- register_callback {{{
---
-- Register a user function to be called when a specific event occurs.
-- @param name Name of the event
-- @param fn Function to call when the event occurs, or nil to clear the
-- callback for this event
-- @return Value of the original callback for this event (or nil if no previous
-- callback had been set)
function register_callback(name, fn)
local old_handler = user_handlers[name]
user_handlers[name] = fn
return old_handler
end
-- }}}
-- }}}
-- misc functions {{{
-- send {{{
-- TODO: CTCP quoting should be explicit, this table thing is quite ugly (if
-- convenient)
---
-- Send a raw IRC command.
-- @param command String containing the raw IRC command
-- @param ... Arguments to the command. Each argument is either a string or
-- an array. Strings are sent literally, arrays are CTCP quoted
-- as a group. The last argument (if it exists) is preceded by
-- a : (so it may contain spaces).
function send(command, ...)
if not serverinfo.connected and not serverinfo.connecting then return end
local message = command
for i, v in base.ipairs({...}) do
if i == #{...} then
v = ":" .. v
end
message = message .. " " .. v
end
message = ctcp._low_quote(message)
-- we just truncate for now. -2 to account for the \r\n
message = message:sub(1, constants.IRC_MAX_MSG - 2)
irc_debug._message("SEND", message)
irc_sock:send(message .. "\r\n")
end
-- }}}
-- get_ip {{{
---
-- Get the local IP address for the server connection.
-- @return A string representation of the local IP address that the IRC server
-- connection is communicating on
function get_ip()
return (ip or irc_sock:getsockname())
end
-- }}}
-- set_ip {{{
---
-- Set the local IP manually (to allow for NAT workarounds)
-- @param new_ip IP address to set
function set_ip(new_ip)
ip = new_ip
end
-- }}}
-- channels {{{
-- TODO: @see doesn't currently work for files/modules
---
-- Iterate over currently joined channels.
-- channels() is an iterator function for use in for loops.
-- For example, <pre>for chan in irc.channels() do print(chan:name) end</pre>
-- @see irc.channel
function channels()
return function(state, arg)
return misc._value_iter(state, arg,
function(v)
return v.join_complete
end)
end,
serverinfo.channels,
nil
end
-- }}}
-- }}}
-- }}}

488
irc/channel.lua Normal file
View file

@ -0,0 +1,488 @@
---
-- Implementation of the Channel class
-- initialization {{{
local base = _G
local irc = require 'irc'
local misc = require 'irc.misc'
local socket = require 'socket'
local table = require 'table'
-- }}}
---
-- This module implements a channel object representing a single channel we
-- have joined.
module 'irc.channel'
-- object metatable {{{
-- TODO: this <br /> shouldn't be necessary - bug in luadoc
---
-- An object of the Channel class represents a single joined channel. It has
-- several table fields, and can be used in string contexts (returning the
-- channel name).<br />
-- @class table
-- @name Channel
-- @field name Name of the channel (read only)
-- @field topic Channel topic, if set (read/write, writing to this sends a
-- topic change request to the server for this channel)
-- @field chanmode Channel mode (public/private/secret) (read only)
-- @field members Array of all members of this channel
local mt = {
-- __index() {{{
__index = function(self, key)
if key == "name" then
return self._name
elseif key == "topic" then
return self._topic
elseif key == "chanmode" then
return self._chanmode
else
return _M[key]
end
end,
-- }}}
-- __newindex() {{{
__newindex = function(self, key, value)
if key == "name" then
return
elseif key == "topic" then
irc.send("TOPIC", self._name, value)
elseif key == "chanmode" then
return
else
base.rawset(self, key, value)
end
end,
-- }}}
-- __concat() {{{
__concat = function(first, second)
local first_str, second_str
if base.type(first) == "table" then
first_str = first._name
else
first_str = first
end
if base.type(second) == "table" then
second_str = second._name
else
second_str = second
end
return first_str .. second_str
end,
-- }}}
-- __tostring() {{{
__tostring = function(self)
return self._name
end
-- }}}
}
-- }}}
-- private methods {{{
-- set_basic_mode {{{
--
-- Sets a no-arg mode on a channel.
-- @name chan:set_basic_mode
-- @param self Channel object
-- @param set True to set the mode, false to unset it
-- @param letter Letter of the mode
local function set_basic_mode(self, set, letter)
if set then
irc.send("MODE", self.name, "+" .. letter)
else
irc.send("MODE", self.name, "-" .. letter)
end
end
-- }}}
-- }}}
-- internal methods {{{
-- TODO: is there a better way to do this? also, storing op/voice as initial
-- substrings of the username is just ugly
-- _add_user {{{
--
-- Add a user to the channel's internal user list.
-- @param self Channel object
-- @param user Nick of the user to add
-- @param mode Mode (op/voice) of the user, in symbolic form (@/+)
function _add_user(self, user, mode)
mode = mode or ''
self._members[user] = mode .. user
end
-- }}}
-- _remove_user {{{
--
-- Remove a user from the channel's internal user list.
-- @param self Channel object
-- @param user Nick of the user to remove
function _remove_user(self, user)
self._members[user] = nil
end
-- }}}
-- _change_status {{{
--
-- Change the op/voice status of a user in the channel's internal user list.
-- @param self Channel object
-- @param user Nick of the user to affect
-- @param on True if the mode is being set, false if it's being unset
-- @param mode 'o' for op, 'v' for voice
function _change_status(self, user, on, mode)
if not self._members[user] then return end
if on then
if mode == 'o' then
self._members[user] = '@' .. user
elseif mode == 'v' then
self._members[user] = '+' .. user
end
else
if (mode == 'o' and self._members[user]:sub(1, 1) == '@') or
(mode == 'v' and self._members[user]:sub(1, 1) == '+') then
self._members[user] = user
end
end
end
-- }}}
-- _change_nick {{{
--
-- Change the nick of a user in the channel's internal user list.
-- @param self Channel object
-- @param old_nick User's old nick
-- @param new_nick User's new nick
function _change_nick(self, old_nick, new_nick)
for member in self:each_member() do
local member_nick = member:gsub('@+', '')
if member_nick == old_nick then
local mode = self._members[old_nick]:sub(1, 1)
if mode ~= '@' and mode ~= '+' then mode = "" end
self._members[old_nick] = nil
self._members[new_nick] = mode .. new_nick
break
end
end
end
-- }}}
-- }}}
-- constructor {{{
---
-- Creates a new Channel object.
-- @param chan Name of the new channel
-- @return The new channel instance
function new(chan)
return base.setmetatable({_name = chan, _topic = {}, _chanmode = "",
_members = {}}, mt)
end
-- }}}
-- public methods {{{
-- iterators {{{
-- each_op {{{
---
-- Iterator over the ops in the channel
-- @param self Channel object
function each_op(self)
return function(state, arg)
return misc._value_iter(state, arg,
function(v)
return v:sub(1, 1) == "@"
end)
end,
self._members,
nil
end
-- }}}
-- each_voice {{{
---
-- Iterator over the voiced users in the channel
-- @param self Channel object
function each_voice(self)
return function(state, arg)
return misc._value_iter(state, arg,
function(v)
return v:sub(1, 1) == "+"
end)
end,
self._members,
nil
end
-- }}}
-- each_user {{{
---
-- Iterator over the normal users in the channel
-- @param self Channel object
function each_user(self)
return function(state, arg)
return misc._value_iter(state, arg,
function(v)
return v:sub(1, 1) ~= "@" and
v:sub(1, 1) ~= "+"
end)
end,
self._members,
nil
end
-- }}}
-- each_member {{{
---
-- Iterator over all users in the channel
-- @param self Channel object
function each_member(self)
return misc._value_iter, self._members, nil
end
-- }}}
-- }}}
-- return tables of users {{{
-- ops {{{
---
-- Gets an array of all the ops in the channel.
-- @param self Channel object
-- @return Array of channel ops
function ops(self)
local ret = {}
for nick in self:each_op() do
table.insert(ret, nick)
end
return ret
end
-- }}}
-- voices {{{
---
-- Gets an array of all the voiced users in the channel.
-- @param self Channel object
-- @return Array of channel voiced users
function voices(self)
local ret = {}
for nick in self:each_voice() do
table.insert(ret, nick)
end
return ret
end
-- }}}
-- users {{{
---
-- Gets an array of all the normal users in the channel.
-- @param self Channel object
-- @return Array of channel normal users
function users(self)
local ret = {}
for nick in self:each_user() do
table.insert(ret, nick)
end
return ret
end
-- }}}
-- members {{{
---
-- Gets an array of all the users in the channel.
-- @param self Channel object
-- @return Array of channel users
function members(self)
local ret = {}
-- not just returning self._members, since the return value shouldn't be
-- modifiable
for nick in self:each_member() do
table.insert(ret, nick)
end
return ret
end
-- }}}
-- }}}
-- setting modes {{{
-- ban {{{
-- TODO: hmmm, this probably needs an appropriate mask, rather than a nick
---
-- Ban a user from a channel.
-- @param self Channel object
-- @param name User to ban
function ban(self, name)
irc.send("MODE", self.name, "+b", name)
end
-- }}}
-- unban {{{
-- TODO: same here
---
-- Remove a ban on a user.
-- @param self Channel object
-- @param name User to unban
function unban(self, name)
irc.send("MODE", self.name, "-b", name)
end
-- }}}
-- voice {{{
---
-- Give a user voice on a channel.
-- @param self Channel object
-- @param name User to give voice to
function voice(self, name)
irc.send("MODE", self.name, "+v", name)
end
-- }}}
-- devoice {{{
---
-- Remove voice from a user.
-- @param self Channel object
-- @param name User to remove voice from
function devoice(self, name)
irc.send("MODE", self.name, "-v", name)
end
-- }}}
-- kick {{{
---
-- Kicks a user.
-- @param self Channel object
-- @param name User to kick
-- @param reason Reason for kicking(optional)
function kick(self, name)
irc.send("KICK", self.name, name, reason)
end
-- }}}
-- op {{{
---
-- Give a user ops on a channel.
-- @param self Channel object
-- @param name User to op
function op(self, name)
irc.send("MODE", self.name, "+o", name)
end
-- }}}
-- deop {{{
---
-- Remove ops from a user.
-- @param self Channel object
-- @param name User to remove ops from
function deop(self, name)
irc.send("MODE", self.name, "-o", name)
end
-- }}}
-- set_limit {{{
---
-- Set a channel limit.
-- @param self Channel object
-- @param new_limit New value for the channel limit (optional; limit is unset
-- if this argument isn't passed)
function set_limit(self, new_limit)
if new_limit then
irc.send("MODE", self.name, "+l", new_limit)
else
irc.send("MODE", self.name, "-l")
end
end
-- }}}
-- set_key {{{
---
-- Set a channel password.
-- @param self Channel object
-- @param key New channel password (optional; password is unset if this
-- argument isn't passed)
function set_key(self, key)
if key then
irc.send("MODE", self.name, "+k", key)
else
irc.send("MODE", self.name, "-k")
end
end
-- }}}
-- set_private {{{
---
-- Set the private state of a channel.
-- @param self Channel object
-- @param set True to set the channel as private, false to unset it
function set_private(self, set)
set_basic_mode(self, set, "p")
end
-- }}}
-- set_secret {{{
---
-- Set the secret state of a channel.
-- @param self Channel object
-- @param set True to set the channel as secret, false to unset it
function set_secret(self, set)
set_basic_mode(self, set, "s")
end
-- }}}
-- set_invite_only {{{
---
-- Set whether joining the channel requires an invite.
-- @param self Channel object
-- @param set True to set the channel invite only, false to unset it
function set_invite_only(self, set)
set_basic_mode(self, set, "i")
end
-- }}}
-- set_topic_lock {{{
---
-- If true, the topic can only be changed by an op.
-- @param self Channel object
-- @param set True to lock the topic, false to unlock it
function set_topic_lock(self, set)
set_basic_mode(self, set, "t")
end
-- }}}
-- set_no_outside_messages {{{
---
-- If true, users must be in the channel to send messages to it.
-- @param self Channel object
-- @param set True to require users to be in the channel to send messages to
-- it, false to remove this restriction
function set_no_outside_messages(self, set)
set_basic_mode(self, set, "n")
end
-- }}}
-- set moderated {{{
---
-- Set whether voice is required to speak.
-- @param self Channel object
-- @param set True to set the channel as moderated, false to unset it
function set_moderated(self, set)
set_basic_mode(self, set, "m")
end
-- }}}
-- }}}
-- accessors {{{
-- contains {{{
---
-- Test if a user is in the channel.
-- @param self Channel object
-- @param nick Nick to search for
-- @return True if the nick is in the channel, false otherwise
function contains(self, nick)
for member in self:each_member() do
local member_nick = member:gsub('@+', '')
if member_nick == nick then
return true
end
end
return false
end
-- }}}
-- }}}
-- }}}

191
irc/constants.lua Normal file
View file

@ -0,0 +1,191 @@
---
-- This module holds various constants used by the IRC protocol.
module "irc.constants"
-- protocol constants {{{
IRC_MAX_MSG = 512
-- }}}
-- server replies {{{
replies = {
-- Command responses {{{
[001] = "RPL_WELCOME",
[002] = "RPL_YOURHOST",
[003] = "RPL_CREATED",
[004] = "RPL_MYINFO",
[005] = "RPL_BOUNCE",
[302] = "RPL_USERHOST",
[303] = "RPL_ISON",
[301] = "RPL_AWAY",
[305] = "RPL_UNAWAY",
[306] = "RPL_NOWAWAY",
[311] = "RPL_WHOISUSER",
[312] = "RPL_WHOISSERVER",
[313] = "RPL_WHOISOPERATOR",
[317] = "RPL_WHOISIDLE",
[318] = "RPL_ENDOFWHOIS",
[319] = "RPL_WHOISCHANNELS",
[314] = "RPL_WHOWASUSER",
[369] = "RPL_ENDOFWHOWAS",
[321] = "RPL_LISTSTART",
[322] = "RPL_LIST",
[323] = "RPL_LISTEND",
[325] = "RPL_UNIQOPIS",
[324] = "RPL_CHANNELMODEIS",
[331] = "RPL_NOTOPIC",
[332] = "RPL_TOPIC",
[341] = "RPL_INVITING",
[342] = "RPL_SUMMONING",
[346] = "RPL_INVITELIST",
[347] = "RPL_ENDOFINVITELIST",
[348] = "RPL_EXCEPTLIST",
[349] = "RPL_ENDOFEXCEPTLIST",
[351] = "RPL_VERSION",
[352] = "RPL_WHOREPLY",
[315] = "RPL_ENDOFWHO",
[353] = "RPL_NAMREPLY",
[366] = "RPL_ENDOFNAMES",
[364] = "RPL_LINKS",
[365] = "RPL_ENDOFLINKS",
[367] = "RPL_BANLIST",
[368] = "RPL_ENDOFBANLIST",
[371] = "RPL_INFO",
[374] = "RPL_ENDOFINFO",
[375] = "RPL_MOTDSTART",
[372] = "RPL_MOTD",
[376] = "RPL_ENDOFMOTD",
[381] = "RPL_YOUREOPER",
[382] = "RPL_REHASHING",
[383] = "RPL_YOURESERVICE",
[391] = "RPL_TIME",
[392] = "RPL_USERSSTART",
[393] = "RPL_USERS",
[394] = "RPL_ENDOFUSERS",
[395] = "RPL_NOUSERS",
[200] = "RPL_TRACELINK",
[201] = "RPL_TRACECONNECTING",
[202] = "RPL_TRACEHANDSHAKE",
[203] = "RPL_TRACEUNKNOWN",
[204] = "RPL_TRACEOPERATOR",
[205] = "RPL_TRACEUSER",
[206] = "RPL_TRACESERVER",
[207] = "RPL_TRACESERVICE",
[208] = "RPL_TRACENEWTYPE",
[209] = "RPL_TRACECLASS",
[210] = "RPL_TRACERECONNECT",
[261] = "RPL_TRACELOG",
[262] = "RPL_TRACEEND",
[211] = "RPL_STATSLINKINFO",
[212] = "RPL_STATSCOMMANDS",
[219] = "RPL_ENDOFSTATS",
[242] = "RPL_STATSUPTIME",
[243] = "RPL_STATSOLINE",
[221] = "RPL_UMODEIS",
[234] = "RPL_SERVLIST",
[235] = "RPL_SERVLISTEND",
[221] = "RPL_UMODEIS",
[251] = "RPL_LUSERCLIENT",
[252] = "RPL_LUSEROP",
[253] = "RPL_LUSERUNKNOWN",
[254] = "RPL_LUSERCHANNELS",
[255] = "RPL_LUSERME",
[256] = "RPL_ADMINME",
[257] = "RPL_ADMINLOC1",
[258] = "RPL_ADMINLOC2",
[259] = "RPL_ADMINEMAIL",
[263] = "RPL_TRYAGAIN",
-- }}}
-- Error codes {{{
[401] = "ERR_NOSUCHNICK", -- No such nick/channel
[402] = "ERR_NOSUCHSERVER", -- No such server
[403] = "ERR_NOSUCHCHANNEL", -- No such channel
[404] = "ERR_CANNOTSENDTOCHAN", -- Cannot send to channel
[405] = "ERR_TOOMANYCHANNELS", -- You have joined too many channels
[406] = "ERR_WASNOSUCHNICK", -- There was no such nickname
[407] = "ERR_TOOMANYTARGETS", -- Duplicate recipients. No message delivered
[408] = "ERR_NOSUCHSERVICE", -- No such service
[409] = "ERR_NOORIGIN", -- No origin specified
[411] = "ERR_NORECIPIENT", -- No recipient given
[412] = "ERR_NOTEXTTOSEND", -- No text to send
[413] = "ERR_NOTOPLEVEL", -- No toplevel domain specified
[414] = "ERR_WILDTOPLEVEL", -- Wildcard in toplevel domain
[415] = "ERR_BADMASK", -- Bad server/host mask
[421] = "ERR_UNKNOWNCOMMAND", -- Unknown command
[422] = "ERR_NOMOTD", -- MOTD file is missing
[423] = "ERR_NOADMININFO", -- No administrative info available
[424] = "ERR_FILEERROR", -- File error
[431] = "ERR_NONICKNAMEGIVEN", -- No nickname given
[432] = "ERR_ERRONEUSNICKNAME", -- Erroneus nickname
[433] = "ERR_NICKNAMEINUSE", -- Nickname is already in use
[436] = "ERR_NICKCOLLISION", -- Nickname collision KILL
[437] = "ERR_UNAVAILRESOURCE", -- Nick/channel is temporarily unavailable
[441] = "ERR_USERNOTINCHANNEL", -- They aren't on that channel
[442] = "ERR_NOTONCHANNEL", -- You're not on that channel
[443] = "ERR_USERONCHANNEL", -- User is already on channel
[444] = "ERR_NOLOGIN", -- User not logged in
[445] = "ERR_SUMMONDISABLED", -- SUMMON has been disabled
[446] = "ERR_USERSDISABLED", -- USERS has been disabled
[451] = "ERR_NOTREGISTERED", -- You have not registered
[461] = "ERR_NEEDMOREPARAMS", -- Not enough parameters
[462] = "ERR_ALREADYREGISTERED", -- You may not reregister
[463] = "ERR_NOPERMFORHOST", -- Your host isn't among the privileged
[464] = "ERR_PASSWDMISMATCH", -- Password incorrect
[465] = "ERR_YOUREBANNEDCREEP", -- You are banned from this server
[466] = "ERR_YOUWILLBEBANNED",
[467] = "ERR_KEYSET", -- Channel key already set
[471] = "ERR_CHANNELISFULL", -- Cannot join channel (+l)
[472] = "ERR_UNKNOWNMODE", -- Unknown mode char
[473] = "ERR_INVITEONLYCHAN", -- Cannot join channel (+i)
[474] = "ERR_BANNEDFROMCHAN", -- Cannot join channel (+b)
[475] = "ERR_BADCHANNELKEY", -- Cannot join channel (+k)
[476] = "ERR_BADCHANMASK", -- Bad channel mask
[477] = "ERR_NOCHANMODES", -- Channel doesn't support modes
[478] = "ERR_BANLISTFULL", -- Channel list is full
[481] = "ERR_NOPRIVILEGES", -- Permission denied- You're not an IRC operator
[482] = "ERR_CHANOPRIVSNEEDED", -- You're not channel operator
[483] = "ERR_CANTKILLSERVER", -- You can't kill a server!
[484] = "ERR_RESTRICTED", -- Your connection is restricted!
[485] = "ERR_UNIQOPPRIVSNEEDED", -- You're not the original channel operator
[491] = "ERR_NOOPERHOST", -- No O-lines for your host
[501] = "ERR_UMODEUNKNOWNFLAG", -- Unknown MODE flag
[502] = "ERR_USERSDONTMATCH", -- Can't change mode for other users
-- }}}
-- unused {{{
[231] = "RPL_SERVICEINFO",
[232] = "RPL_ENDOFSERVICES",
[233] = "RPL_SERVICE",
[300] = "RPL_NONE",
[316] = "RPL_WHOISCHANOP",
[361] = "RPL_KILLDONE",
[362] = "RPL_CLOSING",
[363] = "RPL_CLOSEEND",
[373] = "RPL_INFOSTART",
[384] = "RPL_MYPORTIS",
[213] = "RPL_STATSCLINE",
[214] = "RPL_STATSNLINE",
[215] = "RPL_STATSILINE",
[216] = "RPL_STATSKLINE",
[217] = "RPL_STATSQLINE",
[218] = "RPL_STATSYLINE",
[240] = "RPL_STATSVLINE",
[241] = "RPL_STATSLLINE",
[244] = "RPL_STATSHLINE",
[246] = "RPL_STATSPING",
[247] = "RPL_STATSBLINE",
[250] = "RPL_STATSDLINE",
[492] = "ERR_NOSERVICEHOST",
-- }}}
-- guesses {{{
[333] = "RPL_TOPICDATE", -- date the topic was set, in seconds since the epoch
[505] = "ERR_NOTREGISTERED" -- freenode blocking privmsg from unreged users
-- }}}
}
-- }}}
-- chanmodes {{{
chanmodes = {
["@"] = "secret",
["*"] = "private",
["="] = "public"
}
-- }}}

115
irc/ctcp.lua Normal file
View file

@ -0,0 +1,115 @@
---
-- Implementation of the CTCP protocol
-- initialization {{{
local base = _G
local table = require "table"
-- }}}
---
-- This module implements the various quoting and escaping requirements of the
-- CTCP protocol.
module "irc.ctcp"
-- internal functions {{{
-- _low_quote {{{
--
-- Applies low level quoting to a string (escaping characters which are illegal
-- to appear in an IRC packet).
-- @param ... Strings to quote together, space separated
-- @return Quoted string
function _low_quote(...)
local str = table.concat({...}, " ")
return str:gsub("[%z\n\r\020]", {["\000"] = "\0200",
["\n"] = "\020n",
["\r"] = "\020r",
["\020"] = "\020\020"})
end
-- }}}
-- _low_dequote {{{
--
-- Removes low level quoting done by low_quote.
-- @param str String with low level quoting applied to it
-- @return String with those quoting methods stripped off
function _low_dequote(str)
return str:gsub("\020(.?)", function(s)
if s == "0" then return "\000" end
if s == "n" then return "\n" end
if s == "r" then return "\r" end
if s == "\020" then return "\020" end
return ""
end)
end
-- }}}
-- _ctcp_quote {{{
--
-- Applies CTCP quoting to a block of text which has been identified as CTCP
-- data (by the calling program).
-- @param ... Strings to apply CTCP quoting to together, space separated
-- @return String with CTCP quoting applied
function _ctcp_quote(...)
local str = table.concat({...}, " ")
local ret = str:gsub("[\001\\]", {["\001"] = "\\a",
["\\"] = "\\\\"})
return "\001" .. ret .. "\001"
end
-- }}}
-- _ctcp_dequote {{{
--
-- Removes CTCP quoting from a block of text which has been identified as CTCP
-- data (likely by ctcp_split).
-- @param str String with CTCP quoting
-- @return String with all CTCP quoting stripped
function _ctcp_dequote(str)
local ret = str:gsub("^\001", ""):gsub("\001$", "")
return ret:gsub("\\(.?)", function(s)
if s == "a" then return "\001" end
if s == "\\" then return "\\" end
return ""
end)
end
-- }}}
-- _ctcp_split {{{
--
-- Splits a low level dequoted string into normal text and unquoted CTCP
-- messages.
-- @param str Low level dequoted string
-- @return Array of tables, with each entry in the array corresponding to one
-- part of the split message. These tables will have these fields:
-- <ul>
-- <li><i>str:</i> The text of the split section</li>
-- <li><i>ctcp:</i> True if the section was a CTCP message, false
-- otherwise</li>
-- </ul>
function _ctcp_split(str)
local ret = {}
local iter = 1
while true do
local s, e = str:find("\001.*\001", iter)
local plain_string, ctcp_string
if not s then
plain_string = str:sub(iter, -1)
else
plain_string = str:sub(iter, s - 1)
ctcp_string = str:sub(s, e)
end
if plain_string ~= "" then
table.insert(ret, {str = plain_string, ctcp = false})
end
if not s then break end
if ctcp_string ~= "" then
table.insert(ret, {str = _ctcp_dequote(ctcp_string), ctcp = true})
end
iter = e + 1
end
return ret
end
-- }}}
-- }}}

196
irc/dcc.lua Normal file
View file

@ -0,0 +1,196 @@
---
-- Implementation of the DCC protocol
-- initialization {{{
local base = _G
local irc = require 'irc'
local ctcp = require 'irc.ctcp'
local c = ctcp._ctcp_quote
local irc_debug = require 'irc.debug'
local misc = require 'irc.misc'
local socket = require 'socket'
local coroutine = require 'coroutine'
local io = require 'io'
local string = require 'string'
-- }}}
---
-- This module implements the DCC protocol. File transfers (DCC SEND) are
-- handled, but DCC CHAT is not, as of yet.
module 'irc.dcc'
-- defaults {{{
FIRST_PORT = 1028
LAST_PORT = 5000
-- }}}
-- private functions {{{
-- debug_dcc {{{
--
-- Prints a debug message about DCC events similar to irc.debug.warn, etc.
-- @param msg Debug message
local function debug_dcc(msg)
irc_debug._message("DCC", msg, "\027[0;32m")
end
-- }}}
-- send_file {{{
--
-- Sends a file to a remote user, after that user has accepted our DCC SEND
-- invitation
-- @param sock Socket to send the file on
-- @param file Lua file object corresponding to the file we want to send
-- @param packet_size Size of the packets to send the file in
local function send_file(sock, file, packet_size)
local bytes = 0
while true do
local packet = file:read(packet_size)
if not packet then break end
bytes = bytes + packet:len()
local index = 1
while true do
local skip = false
sock:send(packet, index)
local new_bytes, err = sock:receive(4)
if not new_bytes then
if err == "timeout" then
skip = true
else
irc_debug._warn(err)
break
end
else
new_bytes = misc._int_to_str(new_bytes)
end
if not skip then
if new_bytes ~= bytes then
index = packet_size - bytes + new_bytes + 1
else
break
end
end
end
coroutine.yield(true)
end
debug_dcc("File completely sent")
file:close()
sock:close()
irc._unregister_socket(sock, 'w')
return true
end
-- }}}
-- handle_connect {{{
--
-- Handle the connection attempt by a remote user to get our file. Basically
-- just swaps out the server socket we were listening on for a client socket
-- that we can send data on
-- @param ssock Server socket that the remote user connected to
-- @param file Lua file object corresponding to the file we want to send
-- @param packet_size Size of the packets to send the file in
local function handle_connect(ssock, file, packet_size)
debug_dcc("Offer accepted, beginning to send")
packet_size = packet_size or 1024
local sock = ssock:accept()
sock:settimeout(0.1)
ssock:close()
irc._unregister_socket(ssock, 'r')
irc._register_socket(sock, 'w',
coroutine.wrap(function(s)
return send_file(s, file, packet_size)
end))
return true
end
-- }}}
-- accept_file {{{
--
-- Accepts a file from a remote user which has offered it to us.
-- @param sock Socket to receive the file on
-- @param file Lua file object corresponding to the file we want to save
-- @param packet_size Size of the packets to receive the file in
local function accept_file(sock, file, packet_size)
local bytes = 0
while true do
local packet, err, partial_packet = sock:receive(packet_size)
if not packet and err == "timeout" then packet = partial_packet end
if not packet then break end
if packet:len() == 0 then break end
bytes = bytes + packet:len()
sock:send(misc._str_to_int(bytes))
file:write(packet)
coroutine.yield(true)
end
debug_dcc("File completely received")
file:close()
sock:close()
irc._unregister_socket(sock, 'r')
return true
end
-- }}}
-- }}}
-- internal functions {{{
-- _accept {{{
--
-- Accepts a file offer from a remote user. Called when the on_dcc callback
-- retuns true.
-- @param filename Name to save the file as
-- @param address IP address of the remote user in low level int form
-- @param port Port to connect to at the remote user
-- @param packet_size Size of the packets the remote user will be sending
function _accept(filename, address, port, packet_size)
debug_dcc("Accepting a DCC SEND request from " .. address .. ":" .. port)
packet_size = packet_size or 1024
local sock = base.assert(socket.tcp())
base.assert(sock:connect(address, port))
sock:settimeout(0.1)
local file = base.assert(io.open(misc._get_unique_filename(filename), "w"))
irc._register_socket(sock, 'r',
coroutine.wrap(function(s)
return accept_file(s, file, packet_size)
end))
end
-- }}}
-- }}}
-- public functions {{{
-- send {{{
---
-- Offers a file to a remote user.
-- @param nick User to offer the file to
-- @param filename Filename to offer
-- @param port Port to accept connections on (optional, defaults to
-- choosing an available port between FIRST_PORT and LAST_PORT
-- above)
function send(nick, filename, port)
port = port or FIRST_PORT
local sock
repeat
sock = base.assert(socket.tcp())
err, msg = sock:bind('*', port)
port = port + 1
until msg ~= "address already in use" and port <= LAST_PORT + 1
port = port - 1
base.assert(err, msg)
base.assert(sock:listen(1))
local ip = misc._ip_str_to_int(irc.get_ip())
local file, err = io.open(filename)
if not file then
irc_debug._warn(err)
sock:close()
return
end
local size = file:seek("end")
file:seek("set")
irc._register_socket(sock, 'r',
coroutine.wrap(function(s)
return handle_connect(s, file)
end))
filename = misc._basename(filename)
if filename:find(" ") then filename = '"' .. filename .. '"' end
debug_dcc("Offering " .. filename .. " to " .. nick .. " from " ..
irc.get_ip() .. ":" .. port)
irc.send("PRIVMSG", nick, c("DCC", "SEND", filename, ip, port, size))
end
-- }}}
-- }}}

92
irc/debug.lua Normal file
View file

@ -0,0 +1,92 @@
---
-- Basic debug output
-- initialization {{{
local base = _G
local io = require 'io'
-- }}}
---
-- This module implements a few useful debug functions for use throughout the
-- rest of the code.
module 'irc.debug'
-- defaults {{{
COLOR = true
-- }}}
-- local variables {{{
local ON = false
local outfile = io.output()
-- }}}
-- internal functions {{{
-- _message {{{
--
-- Output a debug message.
-- @param msg_type Arbitrary string corresponding to the type of message
-- @param msg Message text
-- @param color Which terminal code to use for color output (defaults to
-- dark gray)
function _message(msg_type, msg, color)
if ON then
local endcolor = ""
if COLOR and outfile == io.stdout then
color = color or "\027[1;30m"
endcolor = "\027[0m"
else
color = ""
endcolor = ""
end
outfile:write(color .. msg_type .. ": " .. msg .. endcolor .. "\n")
end
end
-- }}}
-- _err {{{
--
-- Signal an error. Writes the error message to the screen in red and calls
-- error().
-- @param msg Error message
-- @see error
function _err(msg)
_message("ERR", msg, "\027[0;31m")
base.error(msg, 2)
end
-- }}}
-- _warn {{{
--
-- Signal a warning. Writes the warning message to the screen in yellow.
-- @param msg Warning message
function _warn(msg)
_message("WARN", msg, "\027[0;33m")
end
-- }}}
-- }}}
-- public functions {{{
-- enable {{{
---
-- Turns on debug output.
function enable()
ON = true
end
-- }}}
-- disable {{{
---
-- Turns off debug output.
function disable()
ON = false
end
-- }}}
-- set_output {{{
---
-- Redirects output to a file rather than stdout.
-- @param file File to write debug output to
function set_output(file)
outfile = base.assert(io.open(file))
end
-- }}}
-- }}}

69
irc/message.lua Normal file
View file

@ -0,0 +1,69 @@
---
-- Implementation of IRC server message parsing
-- initialization {{{
local base = _G
local constants = require 'irc.constants'
local ctcp = require 'irc.ctcp'
local irc_debug = require 'irc.debug'
local misc = require 'irc.misc'
local socket = require 'socket'
local string = require 'string'
local table = require 'table'
-- }}}
---
-- This module contains parsing functions for IRC server messages.
module 'irc.message'
-- internal functions {{{
-- _parse {{{
--
-- Parse a server command.
-- @param str Command to parse
-- @return Table containing the parsed message. It contains:
-- <ul>
-- <li><i>from:</i> The source of this message, in full usermask
-- form (nick!user@host) for messages originating
-- from users, and as a hostname for messages from
-- servers</li>
-- <li><i>command:</i> The command sent, in name form if possible,
-- otherwise as a numeric code</li>
-- <li><i>args:</i> Array of strings corresponding to the arguments
-- to the received command</li>
--
-- </ul>
function _parse(str)
-- low-level ctcp quoting {{{
str = ctcp._low_dequote(str)
-- }}}
-- parse the from field, if it exists (leading :) {{{
local from = ""
if str:sub(1, 1) == ":" then
local e
e, from = socket.skip(1, str:find("^:([^ ]*) "))
str = str:sub(e + 1)
end
-- }}}
-- get the command name or numerical reply value {{{
local command, argstr = socket.skip(2, str:find("^([^ ]*) ?(.*)"))
local reply = false
if command:find("^%d%d%d$") then
reply = true
if constants.replies[base.tonumber(command)] then
command = constants.replies[base.tonumber(command)]
else
irc_debug._warn("Unknown server reply: " .. command)
end
end
-- }}}
-- get the args {{{
local args = misc._split(argstr, " ", ":")
-- the first arg in a reply is always your nick
if reply then table.remove(args, 1) end
-- }}}
-- return the parsed message {{{
return {from = from, command = command, args = args}
-- }}}
end
-- }}}
-- }}}

303
irc/misc.lua Normal file
View file

@ -0,0 +1,303 @@
---
-- Various useful functions that didn't fit anywhere else
-- initialization {{{
local base = _G
local irc_debug = require 'irc.debug'
local socket = require 'socket'
local math = require 'math'
local os = require 'os'
local string = require 'string'
local table = require 'table'
-- }}}
---
-- This module contains various useful functions which didn't fit in any of the
-- other modules.
module 'irc.misc'
-- defaults {{{
DELIM = ' '
PATH_SEP = '/'
ENDIANNESS = "big"
INT_BYTES = 4
-- }}}
-- private functions {{{
--
-- Check for existence of a file. This returns true if renaming a file to
-- itself succeeds. This isn't ideal (I think anyway) but it works here, and
-- lets me not have to bring in LFS as a dependency.
-- @param filename File to check for existence
-- @return True if the file exists, false otherwise
local function exists(filename)
local _, err = os.rename(filename, filename)
if not err then return true end
return not err:find("No such file or directory")
end
-- }}}
-- internal functions {{{
-- _split {{{
--
-- Splits str into substrings based on several options.
-- @param str String to split
-- @param delim String of characters to use as the beginning of substring
-- delimiter
-- @param end_delim String of characters to use as the end of substring
-- delimiter
-- @param lquotes String of characters to use as opening quotes (quoted strings
-- in str will be considered one substring)
-- @param rquotes String of characters to use as closing quotes
-- @return Array of strings, one for each substring that was separated out
function _split(str, delim, end_delim, lquotes, rquotes)
-- handle arguments {{{
delim = "["..(delim or DELIM).."]"
if end_delim then end_delim = "["..end_delim.."]" end
if lquotes then lquotes = "["..lquotes.."]" end
if rquotes then rquotes = "["..rquotes.."]" end
local optdelim = delim .. "?"
-- }}}
local ret = {}
local instring = false
while str:len() > 0 do
-- handle case for not currently in a string {{{
if not instring then
local end_delim_ind, lquote_ind, delim_ind
if end_delim then end_delim_ind = str:find(optdelim..end_delim) end
if lquotes then lquote_ind = str:find(optdelim..lquotes) end
local delim_ind = str:find(delim)
if not end_delim_ind then end_delim_ind = str:len() + 1 end
if not lquote_ind then lquote_ind = str:len() + 1 end
if not delim_ind then delim_ind = str:len() + 1 end
local next_ind = math.min(end_delim_ind, lquote_ind, delim_ind)
if next_ind == str:len() + 1 then
table.insert(ret, str)
break
elseif next_ind == end_delim_ind then
-- TODO: hackish here
if str:sub(next_ind, next_ind) == end_delim:gsub('[%[%]]', '') then
table.insert(ret, str:sub(next_ind + 1))
else
table.insert(ret, str:sub(1, next_ind - 1))
table.insert(ret, str:sub(next_ind + 2))
end
break
elseif next_ind == lquote_ind then
table.insert(ret, str:sub(1, next_ind - 1))
str = str:sub(next_ind + 2)
instring = true
else -- last because the top two contain it
table.insert(ret, str:sub(1, next_ind - 1))
str = str:sub(next_ind + 1)
end
-- }}}
-- handle case for currently in a string {{{
else
local endstr = str:find(rquotes..optdelim)
table.insert(ret, str:sub(1, endstr - 1))
str = str:sub(endstr + 2)
instring = false
end
-- }}}
end
return ret
end
-- }}}
-- _basename {{{
--
-- Returns the basename of a file (the part after the last directory separator).
-- @param path Path to the file
-- @param sep Directory separator (optional, defaults to PATH_SEP)
-- @return The basename of the file
function _basename(path, sep)
sep = sep or PATH_SEP
if not path:find(sep) then return path end
return socket.skip(2, path:find(".*" .. sep .. "(.*)"))
end
-- }}}
-- _dirname {{{
--
-- Returns the dirname of a file (the part before the last directory separator).
-- @param path Path to the file
-- @param sep Directory separator (optional, defaults to PATH_SEP)
-- @return The dirname of the file
function _dirname(path, sep)
sep = sep or PATH_SEP
if not path:find(sep) then return "." end
return socket.skip(2, path:find("(.*)" .. sep .. ".*"))
end
-- }}}
-- _str_to_int {{{
--
-- Converts a number to a low-level int.
-- @param str String representation of the int
-- @param bytes Number of bytes in an int (defaults to INT_BYTES)
-- @param endian Which endianness to use (big, little, host, network) (defaultsi
-- to ENDIANNESS)
-- @return A string whose first INT_BYTES characters make a low-level int
function _str_to_int(str, bytes, endian)
bytes = bytes or INT_BYTES
endian = endian or ENDIANNESS
local ret = ""
for i = 0, bytes - 1 do
local new_byte = string.char(math.fmod(str / (2^(8 * i)), 256))
if endian == "big" or endian == "network" then ret = new_byte .. ret
else ret = ret .. new_byte
end
end
return ret
end
-- }}}
-- _int_to_str {{{
--
-- Converts a low-level int to a number.
-- @param int String whose bytes correspond to the bytes of a low-level int
-- @param endian Endianness of the int argument (defaults to ENDIANNESS)
-- @return String representation of the low-level int argument
function _int_to_str(int, endian)
endian = endian or ENDIANNESS
local ret = 0
for i = 1, int:len() do
if endian == "big" or endian == "network" then ind = int:len() - i + 1
else ind = i
end
ret = ret + string.byte(int:sub(ind, ind)) * 2^(8 * (i - 1))
end
return ret
end
-- }}}
-- _ip_str_to_int {{{
-- TODO: handle endianness here
--
-- Converts a string IP address to a low-level int.
-- @param ip_str String representation of an IP address
-- @return Low-level int representation of that IP address
function _ip_str_to_int(ip_str)
local i = 3
local ret = 0
for num in ip_str:gmatch("%d+") do
ret = ret + num * 2^(i * 8)
i = i - 1
end
return ret
end
-- }}}
-- _ip_int_to_str {{{
-- TODO: handle endianness here
--
-- Converts an int to a string IP address.
-- @param ip_int Low-level int representation of an IP address
-- @return String representation of that IP address
function _ip_int_to_str(ip_int)
local ip = {}
for i = 3, 0, -1 do
local new_num = math.floor(ip_int / 2^(i * 8))
table.insert(ip, new_num)
ip_int = ip_int - new_num * 2^(i * 8)
end
return table.concat(ip, ".")
end
-- }}}
-- _get_unique_filename {{{
--
-- Returns a unique filename.
-- @param filename Filename to start with
-- @return Filename (same as the one we started with, except possibly with some
-- numbers appended) which does not currently exist on the filesystem
function _get_unique_filename(filename)
if not exists(filename) then return filename end
local count = 1
while true do
if not exists(filename .. "." .. count) then
return filename .. "." .. count
end
count = count + 1
end
end
-- }}}
-- _try_call {{{
--
-- Call a function, if it exists.
-- @param fn Function to try to call
-- @param ... Arguments to fn
-- @return The return values of fn, if it was successfully called
function _try_call(fn, ...)
if base.type(fn) == "function" then
return fn(...)
end
end
-- }}}
-- _try_call_warn {{{
--
-- Same as try_call, but complain if the function doesn't exist.
-- @param msg Warning message to use if the function doesn't exist
-- @param fn Function to try to call
-- @param ... Arguments to fn
-- @return The return values of fn, if it was successfully called
function _try_call_warn(msg, fn, ...)
if base.type(fn) == "function" then
return fn(...)
else
irc_debug._warn(msg)
end
end
-- }}}
-- _value_iter {{{
--
-- Iterator to iterate over just the values of a table.
function _value_iter(state, arg, pred)
for k, v in base.pairs(state) do
if arg == v then arg = k end
end
local key, val = base.next(state, arg)
if not key then return end
if base.type(pred) == "function" then
while not pred(val) do
key, val = base.next(state, key)
if not key then return end
end
end
return val
end
-- }}}
-- _parse_user {{{
--
-- Gets the various parts of a full username.
-- @param user A usermask (i.e. returned in the from field of a callback)
-- @return nick
-- @return username (if it exists)
-- @return hostname (if it exists)
function _parse_user(user)
local found, bang, nick = user:find("^([^!]*)!")
if found then
user = user:sub(bang + 1)
else
return user
end
local found, equals = user:find("^.=")
if found then
user = user:sub(3)
end
local found, at, username = user:find("^([^@]*)@")
if found then
return nick, username, user:sub(at + 1)
else
return nick, user
end
end
-- }}}
-- }}}

47
luameat.lua Normal file
View file

@ -0,0 +1,47 @@
require "luarocks.loader"
local irc = require 'irc'
require('socket.http')
plugin = {}
callback = {}
require('config')
irc.DEBUG = true
irc.register_callback("connect", function()
for i,v in ipairs(defaulChannels) do
irc.join(v)
end
end)
irc.register_callback("channel_msg", function(channel, from, message)
local is_cmd, cmd, arg = message:match("^(@)(%w+)%s*(.*)$")
if is_cmd and plugin[cmd] then
plugin[cmd](channel.name, from, arg)
end
for k,v in pairs(callback) do
if type(v) == "function" then
v(channel.name, from, message)
end
end
end)
-- irc.register_callback("private_msg", function(from, message)
-- local is_cmd, cmd, arg = message:match("^(!)(%w+) (.*)$")
-- if is_cmd and plugin[cmd] then
-- plugin[cmd](from, from, arg)
-- end
-- for k,v in pairs(callback) do
-- if type(v) == "function" then
-- v(from, from, message)
-- end
-- end
-- end)
--[[
irc.register_callback("nick_change", function(from, old_nick)
end)
--]]
irc.connect{network = network, port = port, nick = nick, username = username, realname = realname, pass = password}

59
plugin/calc.lua Normal file
View file

@ -0,0 +1,59 @@
-- Nice plugin that uses google's calculator
-- Google's calculator is fun!
-- How to use:
-- !calc <Operation>
-- Example:
-- !calc 1+1
-- !calc sin(0.5)
-- !calc speed of light / sin(0.5)
local socket = require("socket")
local url = require("socket.url")
local json = require("json")
plugin.calc = function (target, from, arg)
local sb_env= {ipairs = ipairs,
next = next,
pairs = pairs,
pcall = pcall,
tonumber = tonumber,
tostring = tostring,
type = type,
unpack = unpack,
coroutine = { create = coroutine.create, resume = coroutine.resume,
running = coroutine.running, status = coroutine.status,
wrap = coroutine.wrap },
string = { byte = string.byte, char = string.char, find = string.find,
format = string.format, gmatch = string.gmatch, gsub = string.gsub,
len = string.len, lower = string.lower, match = string.match,
rep = string.rep, reverse = string.reverse, sub = string.sub,
upper = string.upper },
table = { insert = table.insert, maxn = table.maxn, remove = table.remove,
sort = table.sort },
math = { abs = math.abs, acos = math.acos, asin = math.asin,
atan = math.atan, atan2 = math.atan2, ceil = math.ceil, cos = math.cos,
cosh = math.cosh, deg = math.deg, exp = math.exp, floor = math.floor,
fmod = math.fmod, frexp = math.frexp, huge = math.huge,
ldexp = math.ldexp, log = math.log, log10 = math.log10, max = math.max,
min = math.min, modf = math.modf, pi = math.pi, pow = math.pow,
rad = math.rad, random = math.random, sin = math.sin, sinh = math.sinh,
sqrt = math.sqrt, tan = math.tan, tanh = math.tanh },
os = { clock = os.clock, difftime = os.difftime, time = os.time },}
local func = load("return function() return "..arg.." end", "IIRC", "t", sb_env)
local status, result
if func then
status, result = pcall(func())
else
status = true
result = "INVALID CALL"
end
local r
if status then
if not result then result = "nil" end
r = arg .. " = " .. tostring(result)
else
r = "ERROR: " .. result
end
irc.say(target,r)
end

64
plugin/google.lua Normal file
View file

@ -0,0 +1,64 @@
-- The google plugin, just type:
-- !google <stuff to search>
local urltool = require("socket.url")
local json = require("json")
local socket = require("socket")
local htmlEntities = {
["&nbsp;"] = " ",
["&#160;"] = " ",
["&quot;"] = '"',
["&#34;"] = '"',
["&apos;"] = "'",
["&#39;"] = "'",
["&amp;"] = "&",
["&#38;"] = "&",
["&lt;"] = "<",
["&#60;"] = "<",
["&gt;"] = ">",
["&#62;"] = ">",
["&iexcl;"] = "¡",
["&#161;"] = "¡",
["&acute;"] = "´",
["&#180;"] = "´",
["&Ntilde;"] = "Ñ",
["&#209;"] = "Ñ",
["&ntilde;"] = "ñ",
["&#241;"] = "ñ",
}
local function parseHtmlEntites(s)
local ret =string.gsub(s,"&.-;", function(input) return htmlEntities[input] or "?" end)
return ret
end
plugin.google = function (target, from, arg)
local search = urltool.escape(arg)
local result, error, header = socket.http.request( 'http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q='..search.."&safe=off")
if error == 200 and result then
local jsonTable = json.decode(result)
if (jsonTable.responseData.results[1]) then
--unicode hax
content = string.gsub(urltool.unescape(jsonTable.responseData.results[1].content), "u(%x%x%x%x)",
function(c)
local status, result = pcall(string.char, "0x"..c)
if status then
return result
else
return "u"..c
end
end)
--parse html tags to irc tags
content = string.gsub(content,"(<b>)", "")
content = string.gsub(content,"(</b>)", "")
content = string.gsub(content,"(<u>)", "")
content = string.gsub(content,"(</u>)", "")
content = string.gsub(content,"(<i>)", "")
content = string.gsub(content,"(</i>)", "")
result = urltool.unescape(jsonTable.responseData.results[1].url).." "..content
irc.say(target, parseHtmlEntites(result))
end
end
end

47
plugin/twitch.lua Normal file
View file

@ -0,0 +1,47 @@
-- Getting information out of Twitch
local json = require("json")
local https = require("ssl.https")
local ltn12 = require("ltn12")
local function fetch_data (endpoint)
local response = {}
local r,c,h = https.request{url = "https://api.twitch.tv/kraken/"..endpoint,
headers = { accept = "application/vnd.twitchtv.v3+json"},
sink = ltn12.sink.table(response)}
return json.decode(table.concat(response))
end
local function elapsed (datestring)
local pattern = "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z"
local year,month,day,hour,min,sec = datestring:match(pattern)
local start = os.time({year = year, month = month, day = day, hour = hour, min = min, sec = sec, isdst = false })
local utcnow = os.time(os.date("!*t"))
local diff = utcnow-start
local hours = math.floor(diff/3600)
local mins = math.floor(diff/60 - hours*60)
h = hours > 1 and " hours" or " hour"
m = mins > 1 and " minutes" or " minute"
if hours > 0 then
return hours..h.." and "..mins..m
else
return mins..m
end
end
plugin.uptime = function (target, from, arg)
local channel = string.sub(target, 2)
if arg and arg ~= "" then
channel = arg
end
local j = fetch_data("streams/"..channel)
if j.stream then
if j.stream ~= json.decode("null") then
irc.say(target, j.stream.channel.display_name.." is streaming ["..j.stream.game.."] for "..elapsed(j.stream.created_at)..".")
else
irc.say(target, channel.." is not streaming anything right now.")
end
else
irc.say(target, "Stream "..channel.." not found.")
end
end

22
plugin/youtube.lua Normal file
View file

@ -0,0 +1,22 @@
local https = require("ssl.https")
local json = require("json")
local ltn12 = require("ltn12")
local function fetch_title (videoid)
--local response = {}
local r,c,h = https.request("https://www.googleapis.com/youtube/v3/videos?id="..videoid.."&key="..yt_api_key.."&part=snippet&fields=items(snippet(title))")
local j = json.decode(r)
if j then
return j.items[1].snippet.title
end
end
callback.youtube = function (target, from, message)
local ytid = message:match(".*https?://w?w?w?%.?youtube.com/watch%?v=(%g*)%s*.*")
if ytid then
local t = fetch_title(ytid)
if t then
irc.say(target, "Youtube video title: "..t)
end
end
end