Browse Source

Add IRC bot plusone

TODO:
    Write README file
master
Remi Reuvekamp 3 years ago
commit
d85cbc9607
4 changed files with 370 additions and 0 deletions
  1. +1
    -0
      .gitignore
  2. +68
    -0
      plusone/config.go
  3. +209
    -0
      plusone/irc.go
  4. +92
    -0
      plusone/main.go

+ 1
- 0
.gitignore View File

@@ -0,0 +1 @@
config.json

+ 68
- 0
plusone/config.go View File

@@ -0,0 +1,68 @@
package main

import (
"encoding/json"
"io/ioutil"
)

type config struct {
Server struct {
Host string `json:"host"`
Port int `json:"port"`
} `json:"server"`

User struct {
NickName string `json:"nickname"`
UserName string `json:"username"`
RealName string `json:"realname"`
Password string `json:"password"`
} `json:"user"`

Channels []string `json:"channels"`
RespondIn int `json:"respondOnceIn"`

Response struct {
Responses []string `json:"responses"`
BaseSleep int `json:"baseSleepDurationInMilliseconds"`
ExtraSleep int `json:"extraSleepDurationInMilliSecondsPerCharacter"`
} `json:"response"`
}

// loadConfig parses the configuration file with the given filename and creates one
// with empty values it if there is none with the given name.
// The returned boolean is true if the config file didn't exist yet but was created.
func loadConfig(filename string) (config, bool, error) {
var c config

data, err := ioutil.ReadFile(filename)

if err != nil {
// File with given filename doesn't exist. Create it.

// Set default
c.Channels = []string{}
c.Response.Responses = []string{}
c.Response.BaseSleep = 5000
c.Response.ExtraSleep = 65

data, err := json.MarshalIndent(&c, "", "\t")
if err != nil {
return c, false, err
}

err = ioutil.WriteFile(filename, data, 0644)
if err != nil {
return c, false, err
}

return c, true, nil
}

err = json.Unmarshal(data, &c)

if err != nil {
return c, false, err
}

return c, false, nil
}

+ 209
- 0
plusone/irc.go View File

@@ -0,0 +1,209 @@
package main

import (
"bufio"
"fmt"
"log"
"net"
"strconv"
"strings"
"time"
)

type connection struct {
conn net.Conn

// write: messages that will be sent to the IRC server.
write chan string

// msg is were incoming PRIVMSG's end up.
msg chan message

// quit is used to signal there has been an unrecoverable error with the IRC
// connection.
quit chan struct{}

// log is used to log incoming messages from the IRC server.
log *log.Logger

// logErr is used to log errors that occurred.
logErr *log.Logger

me user
}

type message struct {
msg string
channel string
user user
isPrivate bool
}

type user struct {
nick string
user string
host string
}

// connect opens a TCP connection to the given IRC server and sets up the goroutines
// that handle writing and reading from it. The given loggers are used to log
// incoming IRC messages and possible errors.
func connect(server string, port int, log, logErr *log.Logger) (c *connection, err error) {
c = &connection{
write: make(chan string, 5), // Buffer, just in case.
msg: make(chan message, 5),

log: log,
logErr: logErr,
}

c.conn, err = net.Dial("tcp", server+":"+strconv.Itoa(port))
if err != nil {
return nil, err
}

go c.reader()
go c.writer()

return c, nil
}

func (c *connection) reader() {
r := bufio.NewReader(c.conn)
for {
line, err := r.ReadString(byte('\n'))
if err != nil {
c.logErr.Println(err)
c.quit <- struct{}{}
}

if strings.HasPrefix(line, "PING") {
code := strings.TrimPrefix(line, "PING")
c.write <- "PONG" + code
continue
}

line = strings.TrimSuffix(line, "\r\n")

if strings.Contains(line, " PRIVMSG ") {
msg, err := c.parsePrivMsg(line)
if err != nil {
c.logErr.Println(err)
continue
}

c.log.Println(msg)

c.msg <- msg
continue
}

c.log.Println(line)
}
}

func (c *connection) writer() {
w := bufio.NewWriter(c.conn)
for {
line := <-c.write

if !strings.HasPrefix(line, "\r\n") {
line += "\r\n"
}

_, err := w.WriteString(line)
if err != nil {
c.logErr.Println(err)
continue
}
w.Flush()
}
}

