1// package weechat implements the WeeChat relay protocol.
2package weechat
3
4import (
5	"bufio"
6	"bytes"
7	"compress/zlib"
8	"encoding/binary"
9	"errors"
10	"fmt"
11	"io"
12	"io/ioutil"
13	"net"
14	"sync"
15)
16
17// Reference: http://www.weechat.org/files/doc/stable/weechat_relay_protocol.en.html
18
19type command int
20
21const (
22	cmdInit command = iota
23	cmdHdata
24	cmdInfo
25	cmdInfolist
26	cmdNicklist
27	cmdInput
28	cmdSync
29	cmdDesync
30	cmdQuit
31	cmdCount
32)
33
34var cmdStrings = [cmdCount]string{
35	cmdInit:     "init",
36	cmdHdata:    "hdata",
37	cmdInfo:     "info",
38	cmdInfolist: "infolist",
39	cmdNicklist: "nicklist",
40	cmdInput:    "input",
41	cmdSync:     "sync",
42	cmdDesync:   "desync",
43	cmdQuit:     "quit",
44}
45
46type Conn struct {
47	c    net.Conn
48	r    *bufio.Reader
49	lock sync.Mutex
50}
51
52func Dial(addr string) (*Conn, error) {
53	conn, err := net.Dial("tcp", addr)
54	if err != nil {
55		return nil, err
56	}
57	wc := &Conn{c: conn, r: bufio.NewReader(conn)}
58	// say hello
59	err = wc.send(cmdInit, "compression=off")
60	return wc, err
61}
62
63func (conn *Conn) Close() error {
64	if conn != nil && conn.c != nil {
65		return conn.c.Close()
66	}
67	return nil
68}
69
70func (conn *Conn) send(cmd command, args ...string) error {
71	buf := make([]byte, 0, 80)
72	buf = append(buf, cmdStrings[cmd]...)
73	for _, a := range args {
74		buf = append(buf, ' ')
75		buf = append(buf, a...)
76	}
77	buf = append(buf, '\n')
78	_, err := conn.c.Write(buf)
79	return err
80}
81
82var errMsgTooLarge = errors.New("message too large")
83
84// recv gets a message from the connection.
85func (conn *Conn) recv() (s []byte, err error) {
86	// A message is:
87	// - a uint32 length
88	// - a byte boolean for compression
89	// - length-5 bytes of data (plain or zlib compressed)
90	var buf [5]byte
91	_, err = io.ReadFull(conn.r, buf[:])
92	if err != nil {
93		return nil, err
94	}
95	length := binary.BigEndian.Uint32(buf[:4])
96	isCompressed := buf[4] == 1
97	if length >= 32<<20 {
98		return nil, errMsgTooLarge
99	}
100
101	s = make([]byte, length-5)
102	_, err = io.ReadFull(conn.r, s)
103	if err != nil {
104		return
105	}
106	if isCompressed {
107		zr, err := zlib.NewReader(bytes.NewBuffer(s))
108		if err != nil {
109			return s, err
110		}
111		return ioutil.ReadAll(zr)
112	}
113	return s, nil
114}
115
116func (conn *Conn) ListBuffers() ([]Buffer, error) {
117	conn.lock.Lock()
118	defer conn.lock.Unlock()
119	var s []byte
120	err := conn.send(cmdHdata, "buffer:gui_buffers(*)")
121	if err == nil {
122		s, err = conn.recv()
123	}
124	if err != nil {
125		return nil, err
126	}
127	msg := message(s)
128	id, typ := msg.Buffer(), msg.GetType()
129	//t.Logf("id=%s type=%v", id, typ)
130	_, _ = id, typ
131	var buflist []Buffer
132	msg.HData(&buflist)
133	return buflist, nil
134}
135
136func (conn *Conn) BufferData(ptr uint64, limit int, filter string) (lines []LineData, err error) {
137	conn.lock.Lock()
138	defer conn.lock.Unlock()
139	var s []byte
140	var path string
141	if limit == 0 {
142		path = fmt.Sprintf("buffer:0x%x/lines/first_line(*)/data", ptr)
143	} else if limit > 0 {
144		path = fmt.Sprintf("buffer:0x%x/lines/first_line(%d)/data", ptr, limit)
145	} else if limit < 0 {
146		path = fmt.Sprintf("buffer:0x%x/lines/last_line(%d)/data", ptr, limit)
147	}
148	err = conn.send(cmdHdata, path, filter)
149	if err == nil {
150		s, err = conn.recv()
151	}
152	if err != nil {
153		return nil, err
154	}
155	msg := message(s)
156	id, typ := msg.Buffer(), msg.GetType()
157	//t.Logf("id=%s type=%v", id, typ)
158	_, _ = id, typ
159	msg.HData(&lines)
160	return lines, nil
161}
162
163func (conn *Conn) BuffersData() (lines []LineData, err error) {
164	conn.lock.Lock()
165	defer conn.lock.Unlock()
166	var s []byte
167	err = conn.send(cmdHdata, "buffer:gui_buffers(*)/lines/first_line(*)/data")
168	if err == nil {
169		s, err = conn.recv()
170	}
171	if err != nil {
172		return nil, err
173	}
174	msg := message(s)
175	id, typ := msg.Buffer(), msg.GetType()
176	//t.Logf("id=%s type=%v", id, typ)
177	_, _ = id, typ
178	msg.HData(&lines)
179	return lines, nil
180}
181