Skip to content

Instantly share code, notes, and snippets.

@Fingercomp
Created March 8, 2020 12:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Fingercomp/df483bc2cefa13e0422d656ae82495ac to your computer and use it in GitHub Desktop.
Save Fingercomp/df483bc2cefa13e0422d656ae82495ac to your computer and use it in GitHub Desktop.
IRC bridge
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