// authenticate requests the IRC server to use the given nickname, username,
// realname and, optionally, password.
func (c *connection) authenticate(nick, user, realname, pass string) {
if len(pass) > 0 {
c.write <- "PASS " + pass
}
c.write <- "NICK " + nick
c.write <- "USER " + user + " * * :" + realname

c.me.nick = nick
c.me.user = user
}

// join requests the IRC server to join the given channel.
func (c *connection) join(channel string) {
c.write <- "JOIN " + channel
}

// message requests the IRC server to sent the given message to the given recipients.
func (c *connection) message(recipients []string, message string) {
msgs := strings.Split(message, "\n")
for _, msg := range msgs {
split := strings.Split(msg, "|:|")
if len(split) > 1 {
if sleep, err := strconv.Atoi(split[0]); err == nil {
time.Sleep(time.Duration(sleep) * time.Millisecond)
msg = split[1]
}
}
c.write <- "PRIVMSG " + strings.Join(recipients, ",") + " :" + msg
}
}

func (c *connection) parsePrivMsg(line string) (msg message, err error) {
// For reference:
//:remi!~remi@novaember.com PRIVMSG #remi :Hey

errMsg := "Wut? parsePrivMsg %q split length is less than %d? Part: %q Line: " + line

line = strings.TrimPrefix(line, ":")

split := strings.Split(line, " PRIVMSG ")
if len(split) < 2 {
return msg, fmt.Errorf(errMsg, ":", 2, "")
}

userInfo := split[0]
msgInfo := split[1]

userInfos := strings.Split(userInfo, "!~")
if len(userInfos) < 2 {
return msg, fmt.Errorf(errMsg, "!~", 2, userInfo)
}

nick := userInfos[0]
hostInfo := userInfos[1]

msg.user.nick = nick

hostInfos := strings.Split(hostInfo, "@")
if len(hostInfos) < 2 {
return msg, fmt.Errorf(errMsg, "@", 2, hostInfo)
}

user := hostInfos[0]
host := hostInfos[1]

msg.user.user = user
msg.user.host = host

msgInfos := strings.Split(msgInfo, " :")
if len(msgInfos) < 2 {
return msg, fmt.Errorf(errMsg, " :", 2, msgInfo)
}

channel := msgInfos[0]
message := msgInfos[1]

msg.channel = channel
msg.msg = message

if msg.channel == c.me.nick {
msg.isPrivate = true
}

return
}

+ 92
- 0
plusone/main.go View File

@@ -0,0 +1,92 @@
package main

import (
"fmt"
"log"
"math/rand"
"os"
"time"
)

func main() {
// Loading configuration from file.
cfg, created, err := loadConfig("config.json")
if err != nil {
log.Fatal(err)
}

if created {
fmt.Println("Configuration file created. Please fill it in.")
return
}

// Connect to the IRC server.
c, err := connect(cfg.Server.Host, cfg.Server.Port,
log.New(os.Stdout, "irc: ", log.Ldate|log.Ltime),
log.New(os.Stderr, "ircerr: ", log.Ldate|log.Ltime))

if err != nil {
log.Fatal(err)
}

time.Sleep(time.Second)

// Authenticate to the IRC server.
c.authenticate(cfg.User.NickName, cfg.User.UserName, cfg.User.RealName, cfg.User.Password)

// Join channels.
for _, ch := range cfg.Channels {
time.Sleep(500 * time.Millisecond)
c.join(ch)
}

rand.Seed(time.Now().UnixNano())

// Main loop.
// Wait for incomming PRIVMSG's or stop the application on request.
for {
var msg message
select {
case msg = <-c.msg:
case <-c.quit:
return
}

if msg.isPrivate {
log.Println("Is private")
continue
}

var found bool
for _, ch := range cfg.Channels {
if msg.channel == ch {
found = true
break
}
}

if !found {
continue
}

if rand.Intn(cfg.RespondIn) != 1 {
continue
}

go func() {
dur := cfg.Response.BaseSleep + (cfg.Response.ExtraSleep * len(msg.msg))

log.Println("It is time:", dur/1000)

time.Sleep(time.Duration(dur) * time.Millisecond)

// Determine message
var i int
if len(cfg.Response.Responses) > 1 {
i = rand.Intn(len(cfg.Response.Responses))
}

c.message([]string{msg.channel}, cfg.Response.Responses[i])
}()
}
}

Loading…
Cancel
Save