spongy

A Unixy IRC client
git clone https://git.woozle.org/neale/spongy.git

commit
cfe75df
parent
9b44355
author
Neale Pickett
date
2016-01-28 09:11:07 -0700 MST
Merge branch 'master' of https://github.com/nealey/spongy

Conflicts:
	spongyd/network.go
10 files changed,  +349, -66
A spongycli/network.go
+171, -0
  1@@ -0,0 +1,171 @@
  2+package main
  3+
  4+import (
  5+	"bufio"
  6+	"fmt"
  7+	"github.com/go-fsnotify/fsnotify"
  8+	"io/ioutil"
  9+	"os"
 10+	"path"
 11+	"strconv"
 12+	"strings"
 13+	"time"
 14+)
 15+
 16+const eventIdSep = "/"
 17+
 18+type Network struct {
 19+	running bool
 20+
 21+	Name string
 22+	currentLog string
 23+	lineno int64
 24+
 25+	basePath string
 26+	seq int
 27+}
 28+
 29+func NewNetwork(basePath string) (*Network) {
 30+	return &Network{
 31+		running: true,
 32+		Name: path.Base(basePath),
 33+		basePath: basePath,
 34+	}
 35+}
 36+
 37+func (nw *Network) Close() {
 38+	nw.running = false
 39+}
 40+
 41+func (nw *Network) ReadLastEventId(lastEventId string) {
 42+	for _, eventId := range strings.Split(lastEventId, " ") {
 43+		parts := strings.Split(eventId, eventIdSep)
 44+		if len(parts) != 3 {
 45+			continue
 46+		}
 47+		
 48+		if parts[0] != nw.Name {
 49+			continue
 50+		}
 51+		nw.currentLog = parts[1]
 52+		nw.lineno, _ = strconv.ParseInt(parts[2], 10, 64)
 53+		return
 54+	}
 55+}
 56+
 57+func (nw *Network) LastEventId() string {
 58+	parts := []string{nw.Name, nw.currentLog, strconv.FormatInt(nw.lineno, 10)}
 59+	return strings.Join(parts, eventIdSep)
 60+}
 61+
 62+func (nw *Network) errmsg(err error) string {
 63+	return fmt.Sprintf("ERROR: %s", err.Error())
 64+}
 65+
 66+func (nw *Network) Tail(out chan<- string) {
 67+	if nw.currentLog == "" {
 68+		var err error
 69+		
 70+		currentfn := path.Join(nw.basePath, "log", "current")
 71+		nw.currentLog, err = os.Readlink(currentfn)
 72+		if err != nil {
 73+			out <- nw.errmsg(err)
 74+			return
 75+		}
 76+	}
 77+	
 78+	filepath := path.Join(nw.basePath, "log", nw.currentLog)
 79+	f, err := os.Open(filepath)
 80+	if err != nil {
 81+		out <- nw.errmsg(err)
 82+		return
 83+	}
 84+	defer f.Close()
 85+	
 86+	watcher, err := fsnotify.NewWatcher()
 87+	if err != nil {
 88+		out <- nw.errmsg(err)
 89+		return
 90+	}
 91+	defer watcher.Close()
 92+	
 93+	watcher.Add(filepath)
 94+	lineno := int64(0)
 95+	
 96+	// XXX: some way to stop this?
 97+	for nw.running {
 98+		bf := bufio.NewScanner(f)
 99+		for bf.Scan() {
100+			lineno += 1
101+			if lineno <= nw.lineno {
102+				continue
103+			} else {
104+				nw.lineno = lineno
105+			}
106+			
107+			t := bf.Text()
108+			
109+			parts := strings.Split(t, " ")
110+			if (len(parts) >= 3) && (parts[1] == "NEXTLOG") {
111+				watcher.Remove(filepath)
112+				filename := parts[2]
113+				filepath = path.Join(nw.basePath, "log", filename)
114+				f.Close()
115+				f, err = os.Open(filepath)
116+				if err != nil {
117+					out <- nw.errmsg(err)
118+					return
119+				}
120+				watcher.Add(filepath)
121+				lineno = 0
122+				nw.lineno = 0
123+			}
124+			out <- t
125+		}
126+		
127+		select {
128+		case _ = <-watcher.Events:
129+			// Somethin' happened!
130+		case err := <-watcher.Errors:
131+			out <- nw.errmsg(err)
132+			return
133+		}
134+	}
135+}
136+
137+func (nw *Network) Write(data []byte) {
138+	epoch := time.Now().Unix()
139+	pid := os.Getpid()
140+	filename := fmt.Sprintf("%d-%d-%d.txt", epoch, pid, nw.seq)
141+	
142+	filepath := path.Join(nw.basePath, "outq", filename)
143+	ioutil.WriteFile(filepath, data, 0750)
144+	nw.seq += 1
145+}
146+
147+
148+func Networks(basePath string) (found []*Network) {
149+
150+	dir, err := os.Open(basePath)
151+	if err != nil {
152+		return
153+	}
154+	defer dir.Close()
155+	
156+	
157+	entities, _ := dir.Readdirnames(0)
158+	for _, fn := range entities {
159+		netdir := path.Join(basePath, fn)
160+		
161+		_, err = os.Stat(path.Join(netdir, "nick"))
162+		if err != nil {
163+			continue
164+		}
165+		
166+		nw := NewNetwork(netdir)
167+		found = append(found, nw)
168+	}
169+	
170+	return
171+}
172+	
A spongycli/spongycli.go
+55, -0
 1@@ -0,0 +1,55 @@
 2+package main
 3+
 4+import (
 5+	"bufio"
 6+	"fmt"
 7+	"flag"
 8+	"log"
 9+	"os"
10+	"path/filepath"
11+)
12+
13+var playback int
14+var running bool = true
15+
16+func inputLoop(nw *Network) {
17+	bf := bufio.NewScanner(os.Stdin)
18+	for bf.Scan() {
19+		line := bf.Bytes()
20+		nw.Write(line)
21+	}
22+}
23+
24+func usage() {
25+	fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] NETDIR\n", os.Args[0])
26+	fmt.Fprintf(os.Stderr, "\n")
27+	fmt.Fprintf(os.Stderr, "NETDIR is the path to your IRC directory (see README)\n")
28+	fmt.Fprintf(os.Stderr, "\n")
29+	fmt.Fprintf(os.Stderr, "OPTIONS:\n")
30+	flag.PrintDefaults()
31+}
32+
33+func main() {
34+	flag.Usage = usage
35+	flag.IntVar(&playback, "playback", 0, "Number of lines to play back on startup")
36+	
37+	flag.Parse()
38+	if flag.NArg() != 1 {
39+		usage()
40+		os.Exit(2)
41+	}
42+	netDir, err := filepath.Abs(flag.Arg(0))
43+	if err != nil {
44+		log.Fatal(err)
45+	}
46+
47+	nw := NewNetwork(netDir)
48+	defer nw.Close()
49+	go inputLoop(nw)
50+
51+ 	outq := make(chan string, 50) // to stdout
52+	go nw.Tail(outq)
53+	for line := range outq {
54+		fmt.Println(line)
55+	}
56+}
R spongy/irc.go => spongyd/irc.go
+3, -5
 1@@ -3,10 +3,10 @@ package main
 2 import (
 3 	"strconv"
 4 	"strings"
 5-	"fmt"
 6 )
 7 
 8 type Message struct {
 9+	Unparsed   string
10 	Command    string
11 	FullSender string
12 	Sender     string
13@@ -20,6 +20,7 @@ func NewMessage(v string) (Message, error) {
14 	var parts []string
15 	var lhs string
16 
17+	m.Unparsed = v
18 	parts = strings.SplitN(v, " :", 2)
19 	if len(parts) == 2 {
20 		lhs = parts[0]
21@@ -98,11 +99,9 @@ func NewMessage(v string) (Message, error) {
22 }
23 
24 func (m Message) String() string {
25-	args := strings.Join(m.Args, " ")
26-	return fmt.Sprintf("%s %s %s %s %s :%s", m.FullSender, m.Command, m.Sender, m.Forum, args, m.Text)
27+	return m.Unparsed
28 }
29 
30-
31 func SplitTarget(s string) (string, string, string) {
32 	var parts []string
33 
34@@ -130,4 +129,3 @@ func IsChannel(s string) bool {
35 		return false
36 	}
37 }
38-
R spongy/logfile.go => spongyd/logfile.go
+43, -30
  1@@ -3,6 +3,7 @@ package main
  2 import (
  3 	"fmt"
  4 	"os"
  5+	"log"
  6 	"path"
  7 	"time"
  8 )
  9@@ -13,25 +14,54 @@ type Logfile struct {
 10 	name string
 11 	nlines int
 12 	maxlines int
 13+	outq chan string
 14+}
 15+
 16+func timestamp(s string) string {
 17+	ret := fmt.Sprintf("%d %s", time.Now().Unix(), s)
 18+	return ret
 19 }
 20 
 21 func NewLogfile(baseDir string, maxlines int) (*Logfile) {
 22-	return &Logfile{baseDir, nil, "", 0, maxlines}
 23+	lf := Logfile{baseDir, nil, "", 0, maxlines, make(chan string, 50)}
 24+	go lf.processQueue();
 25+	return &lf
 26 }
 27 
 28 func (lf *Logfile) Close() {
 29 	if lf.file != nil {
 30-		lf.writeln("EXIT")
 31-		lf.file.Close()
 32+		lf.Log("EXIT")
 33+		close(lf.outq)
 34 	}
 35 }
 36 
 37-func (lf *Logfile) writeln(s string) error {
 38-	_, err := fmt.Fprintf(lf.file, "%d %s\n", time.Now().Unix(), s)
 39-	if err == nil {
 40+func (lf *Logfile) Log(s string) error {
 41+	lf.outq <- timestamp(s)
 42+	return nil
 43+}
 44+
 45+//
 46+//
 47+
 48+func (lf *Logfile) processQueue() {
 49+	for line := range lf.outq {
 50+		if (lf.file == nil) || (lf.nlines >= lf.maxlines) {
 51+			if err := lf.rotate(); err != nil {
 52+				// Just keep trying, I guess.
 53+				log.Print(err)
 54+				continue
 55+			}
 56+			lf.nlines = 0
 57+		}
 58+
 59+		if _, err := fmt.Fprintln(lf.file, line); err != nil {
 60+			log.Print(err)
 61+			continue
 62+		}
 63 		lf.nlines += 1
 64 	}
 65-	return err
 66+
 67+	lf.file.Close()
 68 }
 69 
 70 func (lf *Logfile) rotate() error {
 71@@ -45,15 +75,15 @@ func (lf *Logfile) rotate() error {
 72 	currentPath := path.Join(lf.baseDir, "log", "current")
 73 	
 74 	if lf.file == nil {
 75-		// Set lf.file just so we can write out NEXTLOG.
 76-		// If this fails, that's okay
 77+		// Open "current" to append a NEXTLOG line.
 78+		// If there's no "current", that's okay
 79 		lf.file, _ = os.OpenFile(currentPath, os.O_WRONLY|os.O_APPEND, 0666)
 80 	}
 81 	
 82 	if lf.file != nil {
 83 		// Note location of new log
 84-		logmsg := fmt.Sprintf(". NEXTLOG %s", fn)
 85-		lf.writeln(logmsg)
 86+		logmsg := fmt.Sprintf("NEXTLOG %s", fn)
 87+		fmt.Fprintln(lf.file, timestamp(logmsg))
 88 		
 89 		// All done with the current log
 90 		lf.file.Close()
 91@@ -66,27 +96,10 @@ func (lf *Logfile) rotate() error {
 92 	os.Remove(currentPath)
 93 	os.Symlink(fn, currentPath)
 94 	
 95-	logmsg := fmt.Sprintf(". PREVLOG %s", lf.name)
 96-	lf.writeln(logmsg)
 97+	logmsg := fmt.Sprintf("PREVLOG %s", lf.name)
 98+	fmt.Fprintln(lf.file, timestamp(logmsg))
 99 	
100 	lf.name = fn
101 	
102 	return nil
103 }
104-
105-func (lf *Logfile) Log(s string) error {
106-	if lf.file == nil {
107-		lf.rotate()
108-	}
109-	
110-	err := lf.writeln(s)
111-	if err == nil {
112-		return err
113-	}
114-
115-	if lf.nlines >= lf.maxlines {
116-		return lf.rotate()
117-	}
118-	
119-	return nil
120-}
R spongy/network.go => spongyd/network.go
+67, -29
  1@@ -9,6 +9,7 @@ import (
  2 	"net"
  3 	"os"
  4 	"os/user"
  5+	"os/exec"
  6 	"path"
  7 	"strings"
  8 	"time"
  9@@ -48,7 +49,9 @@ type Network struct {
 10 	serverIndex int
 11 
 12 	conn io.ReadWriteCloser
 13-	logq chan Message
 14+
 15+	logf *Logfile
 16+	
 17 	inq  chan string
 18 	outq chan string
 19 }
 20@@ -57,23 +60,21 @@ func NewNetwork(basePath string) *Network {
 21 	nw := Network{
 22 		running: true,
 23 		basePath: basePath,
 24-		logq: make(chan Message, 20),
 25 	}
 26-	
 27-	go nw.LogLoop()
 28+	nw.logf = NewLogfile(nw.basePath, int(maxlogsize))
 29 	
 30 	return &nw
 31 }
 32 
 33 func (nw *Network) Close() {
 34 	nw.running = false
 35-	close(nw.logq)
 36 	if nw.conn != nil {
 37 		nw.conn.Close()
 38 	}
 39+	nw.logf.Close()
 40 }
 41 
 42-func (nw *Network) WatchOutqDirectory() {
 43+func (nw *Network) watchOutqDirectory() {
 44 	outqDirname := path.Join(nw.basePath, "outq")
 45 
 46 	dir, err := os.Open(outqDirname)
 47@@ -113,20 +114,12 @@ func (nw *Network) HandleInfile(fn string) {
 48 	}
 49 }
 50 
 51-func (nw *Network) LogLoop() {
 52-	logf := NewLogfile(nw.basePath, int(maxlogsize))
 53-	defer logf.Close()
 54-	
 55-	for m := range nw.logq {
 56-		logf.Log(m.String())
 57-	}
 58-}
 59-
 60-func (nw *Network) ServerWriteLoop() {
 61+func (nw *Network) serverWriteLoop() {
 62 	for v := range nw.outq {
 63-		m, _ := NewMessage(v)
 64-		nw.logq <- m
 65+		debug("ยป %s", v)
 66+		nw.logf.Log(v)
 67 		fmt.Fprintln(nw.conn, v)
 68+		time.Sleep(500 * time.Millisecond)
 69 	}
 70 }
 71 
 72@@ -163,21 +156,20 @@ func (nw *Network) JoinChannels() {
 73 	}
 74 	
 75 	for _, ch := range chans {
 76+		debug("Joining %s", ch)
 77 		nw.outq <- "JOIN " + ch
 78 	}
 79 }
 80 
 81-func (nw *Network) MessageDispatch() {
 82+func (nw *Network) messageDispatchLoop() {
 83 	for line := range nw.inq {
 84+		nw.logf.Log(line)
 85+
 86 		m, err := NewMessage(line)
 87 		if err != nil {
 88 			log.Print(err)
 89 			continue
 90 		}
 91-
 92-		nw.logq <- m
 93-		
 94-		// XXX: Add in a handler subprocess call
 95 		
 96 		switch m.Command {
 97 		case "PING":
 98@@ -196,6 +188,32 @@ func (nw *Network) MessageDispatch() {
 99 				nw.outq <- "NOTICE " + m.Sender + " :\001 VERSION end\001"
100 			}
101 		}
102+
103+		handlerPath := path.Join(nw.basePath, "handler")
104+		cmd := exec.Command(handlerPath, m.Args...)
105+		cmd.Env = []string{
106+			"command=" + m.Command,
107+			"fullsender=" + m.FullSender,
108+			"sender=" + m.Sender,
109+			"forum=" + m.Forum,
110+			"text=" + m.Text,
111+			"raw=" + line,
112+		}
113+		cmd.Stderr = os.Stderr
114+		out, err := cmd.Output()
115+		if err != nil {
116+			log.Print(err)
117+			continue
118+		}
119+
120+		if len(out) > 0 {
121+			outlines := strings.Split(string(out), "\n")
122+			for _, line := range outlines {
123+				if len(line) > 0 {
124+					nw.outq <- line
125+				}
126+			}
127+		}
128 	}
129 }
130 
131@@ -211,6 +229,7 @@ func (nw *Network) ConnectToNextServer() bool {
132 	}
133 	server := servers[nw.serverIndex]
134 
135+	debug("Connecting to %s", server)
136 	switch (server[0]) {
137 	case '|':
138 		parts := strings.Split(server[1:], " ")
139@@ -229,12 +248,21 @@ func (nw *Network) ConnectToNextServer() bool {
140 		log.Print(err)
141 		return false
142 	}
143+	debug("Connected")
144 	
145 	return true
146 }
147 
148 func (nw *Network) login() {
149 	var name string
150+	var username string
151+
152+	usernames, err := ReadLines(path.Join(nw.basePath, "username"))
153+	if err == nil {
154+		username = usernames[0]
155+	} else {
156+		username = "sponge"
157+	}
158 
159 	passwd, err := ReadLines(path.Join(nw.basePath, "passwd"))
160 	if err == nil {
161@@ -255,15 +283,24 @@ func (nw *Network) login() {
162 	}
163 	
164 	if name == "" {
165-		name = "Charlie"
166+		// Rogue used "Rodney" if you didn't give it a name.
167+		// This one works for the ladies, too.
168+		name = "Ronnie"
169 	}
170 
171-	nw.outq <- "USER g g g :" + name
172+	nw.outq <- "USER " + username + " g g :" + name
173 	nw.NextNick()
174 }
175 
176+func (nw *Network) keepaliveLoop() {
177+	for nw.running {
178+		time.Sleep(1 * time.Minute)
179+		nw.outq <- "PING :keepalive"
180+	}
181+}
182+
183 
184-func (nw *Network) Connect(){
185+func (nw *Network) Connect() {
186 	for nw.running {
187 		if ! nw.ConnectToNextServer() {
188 			time.Sleep(8 * time.Second)
189@@ -273,9 +310,10 @@ func (nw *Network) Connect(){
190 		nw.inq = make(chan string, 20)
191 		nw.outq = make(chan string, 20)
192 
193-		go nw.ServerWriteLoop()
194-		go nw.MessageDispatch()
195-		go nw.WatchOutqDirectory()
196+		go nw.serverWriteLoop()
197+		go nw.messageDispatchLoop()
198+		go nw.watchOutqDirectory()
199+		go nw.keepaliveLoop()
200 
201 		nw.login()
202 		
R spongy/network_test.go => spongyd/network_test.go
+0, -0
R spongy/readwritecloserwrapper.go => spongyd/readwritecloserwrapper.go
+0, -0
R spongy/readwritecloserwrapper_test.go => spongyd/readwritecloserwrapper_test.go
+0, -0
R spongy/spongy_test.go => spongyd/spongy_test.go
+0, -0
R spongy/spongy.go => spongyd/spongyd.go
+10, -2
 1@@ -11,8 +11,15 @@ import (
 2 )
 3 
 4 var running bool = true
 5+var verbose bool = false
 6 var maxlogsize uint
 7 
 8+func debug(format string, a ...interface{}) {
 9+	if verbose {
10+		log.Printf(format, a...)
11+	}
12+}
13+
14 func exists(filename string) bool {
15 	_, err := os.Stat(filename); if err != nil {
16 		return false
17@@ -78,8 +85,9 @@ func usage() {
18 
19 func main() {
20 	flag.Usage = usage
21-	flag.UintVar(&maxlogsize, "logsize", 1000, "Log entries before rotating")
22-	notime := flag.Bool("notime", false, "Don't timestamp log messages")
23+	flag.UintVar(&maxlogsize, "logsize", 6000, "Log entries before rotating")
24+	flag.BoolVar(&verbose, "verbose", false, "Verbose logging")
25+	notime := flag.Bool("notime", false, "Don't timestamp debugging messages")
26 	flag.Parse()
27 	if flag.NArg() != 1 {
28 		usage()