marvin

A simple and modular IRC bot

git clone https://git.8pit.net/marvin.git

  1// This program is free software: you can redistribute it and/or modify
  2// it under the terms of the GNU Affero General Public License as
  3// published by the Free Software Foundation, either version 3 of the
  4// License, or (at your option) any later version.
  5//
  6// This program is distributed in the hope that it will be useful, but
  7// WITHOUT ANY WARRANTY; without even the implied warranty of
  8// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  9// Affero General Public License for more details.
 10//
 11// You should have received a copy of the GNU Affero General Public
 12// License along with this program. If not, see <http://www.gnu.org/licenses/>.
 13
 14package irc
 15
 16import (
 17	"fmt"
 18	"net"
 19	"strings"
 20	"unicode"
 21)
 22
 23type Hook func(*Client, Message) error
 24
 25type Client struct {
 26	conn     net.Conn
 27	hooks    map[string][]Hook
 28	Nickname string
 29	Realname string
 30	Channels []string
 31}
 32
 33func NewClient(conn net.Conn) *Client {
 34	c := &Client{
 35		conn:  conn,
 36		hooks: make(map[string][]Hook),
 37	}
 38
 39	c.CmdHook("join", joinCmd)
 40	c.CmdHook("part", partCmd)
 41	c.CmdHook("kick", kickCmd)
 42
 43	c.CmdHook("ping", pingCmd)
 44	return c
 45}
 46
 47func (c *Client) Setup(nick, name, host string) {
 48	c.Nickname = nick
 49	c.Realname = name
 50
 51	c.Write("USER %s %s * :%s", c.Nickname, host, c.Realname)
 52	c.Write("NICK %s", c.Nickname)
 53}
 54
 55func (c *Client) Connected(channel string) bool {
 56	for _, c := range c.Channels {
 57		if c == channel {
 58			return true
 59		}
 60	}
 61
 62	return false
 63}
 64
 65func (c *Client) Write(format string, argv ...interface{}) error {
 66	_, err := fmt.Fprintf(c.conn, "%s\r\n", sanitize(fmt.Sprintf(format, argv...)))
 67	if err != nil {
 68		return err
 69	}
 70
 71	return nil
 72}
 73
 74func (c *Client) Handle(data string, ch chan error) {
 75	msg := parseMessage(data)
 76	hooks, ok := c.hooks[msg.Command]
 77	if ok {
 78		for _, hook := range hooks {
 79			go func(h Hook) {
 80				if err := h(c, msg); err != nil {
 81					ch <- err
 82				}
 83			}(hook)
 84		}
 85	}
 86}
 87
 88func (c *Client) CmdHook(cmd string, hook Hook) {
 89	c.hooks[cmd] = append(c.hooks[cmd], hook)
 90}
 91
 92func joinCmd(client *Client, msg Message) error {
 93	if msg.Sender.Name == client.Nickname {
 94		client.Channels = append(client.Channels, msg.Data)
 95	}
 96
 97	return nil
 98}
 99
100func partCmd(client *Client, msg Message) error {
101	if msg.Sender.Name == client.Nickname {
102		client.Channels = remove(msg.Data, client.Channels)
103	}
104
105	return nil
106}
107
108func kickCmd(client *Client, msg Message) error {
109	if msg.Data == client.Nickname {
110		target := strings.Fields(msg.Receiver)[0]
111		client.Channels = remove(target, client.Channels)
112	}
113
114	return nil
115}
116
117func pingCmd(client *Client, msg Message) error {
118	return client.Write("PONG %s", msg.Data)
119}
120
121// sanitize removes all non-printable characters from
122// the given string by returning a new string without them.
123func sanitize(text string) string {
124	mfunc := func(r rune) rune {
125		switch {
126		case !unicode.IsPrint(r):
127			return ' '
128		case unicode.IsSpace(r):
129			return ' '
130		default:
131			return r
132		}
133	}
134
135	escaped := strings.Map(mfunc, text)
136	return strings.Join(strings.Fields(escaped), " ")
137}
138
139// remove deletes a given element from a given slice. A new slice
140// which does not contain the given element is returned.
141func remove(element string, slice []string) []string {
142	var newSlice []string
143	for _, e := range slice {
144		if e != element {
145			newSlice = append(newSlice, e)
146		}
147	}
148
149	return newSlice
150}