kunt

golang IRC bot
git clone git://git.2f30.org/kunt.git
Log | Files | Refs | LICENSE

commit ffeb3cf38c94e2f3c362b3b2343ac60cfede89fa
Author: sin <sin@2f30.org>
Date:   Sat Apr 13 20:41:57 +0100

Initial commit

Diffstat:
build | 5+++++
package | 9+++++++++
src/fsdb/fsdb.go | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/kunt/TODO | 5+++++
src/kunt/db/vagina | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
src/kunt/fortune | 3+++
src/kunt/init | 6++++++
src/kunt/kunt.go | 396+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/kunt/resolve | 18++++++++++++++++++
src/kunt/run | 3+++
10 files changed, 650 insertions(+), 0 deletions(-)
diff --git a/build b/build @@ -0,0 +1,5 @@ +#!/bin/sh + +export GOPATH=$PWD +go install fsdb +go build -o src/kunt/kunt -v kunt diff --git a/package b/package @@ -0,0 +1,9 @@ +#!/bin/sh + +mkdir -p kunt-latest +cp build package kunt-latest +cp -r src kunt-latest +rm -rf kunt-latest/pkg +rm -f kunt-latest/src/kunt/kunt +tar cfz kunt-latest.tgz kunt-latest +rm -rf kunt-latest diff --git a/src/fsdb/fsdb.go b/src/fsdb/fsdb.go @@ -0,0 +1,154 @@ +package fsdb + +import ( + "errors" + "fmt" + "io/ioutil" + "log" + "math/rand" + "os" + "path" + "path/filepath" + "strconv" + "strings" + "sync" +) + +type Fsdb struct { + name string + path string + prefix string + dbMap map[int][]byte + lastKey int + lock sync.Mutex +} + +func NewFsdb(name string, path string, prefix string) *Fsdb { + d := new(Fsdb) + d.dbMap = make(map[int][]byte) + d.name = name + d.path = path + d.prefix = prefix + d.lastKey = -1 + return d +} + +func (d *Fsdb) FsRead(quote int) ([]byte, error) { + path := fmt.Sprintf("%s/%s%d.txt", d.path, d.prefix, quote) + b, err := ioutil.ReadFile(path) + if err != nil { + return b, err + } + return b, err +} + +func (d *Fsdb) FsWrite(quote int, buf []byte) error { + path := fmt.Sprintf("%s/%s%d.txt", d.path, d.prefix, quote) + _, err := os.Stat(path) + if err == nil { + return fmt.Errorf("Entry %s already exists in fsdb\n", + path) + } + s := string(buf) + last := strings.Index(s, "\n") + err = ioutil.WriteFile(path, buf[0:last+1], 0644) + if err != nil { + return err + } + return nil +} + +func (d *Fsdb) Load() { + err := filepath.Walk(d.path, func(p string, f os.FileInfo, err error) error { + fi, err := os.Stat(p) + if err != nil { + log.Fatal(err) + } + if fi.IsDir() { + return nil + } + base, ext := path.Base(p), path.Ext(p) + entry := base[len(d.prefix) : len(base)-len(ext)] + i, err := strconv.Atoi(entry) + if err != nil { + log.Fatal(err) + } + b, err := d.FsRead(i) + if err != nil { + log.Fatal(err) + } + _, err = d.Put(i, b) + if err != nil { + log.Fatal(err) + } + return nil + }) + if err != nil { + log.Fatal(err) + } +} + +func (d *Fsdb) Put(key int, buf []byte) (int, error) { + d.lock.Lock() + defer d.lock.Unlock() + for _, v := range d.dbMap { + if string(v) == string(buf) { + return -1, errors.New("Duplicate entry: " + string(buf)) + } + } + if key == -1 { + d.lastKey++ + key = d.lastKey + } else { + if key > d.lastKey { + d.lastKey = key + } + } + if d.dbMap[key] != nil { + log.Fatal("Duplicate entry in fsdb") + } + d.dbMap[key] = buf + return key, nil +} + +func (d *Fsdb) Get(key int) ([]byte, error) { + d.lock.Lock() + defer d.lock.Unlock() + if len(d.dbMap) == 0 { + return nil, errors.New("Empty DB, can't fetch entry") + } + buf := d.dbMap[key] + if buf == nil { + log.Fatal("Fetching nil entry from fsdb") + } + return buf, nil +} + +func (d *Fsdb) Rand() ([]byte, int) { + d.lock.Lock() + defer d.lock.Unlock() + + idx := rand.Intn(len(d.dbMap)) + i := 0 + for k, _ := range d.dbMap { + if i == idx { + buf := d.dbMap[k] + return buf, k + } + i++ + } + return nil, -1 +} + +func (d *Fsdb) Empty() bool { + d.lock.Lock() + defer d.lock.Unlock() + if len(d.dbMap) == 0 { + return true + } + return false +} + +func (d *Fsdb) Len() int { + return len(d.dbMap) +} diff --git a/src/kunt/TODO b/src/kunt/TODO @@ -0,0 +1,5 @@ +Add multichannel/multiserver support +Optionally cache youtube titles locally +Write a sensible parser for the irc protocol +Add a !mode command for setting up various options/modes +Add a !flush command to write out the entire db to the fs diff --git a/src/kunt/db/vagina b/src/kunt/db/vagina @@ -0,0 +1,51 @@ +#!/bin/bash +# thx tsakos! :) + +TMP_FILE=tmp.$$ # a temporary file for storing the URLs +URL_DB=urls # directory that contains the URLs +CNT=0 # link counter + +tar cfz $URL_DB.tgz $URL_DB + +check_avail() { +STATUS=`curl -Is $1 | grep HTTP | cut -d ' ' -f 2` + +case $STATUS in +200) + return 0 + ;; +404) + return 1 + ;; +esac +} + +for i in `ls $URL_DB | grep -o '[0-9]*' | sort -g` +do + cat $URL_DB/url$i.txt | tr -d '\r' >> $TMP_FILE +done + +rm -rf urls/* + +while read URL +do + if [ "`echo $URL | grep -Eo '([a-z0-9]*\.)?[a-z]*\.[a-z]*'`" ] + then + check_avail $URL + RETURN_CODE=$? + + if [ $RETURN_CODE -ne 0 ] + then + echo "url$CNT.txt: BROKEN" + else + echo "url$CNT.txt: OK" + echo "$URL" >> $URL_DB/url$CNT.txt + CNT=$((CNT+1)) + fi + else + echo "Invalid URL format: url$CNT.txt" + fi + +done < $TMP_FILE + +rm $TMP_FILE diff --git a/src/kunt/fortune b/src/kunt/fortune @@ -0,0 +1,3 @@ +#!/bin/sh + +fortune -s -o | expand diff --git a/src/kunt/init b/src/kunt/init @@ -0,0 +1,6 @@ +#!/bin/sh + +mkdir -p db/quotes +mkdir -p db/urls +mkdir -p certs +openssl req -new -nodes -x509 -out certs/client.pem -keyout certs/client.key -days 3650 -subj "/C=DE/ST=NRW/L=Earth/O=Random Company/OU=IT/CN=www.random.com/emailAddress=kunt@2f30.org" diff --git a/src/kunt/kunt.go b/src/kunt/kunt.go @@ -0,0 +1,396 @@ +package main + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "fsdb" + "io/ioutil" + "log" + "math/rand" + "net" + "os" + "os/exec" + "strings" + "time" +) + +/* Kunt context */ +type kuntCtx struct { + channel string + nick string + user string + srv string + port string + ssl bool + stime time.Time +} + +func newKunt(channel string, nick string, + user string, srv string, port string, ssl bool) *kuntCtx { + return &kuntCtx{channel, nick, user, srv, port, ssl, time.Now()} +} + +/* Helper routines */ +func botWrite(conn net.Conn, buf []byte) { + _, err := conn.Write(buf[0:]) + if err != nil { + log.Fatal(err) + } +} + +func resolveYoutubeTitle(link string) (title string, ret bool) { + var out bytes.Buffer + cmd := exec.Command("./resolve", link) + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + if _, ok := err.(*exec.ExitError); ok { + title = "" + ret = false + return + } + } + title = out.String() + ret = true + return +} + +/* Kunt command handling routines */ +func cmdKunt(conn net.Conn, s string) { + actions := []string{ + "die", + "die bitch", + "are you talking to me?", + "ok", + "maybe", + "yes", + "no", + "no fucking way", + } + idx := rand.Intn(len(actions)) + r := fmt.Sprintf("PRIVMSG %s :%s\r\n", kunt.channel, actions[idx]) + botWrite(conn, []byte(r)) +} + +func cmdWisdom(conn net.Conn, s string) { + out, err := exec.Command("./fortune").Output() + if err != nil { + log.Fatal(err) + } + lines := strings.Split(string(out), "\n") + for _, i := range lines { + if i == "" { + continue + } + r := fmt.Sprintf("PRIVMSG %s :%s\r\n", kunt.channel, i) + botWrite(conn, []byte(r)) + time.Sleep(512 * time.Millisecond) + } +} + +func cmdHelp(conn net.Conn, s string) { + help := []string{ + "!addquote <quote> -> Add a quote to the db", + "!randquote -> Print a random quote from the db", + "!countquotes -> Print the number of entries in the quote db", + "!addurl <url> -> Add a url to the db", + "!randurl -> Print a random url from the db", + "!counturls -> Print the number of entries in the url db", + "!uptime -> Show uptime", + "!wisdom -> Invoke fortune(6)", + "!src -> Fetch kunt's source code", + "!TODO -> Print the TODO list", + "!version -> Show version", + } + for _, i := range help { + r := fmt.Sprintf("PRIVMSG %s :%s\r\n", kunt.channel, i) + botWrite(conn, []byte(r)) + time.Sleep(512 * time.Millisecond) + } +} + +func cmdVersion(conn net.Conn, s string) { + ver := "v0.2.7" + r := fmt.Sprintf("PRIVMSG %s :%s\r\n", kunt.channel, ver) + botWrite(conn, []byte(r)) +} + +func cmdRandQuote(conn net.Conn, s string) { + if quoteDb.Empty() { + r := fmt.Sprintf("PRIVMSG %s :Empty quote database\r\n", + kunt.channel) + botWrite(conn, []byte(r)) + return + } + quote, idx := quoteDb.Rand() + /* Print idx in red */ + r := fmt.Sprintf("PRIVMSG %s :%s%02d[%d]%s %s\r\n", kunt.channel, + "\003", '5', idx, "\003", string(quote)) + botWrite(conn, []byte(r)) +} + +func cmdAddQuote(conn net.Conn, s string) { + idx := len("!addquote") + if s[idx:idx+1] != " " { + r := fmt.Sprintf("PRIVMSG %s :Missing parameter for !addquote\r\n", + kunt.channel) + botWrite(conn, []byte(r)) + return + } + s = s[idx+1:] + s = strings.TrimSpace(s) + s += "\n" + buf := []byte(s) + quote, err := quoteDb.Put(-1, buf) + if err != nil { + r := fmt.Sprintf("PRIVMSG %s :%s\r\n", + kunt.channel, err.Error()) + botWrite(conn, []byte(r)) + return + } + err = quoteDb.FsWrite(quote, buf) + if err != nil { + log.Fatal(err) + } +} + +func cmdCountQuotes(conn net.Conn, s string) { + r := fmt.Sprintf("PRIVMSG %s :The quote DB has %d quotes\r\n", + kunt.channel, quoteDb.Len()) + botWrite(conn, []byte(r)) +} + +func cmdRandUrl(conn net.Conn, s string) { + if urlDb.Empty() { + r := fmt.Sprintf("PRIVMSG %s :Empty url database\r\n", + kunt.channel) + botWrite(conn, []byte(r)) + return + } + url, idx := urlDb.Rand() + /* Print idx in red */ + r := fmt.Sprintf("PRIVMSG %s :%s%02d[%d]%s %s\r\n", kunt.channel, + "\003", '5', idx, "\003", string(url)) + botWrite(conn, []byte(r)) + title, ok := resolveYoutubeTitle(string(url)) + if ok { + r = fmt.Sprintf("PRIVMSG %s :[YouTube] %s\r\n", + kunt.channel, title) + botWrite(conn, []byte(r)) + } +} + +func cmdAddUrl(conn net.Conn, s string) { + idx := len("!addurl") + if s[idx:idx+1] != " " { + r := fmt.Sprintf("PRIVMSG %s :Missing parameter for !addurl\r\n", + kunt.channel) + botWrite(conn, []byte(r)) + return + } + s = s[idx+1:] + s = strings.TrimSpace(s) + s += "\n" + buf := []byte(s) + url, err := urlDb.Put(-1, buf) + if err != nil { + r := fmt.Sprintf("PRIVMSG %s :%s\r\n", + kunt.channel, err.Error()) + botWrite(conn, []byte(r)) + return + } + err = urlDb.FsWrite(url, buf) + if err != nil { + log.Fatal(err) + } +} + +func cmdCountUrls(conn net.Conn, s string) { + r := fmt.Sprintf("PRIVMSG %s :The url DB has %d urls\r\n", + kunt.channel, urlDb.Len()) + botWrite(conn, []byte(r)) +} + +func cmdUptime(conn net.Conn, s string) { + etime := time.Now() + r := fmt.Sprintf("PRIVMSG %s :%v\r\n", kunt.channel, + etime.Sub(kunt.stime)) + botWrite(conn, []byte(r)) +} + +func cmdSrc(conn net.Conn, s string) { + src := "http://amnezia.2f30.org/tmp/kunt-latest.tgz" + r := fmt.Sprintf("PRIVMSG %s :%s\r\n", kunt.channel, src) + botWrite(conn, []byte(r)) +} + +func cmdTodo(conn net.Conn, s string) { + todo, err := ioutil.ReadFile("TODO") + if err != nil { + r := fmt.Sprintf("PRIVMSG %s :No TODO list available\r\n", kunt.channel) + botWrite(conn, []byte(r)) + return + } + sf := strings.FieldsFunc(string(todo), func(r rune) bool { + switch r { + case '\n': + return true + } + return false + }) + for _, i := range sf { + r := fmt.Sprintf("PRIVMSG %s :%s\r\n", kunt.channel, i) + botWrite(conn, []byte(r)) + time.Sleep(512 * time.Millisecond) + } +} + +var sslon = flag.Bool("s", false, "SSL support") + +var quoteDb *fsdb.Fsdb +var urlDb *fsdb.Fsdb +var kunt *kuntCtx + +func main() { + flag.Parse() + + if flag.NArg() < 1 { + fmt.Fprintf(os.Stderr, "usage: %s [-s] host:port\n", + os.Args[0]) + os.Exit(1) + } + service := flag.Arg(0) + + log.SetPrefix("kunt: ") + + hostport := strings.Split(flag.Arg(0), ":") + kunt = newKunt("#2f30", "kunt", "z0mg", hostport[0], hostport[1], *sslon) + + if *sslon { + fmt.Println("SSL on") + } + + tcpAddr, err := net.ResolveTCPAddr("tcp4", service) + if err != nil { + log.Fatal(err) + } + + if *sslon { + cert, err := tls.LoadX509KeyPair("certs/client.pem", + "certs/client.key") + if err != nil { + log.Fatal(err) + } + conf := tls.Config{Certificates: []tls.Certificate{cert}, + InsecureSkipVerify: true} + + conn, err := tls.Dial("tcp", service, &conf) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + + fmt.Println("Client: connected to: ", conn.RemoteAddr()) + + state := conn.ConnectionState() + + for _, v := range state.PeerCertificates { + fmt.Println(x509.MarshalPKIXPublicKey(v.PublicKey)) + fmt.Println(v.Subject) + } + + fmt.Println("Client: handshake: ", state.HandshakeComplete) + fmt.Println("Client: mutual: ", state.NegotiatedProtocolIsMutual) + loop(conn) + } else { + conn, err := net.DialTCP("tcp", nil, tcpAddr) + if err != nil { + log.Fatal(err) + } + defer conn.Close() + loop(conn) + } +} + +func loop(conn net.Conn) { + quoteDb = fsdb.NewFsdb("Quote DB", "db/quotes", "quote") + urlDb = fsdb.NewFsdb("Url DB", "db/urls", "url") + + quoteDb.Load() + urlDb.Load() + + go InitiateConnection(conn) + + rand.Seed(time.Now().UnixNano()) + + dispatch := map[string]func(net.Conn, string){ + "kunt": cmdKunt, + "!wisdom": cmdWisdom, + "!help": cmdHelp, + "!version": cmdVersion, + "!randquote": cmdRandQuote, + "!addquote": cmdAddQuote, + "!countquotes": cmdCountQuotes, + "!randurl": cmdRandUrl, + "!addurl": cmdAddUrl, + "!counturls": cmdCountUrls, + "!uptime": cmdUptime, + "!src": cmdSrc, + "!TODO": cmdTodo, + } + + for { + var buf [512]byte + n, err := conn.Read(buf[0:]) + if err != nil { + log.Fatal(err) + } + fmt.Println(string(buf[0 : n-1])) + s := string(buf[0:n]) + for i := range buf { + buf[i] = 0 + } + + if strings.HasPrefix(s, "PING :") { + r := strings.Replace(s, "PING :", "PONG :", 6) + r = r + "\r\n" + _, err := conn.Write([]byte(r)) + if err != nil { + log.Fatal(err) + } + continue + } + + q := fmt.Sprintf("PRIVMSG %s :", kunt.channel) + msg := strings.Index(s, q) + if msg == -1 { + continue + } + msg += len(q) + s = s[msg:] + s = strings.TrimSpace(s) + s += "\n" + + for i, v := range dispatch { + if strings.HasPrefix(s, i) { + go v(conn, s) + break + } + } + } + + os.Exit(0) +} + +func InitiateConnection(conn net.Conn) { + fmt.Println("initcon") + query := fmt.Sprintf("NICK %s\r\n", kunt.nick) + botWrite(conn, []byte(query)) + query = fmt.Sprintf("USER %s * 8 :%s\r\n", kunt.user, kunt.nick) + botWrite(conn, []byte(query)) + query = fmt.Sprintf("JOIN %s\r\n", kunt.channel) + botWrite(conn, []byte(query)) +} diff --git a/src/kunt/resolve b/src/kunt/resolve @@ -0,0 +1,18 @@ +#!/bin/sh + +export LINK=$@ + +if [ -z ${LINK} ]; then + echo "usage: $0 <link>" + exit 1 +fi + +echo ${LINK} | grep -qie "\(\(\(www.\)\?youtube.com/.*\(?v=\|&v=\).\+\)\|\(youtu.be/.\+\)\)" +if [ $? -eq 0 ]; then + TITLE=$(echo ${LINK} | awk -F'=' '{print $2}') + QUERY="-qO - https://gdata.youtube.com/feeds/api/videos/${TITLE}" + wget ${QUERY} | grep -oe "<title type='text'>.*</title>" | awk -F'>' '{print $2}' | awk -F'<' '{print $1}' | tr -d '\n' + exit 0 +else + exit 1 +fi diff --git a/src/kunt/run b/src/kunt/run @@ -0,0 +1,3 @@ +#!/bin/sh + +while :; do ./kunt -s otrere.irc.gr:9667; sleep 20; done