Compare commits

..

11 Commits

22 changed files with 610 additions and 100 deletions

View File

@ -2,4 +2,10 @@ FROM xena/lua:5.3
WORKDIR /ketracel
COPY . .
ENV PATH $PATH:/root/.luarocks/bin
RUN ./scripts/build_docker.sh
RUN apk --no-cache add ngircd \
&& luarocks-5.3 install --local moonscript \
&& luarocks-5.3 install --local busted \
&& luarocks-5.3 install --local --only-deps ./ketracel*.rockspec \
&& ./scripts/test.sh \
&& ./scripts/build_docker.sh \
&& apk del ngircd

View File

@ -2,4 +2,4 @@
export LUA_PATH='/root/.luarocks/share/lua/5.3/?.lua;/root/.luarocks/share/lua/5.3/?/init.lua'
export LUA_CPATH='/root/.luarocks/lib/lua/5.3/?.so'
lua5.3 -l ketracel
lua5.3 -l ketracel.main

View File

@ -16,11 +16,19 @@ dependencies = {
"lua ~> 5.3",
"irc-engine",
"luasocket",
"ln",
"uuid",
}
build = {
type = "builtin",
modules = {
["ketracel"] = "src/ketracel.lua",
["ketracel.bots.commands"] = "src/ketracel/bots/commands.lua",
["ketracel.bots.ketracel"] = "src/ketracel/bots/ketracel.lua",
["ketracel.main"] = "src/ketracel/main.lua",
["ketracel.server"] = "src/ketracel/server.lua",
["irce.modules.ngircd"] = "src/irce/modules/ngircd/init.lua",
["irce.modules.ngircd.channel"] = "src/irce/modules/ngircd/channel.lua",
["irce.modules.ngircd.modes"] = "src/irce/modules/ngircd/modes.lua",
["irce.modules.oper"] = "src/irce/modules/oper.lua",
},
install = {

17
scripts/dev_ipad_run.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
moonc-5.3 `find -type f | grep -v spec | grep 'moon$'`
ngircd -f `pwd`/spec/ngircd.conf
function trap_ctrlc () {
killall ngircd
exit 0
}
trap "trap_ctrlc" 2
cd src
export KETRACEL_DEBUG=yes
export KETRACEL_SPASS=hunter2
lua5.3 -l set_paths ketracel/main.lua
trap_ctrlc

View File

@ -2,4 +2,4 @@
tup
cd src
godotenv -f ../.env lua5.3 -l set_paths ketracel.lua
godotenv -f ../.env lua5.3 -l set_paths ./ketracel/main.lua

4
scripts/test.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
moonc `find -type f | grep -v spec | grep 'moon$'`
busted -o TAP

46
spec/ngircd.conf Normal file
View File

@ -0,0 +1,46 @@
[Global]
Name = test.localhost
Info = oh god
Listen = 0.0.0.0
MotdPhrase = testing
Network = ShadowNET
Ports = 6667
ServerGID = 65534
ServerUID = 65534
[Limits]
MaxJoins = 50
MaxNickLength = 31
MaxListSize = 100
PingTimeout = 120
PongTimeout = 20
[Options]
AllowedChannelTypes = #&+
AllowRemoteOper = yes
CloakUserToNick = yes
DNS = no
Ident = no
MorePrivacy = no
NoticeBeforeRegistration = yes
OperCanUseMode = yes
OperChanPAutoOp = yes
PAM = no
PAMIsOptional = yes
RequireAuthPing = yes
[Channel]
Name = #lobby
Topic = Welcome to the new ShadowNET!
Modes = tn
[Operator]
Name = Cadey
Password = hunter2
[Server]
Name = ketracel.akua
Passive = yes
MyPassword = hunter2
PeerPassword = hunter2
ServiceMask = Ketracel,*Serv

View File

@ -0,0 +1,27 @@
Channel = require "irce.modules.ngircd.channel"
describe "Channel", ->
it "requires a channel name", ->
assert.has.errors -> Channel {}
it "lets you create a channel", ->
assert.truthy Channel name: "#butts"
it "lets you set modes", ->
with Channel name: "#butts"
\add_mode "p"
assert .modes[1] == "p"
\del_mode "p"
assert #.modes == 0
it "requires channel membership to give hats", ->
with Channel name: "#butts"
assert.has.errors -> \add_hat "cadey", "@"
assert.has.errors -> \del_hat "cadey", "@"
it "lets you give people hats", ->
with Channel name: "#butts", members: {cadey: {}}
\add_hat "cadey", "@"
assert .members.cadey[1] == "o"
\del_hat "cadey", "@"
assert #.members.cadey == 0

View File

@ -0,0 +1,87 @@
irce = require "irce"
socket = require "socket"
test = require "ketracel.test"
Server = require("ketracel.server").Server
uuid = require "uuid"
ln = require "ln"
describe "ngircd protocol support", ->
local server
local client
setup ->
file = io.popen "ngircd -n -f ./spec/ngircd.conf", "r"
server = Server
server: "127.0.0.1"
sname: "ketracel.akua"
spass: "hunter2"
sreal: "Ketracel"
nicklen: "31"
debug: os.getenv "DEBUG"
client = test.Client "test_user"
teardown ->
with io.popen "killall ngircd", "r"
\close!
it "actually is running ngircd", ->
with io.popen "pstree"
data = \read "*all"
assert.truthy string.find data, "ngircd"
it "sees end of burst", ->
server\wait_for "376"
it "can use the Client class", ->
client\wait_for "005"
it "sees the client", ->
server\wait_for "NICK", (source, user) ->
user.nick == "test_user"
local channame
it "sees the client join a new channel", ->
channame = uuid()
client.irc\JOIN "#" .. channame
server\wait_for "JOIN", (who, chan) ->
chan.name == "#" .. channame
client\wait_for "JOIN", (sender, channel) ->
channel\sub(2) == channame
it "sees the client part the channel", ->
client.irc\PART "#" .. channame
server\wait_for "PART", (who, chan) ->
chan.name == "#" .. channame
it "sees nickchanges", ->
client.irc\send_raw "NICK newnick"
server\wait_for "NICKCHG", (sender, clinfo) ->
clinfo.nick == "newnick"
it "sees quits", ->
cli = test.Client "quitter"
cli\wait_for "005"
cli.irc\QUIT "bye"
server\wait_for "QUIT", (sender, msg) ->
sender == "quitter"
it "sees a privmsg", ->
client.irc\JOIN "#ketracel"
client.irc\PRIVMSG "#ketracel", "hi"
server\wait_for "PRIVMSG", (sender, params) ->
params[1] == "#ketracel" and params[2] == "hi"
describe "ketracel bot", ->
it "has a working VHOST command", ->
client.irc\PRIVMSG "Ketracel", "VHOST my.cool.vhost"
server\wait_for "PRIVMSG", (sender, params) ->
params[1] == "Ketracel" and params[2] == "VHOST my.cool.vhost"
client\wait_for "396"
client\wait_for "NOTICE", (sender, origin, message, pm) ->
message\find "my.cool.vhost"
it "replies to unknown commands", ->
client.irc\PRIVMSG "KETRACEL", uuid()
server\wait_for "PRIVMSG"
client\wait_for "NOTICE"

View File

@ -0,0 +1,20 @@
modes = require "irce.modules.ngircd.modes"
describe "mode conversion", ->
it "has modes2prefix", ->
assert.truthy modes.mode2prefix
it "has prefix2modes", ->
assert.truthy modes.prefix2mode
it "has convert", ->
assert.truthy modes.convert
describe "convert", ->
for k, v in pairs modes.mode2prefix
msg = string.format "%s: %s", k, v
it msg, ->
assert.equal(modes.convert(k), v)
msg = string.format "%s: %s", v, k
it msg, ->
assert.equal(modes.convert(v), k)

View File

@ -1,40 +0,0 @@
irce = require "irce"
moon = require "moon"
stringx = require "pl.stringx"
{
hooks:
["376"]: (state) =>
self\NICK "Ketracel", "white", "the.dominion", "+io", "Ketracel White"
self\NJOIN "#ketracel", "@Ketracel"
--["NJOIN"]: (state, chan) =>
-- self\NJOIN chan, "Ketracel"
["PRIVMSG"]: (state, sender, params) =>
cmdchar = params[2]\sub 1, 1
destsigil = params[1]\sub 1, 1
if params[1]\lower! == "ketracel"
sp = stringx.split params[2]
cmd = sp[1]
table.remove sp, 1
self\handle("Ketracel command", sender[1], params[1], cmd, sp)
if cmdchar == "?" and destsigil == "#"
sp = stringx.split params[2]
cmd = string.sub sp[1], 2
table.remove sp, 1
self\handle("Ketracel command", sender[1], params[1], cmd, sp)
["Ketracel command"]: (state, sender, target, cmd, args) =>
switch string.upper cmd
when "VHOST"
if #args > 0
self\VHOST sender, args[1]
self\PRIVMSG "Ketracel", sender, "your vhost is now " ..args[1]
else
self\PRIVMSG "Ketracel", sender, "usage: VHOST <your.vhost>"
else
self\PRIVMSG "Ketracel", sender, "i don't know " .. cmd
}

View File

@ -0,0 +1,43 @@
modeconv = require "irce.modules.ngircd.modes"
class Channel
new: (data) =>
assert data.name
data.members = {} unless data.members
data.modes = {} unless data.modes
data.topic = "" unless data.topic
for k, v in pairs data
self[k] = v
add_hat: (who, mode) =>
hat = modeconv.convert mode
usermodes = assert @members[who\lower!]
for _, chhat in pairs usermodes
return if chhat == hat
usermodes[#usermodes+1] = hat
del_hat: (who, mode) =>
hat = modeconv.convert mode
usermodes = assert @members[who\lower!]
for i, v in pairs usermodes
if v == hat
table.remove usermodes, i
return
add_mode: (mode) =>
for _, chmd in pairs @modes
return if chmd == mode
@modes[#@modes+1] = mode
del_mode: (mode) =>
for i, v in pairs @modes
if v == mode
table.remove @modes, i
return
string.format "desync detected: mode %s isn't set on %s", mode, @name
Channel

View File

@ -2,6 +2,8 @@ irce = require "irce"
util = require "irce.util"
stringx = require "pl.stringx"
moon = require "moon"
ln = require "ln"
modeconv = require "irce.modules.ngircd.modes"
{
init: (state) =>
@ -11,10 +13,10 @@ moon = require "moon"
senders:
["REGISTER"]: (state, password, software, version, sname, real, nicklen) =>
assert self\PASS password, software, version
assert self\SERVER sname, real
assert self\NICKLEN nicklen
assert self\EMOTD!
assert @PASS password, software, version
assert @SERVER sname, real
assert @NICKLEN nicklen
assert @EMOTD!
string.format ":%s PING :%s", sname, sname
["PASS"]: (state, password, software, version) =>
@ -43,35 +45,124 @@ moon = require "moon"
["NICK"]: (state, nick, user, host, modes, real) =>
state.clients[string.lower nick] =
:nick, :user, :host, :modes, :real, metadata: {}
:nick, :user, :host, modes: string.sub(modes, 2), :real, metadata: {}
string.format ":%s NICK %s 1 %s %s 1 %s :%s", state.sname, nick, user, host, modes, real
["NJOIN"]: (state, channame, who) =>
string.format ":%s NJOIN %s :%s", state.sname, channame, who
["CHANINFO"]: (state, channame) =>
if state.channels[channame] == nil
state.channels[channame] =
name: channame
mode: "tn"
topic: ""
members: {}
string.format ":%s CHANINFO %s +tn", state.sname, channame
["NJOIN"]: (state, channame, who, prefix) =>
if state.channels[channame] == nil
state.channels[channame] =
name: channame,
mode: ""
topic: ""
members: {}
pfxarr = {}
if prefix
for i = 1, #prefix
pfxarr[i] = string.sub prefix, i, i
state.channels[channame].members[string.lower who] = pfxarr
string.format ":%s NJOIN %s :%s%s", state.sname, channame, prefix or "", who
["METADATA"]: (state, nick, key, val) =>
state.clients[string.lower nick].metadata[key] = val
string.format ":%s METADATA %s %s :%s", state.sname, nick, key, val
["VHOST"]: (state, nick, vhost) =>
self\METADATA nick, "cloakhost", vhost
@METADATA nick, "cloakhost", vhost
string.format ":%s MODE %s +x", state.sname, nick
["PRIVMSG"]: (state, nick, target, message) =>
string.format ":%s PRIVMSG %s :%s", nick, target, message
["NOTICE"]: (state, nick, target, message) =>
string.format ":%s NOTICE %s :%s", nick, target, message
["PART"]: (state, nick, chan, why) =>
state.channels[chan].members[nick] = nil
if #state.channels[chan].members == 0
state.channels[chan] = nil
string.format ":%s PART %s :%s", nick, chan, why
handlers:
["461"]: (state, sender, params) =>
@handle "DIE", string.format("%s: %s", sender[1], params[1])
["ERROR"]: (state, sender, params) =>
error string.format "%s: %s", sender[1], params[1]
["PING"]: (state, sender, params) =>
self\send "PONG", params[1]
@send "PONG", params[1]
sender, params[1]
["PONG"]: (state, sender, params) =>
sender, params[1]
["JOIN"]: (state, sender, params) =>
chan = params[1]
mode = {}
bell = string.find chan, "\x07"
if bell
sp = {string.sub(chan, 1, bell - 1), string.sub(chan, bell + 1)}
chan = sp[1]
mode = {modeconv.convert sp[2]}
nick = string.lower sender[1]
if not state.channels[chan]
state.channels[chan] = {name: chan, mode: "", members: {}, topic: ""}
state.channels[chan].members[nick] = mode
sender[1], state.channels[chan]
["PART"]: (state, sender, params) =>
chan = params[1]
nick = string.lower sender[1]
cinfo = state.channels[chan]
state.channels[chan].members[nick] = nil
if #state.channels[chan].members == 0
state.channels[chan] = nil
sender[1], cinfo
["QUIT"]: (state, sender, params) =>
nick = string.lower sender[1]
state.clients[nick] = nil
for k, v in pairs state.channels
v.members[nick] = nil
sender[1], params[1]
["NICK"]: (state, sender, params) =>
if #params == 1
oldnick = sender[1]
oldnicksmall = string.lower oldnick
newnick = params[1]
cli = state.clients[oldnicksmall]
cli.nick = newnick
state.clients[string.lower oldnicksmall] = nil
nick = string.lower newnick
state.clients[string.lower newnick] = cli
for k, v in pairs state.channels
for kk, vv in pairs v.members
if kk == oldnicksmall
v.members[nick] = v.members[oldnicksmall]
v.members[oldnicksmall] = nil
@handle "NICKCHG", sender, state.clients[nick]
return
nick = params[1]
user = params[3]
host = params[4]
@ -82,7 +173,7 @@ moon = require "moon"
state.clients[string.lower nick] =
:nick, :user, :host, :modes, :real, :metadata
sender, state.clients[nick]
sender, state.clients[string.lower nick]
["METADATA"]: (state, sender, params) =>
nick = params[1]
@ -96,6 +187,13 @@ moon = require "moon"
chan = params[1]
whose = stringx.split tostring(params[2]), ","
if state.channels[chan] == nil
state.channels[chan] =
name: chan
mode: ""
topic: ""
members: {}
for k, v in pairs whose
pfxlen = string.find v, "%a+"
nick = string.lower string.sub v, pfxlen
@ -103,7 +201,7 @@ moon = require "moon"
pfxarr = {}
for i = 1, #prefix
pfxarr[i] = string.sub prefix, i, i
pfxarr[i] = modeconv.convert string.sub prefix, i, i
state.channels[chan].members[nick] = pfxarr
@ -127,19 +225,24 @@ moon = require "moon"
state.channels[name] =
:name, :mode, :topic, :key, :limit, members: {}
state.channels[name]
["PRIVMSG"]: (state, sender, params) =>
if params[2] == "?state"
moon.p state
return sender, params
if sender[1] == "Cadey" and stringx.startswith params[2], "?eval"
code = string.sub(params[2], 7)
print string.format "%s %s %s - evaling", params[1], sender[1], code
fun, err = load(code, sender[1].."-"..params[1], "t", {
state: state, :string,
state: state, :string, irc: @, :_VERSION
})
if err ~= nil
error err
result = fun!
if result ~= nil
self\PRIVMSG state.sname, params[1], tostring result
@PRIVMSG state.sname, params[1], tostring result
sender, params

View File

@ -0,0 +1,22 @@
mode2prefix =
q: "~"
a: "&"
o: "@"
h: "%"
v: "+"
prefix2mode =
["~"]: "q"
["&"]: "a"
["@"]: "o"
["%"]: "h"
["+"]: "v"
convert = (mode_or_prefix) ->
mode2prefix[mode_or_prefix] or prefix2mode[mode_or_prefix]
{
:mode2prefix
:prefix2mode
:convert
}

View File

@ -1,43 +0,0 @@
irce = require "irce"
socket = require "socket"
config =
server: os.getenv("IRC_HOST") or "127.0.0.1"
sname: os.getenv("KETRACEL_SNAME") or "ketracel.akua"
spass: os.getenv("KETRACEL_SPASS") or error("need KETRACEL_SPASS")
sreal: os.getenv("KETRACEL_SREAL") or "The favorite of the Jem'Hadar"
debug: os.getenv("KETRACEL_DEBUG")
nicklen: os.getenv("KETRACEL_NICKLEN") or "31"
irc = irce.new!
running = true
-- load IRC modules
assert irc\load_module require "irce.modules.ngircd"
assert irc\load_module require "bots.ketracel"
client = socket.tcp!
client\settimeout 1
irc\set_send_func (message) =>
client\send message
if config.debug
irc\set_callback irce.RAW, (send, message) =>
print string.format "%s %s", (send and ">" or "<"), message
print "Ketracel loaded using " .. irce._VERSION .. " running on " .. _VERSION
-- connect to irc server
assert client\connect config.server, 6667
assert irc\REGISTER config.spass, "ketracel", "0.0.1", config.sname, config.sreal, config.nicklen
if config.oper
irc\OPER config.nick, config.oper
print "IRC operator status requested for " .. config.nick
while running
irc\process client\receive!
client\close!

View File

@ -0,0 +1,3 @@
include_rules
.gitignore

View File

@ -0,0 +1,38 @@
ln = require "ln"
stringx = require "pl.stringx"
class CommandRouter
new: (bot, cmdchar = "?") =>
@bot = bot
@cmdchar = cmdchar
@commands = {}
register: (verb, action) =>
self.commands[verb] = action
run: (sender, target, verb, args) =>
verb = string.upper verb
cmd = self.commands[verb]
if cmd == nil
@bot.irc\NOTICE @bot.name, sender, "unknown command verb " .. verb
return
cmd sender, target, verb, args
privmsg: (sender, params) =>
cmdchar = params[2]\sub 1, 1
destsigil = params[1]\sub 1, 1
if params[1]\lower! == @bot.name\lower!
sp = stringx.split params[2]
cmd = sp[1]
table.remove sp, 1
@run sender[1], params[1], cmd, sp
if cmdchar == @cmdchar and destsigil == "#"
sp = stringx.split params[2]
cmd = string.sub sp[1], (#@cmdchar + 1)
table.remove sp, 1
@run sender[1], params[1], cmd, sp
CommandRouter

View File

@ -0,0 +1,48 @@
irce = require "irce"
ln = require "ln"
moon = require "moon"
stringx = require "pl.stringx"
CommandRouter = require "ketracel.bots.commands"
class Ketracel
new: (irc) =>
@name = "Ketracel"
@irc = irc
router = CommandRouter self
router\register "DIE", (sender, target, verb, args) -> @die sender, target, verb, args
router\register "VHOST", (...) -> @set_vhost ...
router\register "STATE", ->
router\register "EVAL", ->
@router = router
burst: =>
@irc\NICK @name, "white", "the.dominion", "+io", "Ketracel White"
njoin: (chan) =>
@irc\NJOIN chan, "Ketracel"
die: (sender, target, verb, args) =>
@irc\handle "DIE", string.format("%s asked me to die in %s", sender, target)
set_vhost: (sender, target, verb, args) =>
if #args > 0
ln.log :sender, vhost: args[1], action: "setting vhost"
@irc\VHOST sender, args[1]
@irc\NOTICE "Ketracel", sender, "your vhost is now " ..args[1]
else
@irc\NOTICE "Ketracel", sender, "usage: VHOST <your.vhost>"
{
init: (state) =>
state.bot = Ketracel self
hooks:
["376"]: (state) =>
state.bot\burst!
["NJOIN"]: (state, chan) =>
state.bot\njoin chan
["PRIVMSG"]: (state, sender, params) =>
state.bot.router\privmsg sender, params
}

15
src/ketracel/main.moon Normal file
View File

@ -0,0 +1,15 @@
irce = require "irce"
ln = require "ln"
socket = require "socket"
server = require "ketracel.server"
config =
server: os.getenv("IRC_HOST") or "127.0.0.1"
sname: os.getenv("KETRACEL_SNAME") or "ketracel.akua"
spass: os.getenv("KETRACEL_SPASS") or error("need KETRACEL_SPASS")
sreal: os.getenv("KETRACEL_SREAL") or "The favorite of the Jem'Hadar"
debug: os.getenv("KETRACEL_DEBUG")
nicklen: os.getenv("KETRACEL_NICKLEN") or "31"
with server.Server config
\run!

59
src/ketracel/server.moon Normal file
View File

@ -0,0 +1,59 @@
irce = require "irce"
ln = require "ln"
socket = require "socket"
class Server
new: (config) =>
@config = config
sock = socket.tcp!
irc = irce.new!
irc\load_module require "irce.modules.ngircd"
irc\load_module require "ketracel.bots.ketracel"
irc\set_send_func (message) =>
sock\send message
if config.debug
ln.log {"msg": "debug enabled"}, config
irc\set_callback irce.RAW, (send, message) =>
sigil = send and ">" or "<"
print string.format "%s %s", (send and ">" or "<"), message
ln.log msg: "Ketracel loaded", irce: irce._VERSION, lua: _VERSION
assert sock\connect config.server, 6667
assert irc\REGISTER config.spass, "ketracel", "0.0.1", config.sname, config.sreal, config.nicklen
@irc = irc
@socket = sock
wait_for: (event, checker) =>
running = true
@irc\set_callback event, (...) =>
if checker and not checker(...)
return
print "! got event " .. event if os.getenv "DEBUG"
running = false
while running
@irc\process @socket\receive!
@irc\clear_callback event
run: =>
running = true
@irc\set_callback "DIE", (why) =>
ln.err why, {msg: "told to die"}
running = false
while running
@irc\process @socket\receive!
@socket\close!
{
:Server
}

47
src/ketracel/test.moon Normal file
View File

@ -0,0 +1,47 @@
irce = require "irce"
socket = require "socket"
get_client_modules = (irc) ->
irc\load_module require "irce.modules.base"
irc\load_module require "irce.modules.message"
irc\load_module require "irce.modules.channel"
class Client
new: (nick) =>
@nick = nick
sock = socket.tcp!
irc = irce.new!
get_client_modules irc
irc\set_send_func (message) =>
print string.format "[client %s] > %s", nick, message if os.getenv "DEBUG"
sock\send message
sock\connect "127.0.0.1", 6667
irc\NICK nick
irc\USER nick, nick
@socket = sock
@irc = irc
wait_for: (event, checker) =>
running = true
nick = @nick
@irc\set_callback event, (...) =>
if checker and not checker ...
return
print string.format "[client %s] ! got event %s", nick, event if os.getenv "DEBUG"
running = false
while running
msg = assert @socket\receive!
print string.format "[client %s] < %s", nick, msg if os.getenv "DEBUG"
@irc\process msg
@irc\clear_callback event
quit: =>
@irc\QUIT "bye"
@socket\close!
{
:Client
}