-
-
Save Fingercomp/df483bc2cefa13e0422d656ae82495ac to your computer and use it in GitHub Desktop.
IRC bridge
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local event = require("event") | |
local fs = require("filesystem") | |
local srl = require("serialization") | |
local thread = require("thread") | |
local irc = require("irc") | |
local events = irc.events | |
local priority = events.priority | |
local com = require("component") | |
local chatbox = com.admin_chatbox | |
local debug = com.debug | |
local gpu = com.gpu | |
local colors = {"02", "03", "04", "13", "08", "09", "11", "06"} | |
local function formatNick(nick) | |
local modulo = #colors + 1 | |
local result = 0 | |
for char in nick:gmatch(".") do | |
result = (result + char:byte()) % modulo | |
end | |
if result == 0 then | |
return ("\x0f%s"):format(nick) | |
else | |
return ("\x02\x03%s%s"):format(colors[result], nick) | |
end | |
end | |
local defaultCfg = { | |
channel = "", | |
nickname = "s1-conversationalist", | |
username = "conversationalist", | |
realname = "IRC-MC bridge relay", | |
account = "s1-conversationlist", | |
accountPassword = "", | |
mcMessageFormat = "§3[§lIRC§3] §7%s§8: §r%s", | |
mcPmFormat = "§3[§lIRC§3 PM] §7%s§8 -> §7%s§8: §r%s", | |
ircMessageFormat = "%s\x0f: %s", | |
ircPmFormat = "\x0315PM: %s \x0f\x0315-> %s\x0f: %s", | |
ircJoinFormat = "\x0315* %s \x0309has joined the server\x0315.", | |
ircQuitFormat = "\x0315* %s \x0305has left the server\x0315.", | |
ircDeathFormat = "\x0315* %s \x0306has died: \x0f%s\x0315.", | |
mcCommandPrefix = "IRC: ", | |
mcResponseFormat = "§3[§lIRC§3] §r%s", | |
gameAdmins = {}, | |
ircAdmins = {}, | |
ircWhitelist = {}, | |
mcBlacklist = {}, | |
aliases = {}, | |
ircPm = {}, | |
mcPm = {}, | |
debug = false, | |
} | |
local cfgPath = | |
os.getenv("CONVERSATIONALIST_CONFIG") or "/etc/conversationalist.cfg" | |
assert(not fs.exists(cfgPath) or not fs.isDirectory(cfgPath), | |
("%s is a directory"):format(cfgPath)) | |
local cfg | |
local function saveCfg() | |
local serialized = srl.serialize(cfg) | |
local f = io.open(cfgPath, "w") | |
f:write(serialized) | |
f:close() | |
end | |
if fs.exists(cfgPath) then | |
local f = io.open(cfgPath, "r") | |
cfg = srl.unserialize(f:read("*a")) | |
f:close() | |
else | |
cfg = defaultCfg | |
saveCfg() | |
print(("Configuration file written to %s"):format(cfgPath)) | |
print("Edit and run the program again.") | |
os.exit(0) | |
end | |
local function isini(tbl, str) | |
str = str:lower() | |
for k, v in pairs(tbl) do | |
if v:lower() == str then | |
return true, k | |
end | |
end | |
return false | |
end | |
local fg | |
local function log(color, text) | |
if fg ~= color then | |
gpu.setForeground(color) | |
fg = color | |
end | |
print(text) | |
end | |
local function sanitizeMessage(message) | |
return message:gsub("§.?", ""):gsub("%c", "") | |
end | |
local handleCommand | |
local client = irc.builder() | |
:connection { | |
host = "irc.esper.net:6667", | |
throttling = { | |
maxDelay = 2, | |
maxThroughput = 5, | |
}, | |
} | |
:auth { | |
username = cfg.username, | |
nickname = cfg.nickname, | |
realname = cfg.realname, | |
} | |
:account { | |
nickname = cfg.account, | |
password = cfg.accountPassword, | |
} | |
:bot { | |
channels = {cfg.channel}, | |
tracking = { | |
account = true, | |
users = true, | |
modes = true, | |
} | |
} | |
:execution { | |
threaded = true, | |
reconnect = true, | |
catchErrors = true, | |
} | |
:subscribe(events.irc.message, priority.normal, function(self, client, evt) | |
if evt.target ~= cfg.channel then | |
return | |
end | |
local user = client:getUser(evt.source) | |
if not user or not user.account then | |
return | |
end | |
if not isini(cfg.ircWhitelist, user.account) then | |
return | |
end | |
local message = sanitizeMessage(evt.message) | |
log(0xffdb80, ("IRC → %s: %s"):format(user.account, message)) | |
event.push("chat::message", user.account, message) | |
end) | |
:subscribe(events.irc.notice, priority.normal, function(self, client, evt) | |
if evt.target ~= "@" .. cfg.channel then | |
return | |
end | |
local user = client:getUser(evt.source) | |
if not user or not user.account then | |
return | |
end | |
local admin = isini(cfg.ircAdmins, user.account) | |
log(0xffff00, ("! IRC → %s: %s"):format(user.account, evt.message)) | |
handleCommand(evt, user.account, evt.message, admin) | |
end) | |
:subscribe(events.client.connected, priority.normal, | |
function(self, client, evt) | |
log(0x00ff80, "* Connected") | |
end) | |
:subscribe(events.client.disconnected, priority.normal, | |
function(self, client, evt) | |
log(0xff0000, "* Disconnected") | |
end) | |
:subscribe(events.client.registered, priority.normal, | |
function(self, client, evt) | |
log(0x99ff80, "* Registered as " .. client.botUser.nickname) | |
end) | |
:subscribe(events.client.authenticated, priority.normal, | |
function(self, client, evt) | |
log(0x99ff80, "* Authenticated") | |
event.push("chat::authenticated") | |
end) | |
:subscribe(events.client.error, priority.top, function(self, client, evt) | |
log(0xff0000, ("Caught error: %s"):format(evt.traceback)) | |
evt:cancel() | |
end) | |
:subscribe(events.irc.write, priority.high, function(self, client, evt) | |
if cfg.debug then | |
log(0x999999, "← " .. evt.line:gsub("[\r\n]+$", "")) | |
end | |
end) | |
:subscribe(events.irc.command, priority.high, function(self, client, evt) | |
if cfg.debug then | |
log(0xcccccc, "→ " .. evt.rawLine) | |
end | |
end) | |
:build() | |
local function formatting(irc, mc) | |
return function(data) | |
return { | |
compile = function(self, isIrc) | |
local compiled = {} | |
for _, v in ipairs(data) do | |
if type(v) == "string" then | |
table.insert(compiled, v) | |
else | |
table.insert(compiled, v:compile(isIrc)) | |
end | |
end | |
return (isIrc and irc or mc):format(table.concat(compiled)) | |
end | |
} | |
end | |
end | |
local bold = formatting("\x02%s", "§l%s") | |
local reset = formatting("\x0f%s", "§r%s") | |
local red = formatting("\x0305%s", "§c%s") | |
local green = formatting("\x0309%s", "§a%s") | |
local cyan = formatting("\x0311%s", "§3%s") | |
local fmt = formatting("%s", "%s") | |
local function reply(irc, nickname, msg) | |
if type(msg) ~= "string" then | |
msg = msg:compile(irc) | |
end | |
if irc then | |
client:notice(irc.source.nickname, | |
("[%s] %s"):format(cfg.channel, msg)) | |
else | |
chatbox.tell(nickname, cfg.mcResponseFormat:format(msg)) | |
end | |
end | |
local function split(str, n) | |
n = n or math.huge | |
local parts = {} | |
local i = 1 | |
for part, pos in str:gmatch("(%S+)()") do | |
if n == 1 then | |
break | |
end | |
table.insert(parts, part) | |
i = pos | |
n = n - 1 | |
end | |
local lastPart = str:match("^%s*(%S.-)%s*$", i) | |
if lastPart ~= "" then | |
table.insert(parts, lastPart) | |
end | |
return table.unpack(parts) | |
end | |
local commands = { | |
irc = function(self, irc, nickname, msg, admin) | |
if not admin then | |
return | |
end | |
local action, subaction, arg | |
if msg then | |
action, subaction, arg = split(msg, 3) | |
end | |
if action == "admin" then | |
if subaction == "list" then | |
reply(irc, nickname, fmt { | |
bold {"IRC bot admins: "}, | |
reset {table.concat(cfg.ircAdmins, ", ")} | |
}) | |
elseif subaction == "add" and arg then | |
table.insert(cfg.ircAdmins, arg) | |
saveCfg() | |
reply(irc, nickname, bold {"Admin added."}) | |
elseif subaction == "del" and arg then | |
local _, k = isini(cfg.ircAdmins, arg) | |
if k then | |
table.remove(cfg.ircAdmins, k) | |
saveCfg() | |
reply(irc, nickname, bold {"Admin removed."}) | |
else | |
reply(irc, nickname, red {"No such user."}) | |
end | |
else | |
reply(irc, nickname, | |
red {"Available commands: ", reset {"list, add, del"}}) | |
end | |
elseif action == "whitelist" then | |
if subaction == "list" then | |
reply(irc, nickname, fmt { | |
bold {"IRC whitelist: "}, | |
reset {table.concat(cfg.ircWhitelist, ", ")} | |
}) | |
elseif subaction == "add" and arg then | |
if not isini(cfg.ircWhitelist, arg) then | |
table.insert(cfg.ircWhitelist, arg) | |
end | |
saveCfg() | |
reply(irc, nickname, bold {"User added."}) | |
elseif subaction == "del" and arg then | |
local _, k = isini(cfg.ircWhitelist, arg) | |
if k then | |
table.remove(cfg.ircWhitelist, k) | |
saveCfg() | |
reply(irc, nickname, bold {"User removed."}) | |
else | |
reply(irc, nickname, red {"No such user."}) | |
end | |
else | |
reply(irc, nickname, | |
red {"Available commands: ", reset {"list, add, del"}}) | |
end | |
elseif action == "alias" then | |
local args = arg and {split(arg, 2)} or {} | |
if subaction == "get" and args[1] then | |
reply(irc, nickname, bold { | |
"Alias for ", | |
args[1], | |
": ", | |
reset {cfg.aliases[args[1]] or cyan {"not set"}} | |
}) | |
elseif subaction == "set" and args[1] then | |
cfg.aliases[args[1]] = args[2] | |
saveCfg() | |
if args[2] then | |
reply(irc, nickname, bold { | |
"Alias for ", | |
args[1], | |
" set to ", | |
reset {args[2]} | |
}) | |
else | |
reply(irc, nickname, bold { | |
"Alias for ", | |
args[1], | |
" has been removed." | |
}) | |
end | |
else | |
reply(irc, nickname, | |
red {"Available commands: ", reset {"get, set"}}) | |
end | |
else | |
reply(irc, nickname, | |
red {"Available commands: ", reset {"admin, whitelist, alias"}}) | |
end | |
end, | |
mc = function(self, irc, nickname, msg, admin) | |
if not admin then | |
return | |
end | |
local action, subaction, arg | |
if msg then | |
action, subaction, arg = split(msg, 3) | |
end | |
if action == "admin" then | |
if subaction == "list" then | |
reply(irc, nickname, fmt { | |
bold {"MC bot admins: "}, | |
reset {table.concat(cfg.gameAdmins, ", ")} | |
}) | |
elseif subaction == "add" and arg then | |
if not isini(cfg.gameAdmins, arg) then | |
table.insert(cfg.gameAdmins, arg) | |
end | |
saveCfg() | |
reply(irc, nickname, bold {"Admin added."}) | |
elseif subaction == "del" and arg then | |
local _, k = isini(cfg.gameAdmins, arg) | |
if k then | |
table.remove(cfg.gameAdmins, k) | |
saveCfg() | |
reply(irc, nickname, bold {"Admin removed."}) | |
else | |
reply(irc, nickname, red {"No such user."}) | |
end | |
else | |
reply(irc, nickname, | |
red {"Available commands: ", reset {"list, add, del"}}) | |
end | |
elseif action == "blacklist" then | |
if subaction == "list" then | |
reply(irc, nickname, fmt { | |
bold {"MC blacklist: "}, | |
reset {table.concat(cfg.mcBlacklist, ", ")} | |
}) | |
elseif subaction == "add" and arg then | |
if not isini(cfg.mcBlacklist, arg) then | |
table.insert(cfg.mcBlacklist, arg) | |
end | |
saveCfg() | |
reply(irc, nickname, bold {"User added."}) | |
elseif subaction == "del" and arg then | |
local _, k = isini(cfg.mcBlacklist, arg) | |
if k then | |
table.remove(cfg.mcBlacklist, k) | |
saveCfg() | |
reply(irc, nickname, bold {"User removed."}) | |
else | |
reply(irc, nickname, red {"No such user."}) | |
end | |
else | |
reply(irc, nickname, | |
red {"Available commands: ", reset {"list, add, del"}}) | |
end | |
else | |
reply(irc, nickname, | |
red {"Available commands: ", reset {"admin, blacklist"}}) | |
end | |
end, | |
debug = function(self, irc, nickname, msg, admin) | |
if not admin then | |
return | |
end | |
local msg = msg and split(msg, 1) | |
if msg == "on" then | |
cfg.debug = true | |
saveCfg() | |
reply(irc, nickname, bold {"Debug mode turned on."}) | |
elseif msg == "off" then | |
cfg.debug = false | |
saveCfg() | |
reply(irc, nickname, bold {"Debug mode turned off."}) | |
elseif not msg then | |
reply(irc, nickname, | |
fmt {"Debug mode is ", | |
bold {cfg.debug and red {"on"} or green {"off"}}, reset {"."}}) | |
end | |
end, | |
online = function(self, irc, nickname, msg, admin) | |
local users | |
if irc then | |
users = debug.getPlayers() | |
else | |
users = {} | |
for user, prefix in pairs(client:getChannel(cfg.channel).users) do | |
if user.account and user ~= client.botUser then | |
table.insert(users, ("§a%s§r%s"):format(prefix, user.account)) | |
end | |
end | |
end | |
reply(irc, nickname, | |
bold {"Users online: ", reset {table.concat(users, ", ")}}) | |
end, | |
pm = function(self, irc, nickname, msg, admin) | |
local section = cfg[irc and "ircPm" or "mcPm"] | |
local args = msg and {split(msg, 3)} or {} | |
if args[1] == "on" then | |
section[nickname:lower()] = {} | |
saveCfg() | |
reply(irc, nickname, bold { | |
"PMs from ", | |
irc and "MC" or "IRC", | |
" have been ", | |
green {"enabled"}, | |
}) | |
elseif args[1] == "off" then | |
section[nickname:lower()] = false | |
saveCfg() | |
reply(irc, nickname, bold { | |
"PMs from ", | |
irc and "MC" or "IRC", | |
" have been ", | |
red {"disabled"} | |
}) | |
elseif args[1] == "ignore" then | |
local ignoreList = section[nickname:lower()] | |
if not ignoreList then | |
reply(irc, nickname, red { | |
"PMs from ", | |
irc and "MC" or "IRC", | |
" are disabled." | |
}) | |
elseif args[2] == "list" then | |
reply(irc, nickname, bold { | |
"Your ignore list: ", | |
reset {table.concat(ignoreList, ", ")} | |
}) | |
elseif args[2] == "add" and args[3] then | |
local name = args[3]:lower() | |
if not irc then | |
for k, v in pairs(cfg.aliases) do | |
if v:lower() == name then | |
name = k:lower() | |
break | |
end | |
end | |
end | |
if not isini(ignoreList, name) then | |
table.insert(ignoreList, name) | |
saveCfg() | |
end | |
reply(irc, nickname, bold {"You are now ignoring ", reset {name}}) | |
elseif args[2] == "del" and args[3] then | |
local _, k = isini(ignoreList, args[3]:lower()) | |
if k then | |
table.remove(ignoreList, k) | |
saveCfg() | |
reply(irc, nickname, bold { | |
"You no longer ignore ", | |
reset {args[3]:lower()} | |
}) | |
else | |
reply(irc, nickname, red {"No such user."}) | |
end | |
else | |
reply(irc, nickname, | |
red {"Available commands: ", reset {"list, add, del"}}) | |
end | |
elseif not args[1] then | |
local enabled = section[nickname:lower()] | |
if irc and enabled == nil then | |
enabled = true | |
end | |
reply(irc, nickname, bold { | |
"PMs from ", | |
irc and "MC" or "IRC", | |
" are ", | |
enabled and green {"enabled"} or red {"disabled"} | |
}) | |
else | |
reply(irc, nickname, | |
red {"Available commands: ", reset {"on, off, ignore"}}) | |
end | |
end, | |
msg = function(self, irc, nickname, msg, admin) | |
local addressee, message = split(msg, 2) | |
if not addressee or not message then | |
reply(irc, nickname, red {"Invalid syntax."}) | |
return | |
end | |
local ignoreList = cfg[irc and "mcPm" or "ircPm"][addressee:lower()] | |
if (irc and not ignoreList) or (not irc and ignoreList == false) then | |
reply(irc, nickname, bold { | |
addressee, | |
reset {red {" has not enabled PMs from ", irc and "MC" or "IRC"}} | |
}) | |
return | |
end | |
local ignored = false | |
local reason, player, user | |
if ignoreList and isini(ignoreList, nickname) then | |
ignored = true | |
end | |
if irc then | |
local players = debug.getPlayers() | |
for i, nick in ipairs(players) do | |
if nick:lower() == addressee:lower() then | |
player = nick | |
break | |
end | |
end | |
if player then | |
if not ignored then | |
chatbox.tell(player, cfg.mcPmFormat:format(nickname, player, message)) | |
end | |
else | |
reason = "no such user" | |
end | |
else | |
for u in pairs(client:getChannel(cfg.channel).users) do | |
if u.account and u.account:lower() == addressee:lower() then | |
user = u | |
break | |
end | |
end | |
if user then | |
if not ignored then | |
client:notice(user.nickname, | |
("[%s]"):format(cfg.channel) .. | |
cfg.ircPmFormat:format( | |
formatNick(nickname), | |
formatNick(user.account), | |
message | |
)) | |
end | |
else | |
reason = "no such user" | |
end | |
end | |
if reason then | |
reply(irc, nickname, red {"Message was not sent: ", reason, "."}) | |
else | |
if irc then | |
reply(irc, nickname, fmt {cfg.ircPmFormat:format( | |
formatNick(nickname), | |
formatNick(player), | |
message | |
)}) | |
else | |
chatbox.tell( | |
nickname, | |
cfg.mcPmFormat:format(nickname, user.account, message) | |
) | |
end | |
end | |
end, | |
} | |
function handleCommand(irc, nickname, msg, admin) | |
local cmd, rest = split(msg, 2) | |
if not cmd then | |
return | |
end | |
if not commands[cmd] then | |
return | |
end | |
commands[cmd](commands[cmd], irc, nickname, rest, admin) | |
end | |
local chatThread = thread.create(function() | |
event.listen("chat::message", function(evt, nickname, message) | |
nickname = cfg.aliases[nickname] or nickname | |
chatbox.say(cfg.mcMessageFormat:format(nickname, message)) | |
end) | |
event.listen("chat::authenticated", function() | |
event.listen("chat_message", | |
function(evt, addr, dim, x, y, z, dist, nickname, msg) | |
if isini(cfg.mcBlacklist, nickname) then | |
return | |
end | |
log(0x66dbff, (" MC → %s: %s"):format(nickname, msg)) | |
client:msg(cfg.channel, | |
cfg.ircMessageFormat:format(formatNick(nickname), msg)) | |
end) | |
event.listen("chat_command", | |
function(evt, addr, dim, x, y, z, dist, nickname, msg) | |
if msg:sub(1, #cfg.mcCommandPrefix) ~= cfg.mcCommandPrefix then | |
return | |
end | |
msg = msg:sub(#cfg.mcCommandPrefix + 1) | |
local admin = isini(cfg.gameAdmins, nickname) | |
log(0xffff00, ("! MC → %s: %s"):format(nickname, msg)) | |
handleCommand(false, nickname, msg, admin) | |
end) | |
event.listen("player_join", function(evt, addr, dim, x, y, z, dist, nickname) | |
log(0x66dbff, (" MC → %s has joined the server"):format(nickname)) | |
client:msg(cfg.channel, cfg.ircJoinFormat:format(formatNick(nickname))) | |
end) | |
event.listen("player_quit", function(evt, addr, dim, x, y, z, dist, nickname) | |
log(0x66dbff, (" MC → %s has left the server"):format(nickname)) | |
client:msg(cfg.channel, cfg.ircQuitFormat:format(formatNick(nickname))) | |
end) | |
event.listen("player_death", | |
function(evt, addr, dim, x, y, z, dist, nickname, reason, msg) | |
if isini(cfg.mcBlacklist, nickname) then | |
return | |
end | |
reason = msg or reason | |
log(0x66dbff, | |
(" MC → %s has died (%s)"):format(nickname, reason)) | |
client:msg(cfg.channel, | |
cfg.ircDeathFormat:format(formatNick(nickname), reason)) | |
end) | |
end) | |
log(0x0092ff, "* Press ^C to quit...") | |
repeat | |
local evt = event.pull("interrupted") | |
until evt | |
end) | |
client:run() | |
thread.waitForAny({client.thread, chatThread}) | |
log(0xff4000, "* Quitting") | |
client:stop("Quitting.") | |
gpu.setForeground(0xffffff) | |
os.exit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment