Split client into its own repo
This commit is contained in:
parent
30eaf147f1
commit
9303e6f8e5
|
@ -0,0 +1 @@
|
|||
icon-*.png
|
|
@ -0,0 +1,56 @@
|
|||
Copyright (c) 2015 Neale Pickett
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
The software is provided "as is", without warranty of any kind, express or
|
||||
implied, including but not limited to the warranties of merchantability,
|
||||
fitness for a particular purpose and noninfringement. In no event shall
|
||||
the authors or copyright holders be liable for any claim, damages or
|
||||
other liability, whether in an action of contract, tort or otherwise,
|
||||
arising from, out of or in connection with the Software or the use or
|
||||
other dealings in the Software.
|
||||
|
||||
|
||||
-------------------------
|
||||
|
||||
|
||||
Parts of this program are from circ,
|
||||
which was obtained under the following license:
|
||||
|
||||
|
||||
Copyright 2012, Google Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
28
Makefile
28
Makefile
|
@ -1,24 +1,18 @@
|
|||
ICONS += app/icon-16.png
|
||||
ICONS += app/icon-32.png
|
||||
ICONS += app/icon-48.png
|
||||
ICONS += app/icon-128.png
|
||||
ICONS += app/icon-256.png
|
||||
ICONS += icon-16.png
|
||||
ICONS += icon-32.png
|
||||
ICONS += icon-48.png
|
||||
ICONS += icon-128.png
|
||||
ICONS += icon-256.png
|
||||
|
||||
all: icons serverside
|
||||
|
||||
serverside: spongy spongy.cgi
|
||||
|
||||
spongy: src/spongy/spongy.go
|
||||
GOPATH=$(CURDIR) go build -v $@
|
||||
|
||||
spongy.cgi: src/spongy.cgi/spongy.cgi.go
|
||||
GOPATH=$(CURDIR) go build -v $@
|
||||
chmod +s $@
|
||||
all: icons
|
||||
|
||||
icons: $(ICONS)
|
||||
|
||||
app/icon-%.png: chat.svg
|
||||
icon-%.png: icon.svg
|
||||
inkscape --export-png=$@ --export-width=$* $<
|
||||
|
||||
package: icons
|
||||
cd app && zip -ru ../package.zip .
|
||||
git ls-files | zip -ru -@ /tmp/spongy-client-chrome.zip
|
||||
|
||||
clean:
|
||||
rm -f $(ICONS)
|
||||
|
|
37
README.md
37
README.md
|
@ -1,30 +1,13 @@
|
|||
Spongy IRC
|
||||
=========
|
||||
Spongy IRC Client for Chrome
|
||||
===================
|
||||
|
||||
This is a sort of bouncer for clients with transient network connections,
|
||||
like cell phones and laptops.
|
||||
It's a lot like [tapchat](https://github.com/tapchat/tapchat) but is a whole lot simpler
|
||||
while being (at time of writing) much more feature-complete.
|
||||
This is a Chrome client for the
|
||||
[Spongy IRC Bouncer Thing](https://github.com/nealey/spongy).
|
||||
|
||||
It supports (currently) an JavaScript browser-based client,
|
||||
and can also be worked from the command-line using Unix tools like "tail" and "echo".
|
||||
The user interface is based heavily on
|
||||
[circ](https://github.com/flackr/circ).
|
||||
And when I say "heavily" here,
|
||||
I mean that I lifted it almost verbatim and touched only what was required to have it work with my JavaScript.
|
||||
|
||||
Ironically, it doesn't currently work with any existing IRC clients,
|
||||
although we are kicking around ideas for such a thing.
|
||||
Honestly, though, if you want a bouncer for a traditional IRC client,
|
||||
you are better off using something like znc.
|
||||
|
||||
We have an [architectural diagram](https://docs.google.com/drawings/d/1am_RTUh89kul-318GoYK73AbjOE_jMYi4vI4NyEgKrY/edit?usp=sharing) if you care about such things.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
* Gracefully handles clients with transient networking, such as cell phones and laptops.
|
||||
|
||||
|
||||
Other Documentation
|
||||
-------------------
|
||||
|
||||
* [Installation Instructions](INSTALL.md)
|
||||
* [Todo list](TODO.md)
|
||||
You probably want to install this from the Chrome Store.
|
||||
I'll put a link to that when I have one.
|
||||
|
|
109
doc/INSTALL.md
109
doc/INSTALL.md
|
@ -1,109 +0,0 @@
|
|||
Spongy Server Installation
|
||||
==========================
|
||||
|
||||
You gotta make a base directory with an `authtok` file,
|
||||
and a subdirectory for every server you want to connect to.
|
||||
|
||||
BASE_DIRECTORY
|
||||
+-- slashnet
|
||||
| +-- handler
|
||||
| +-- config/
|
||||
| | +-- server
|
||||
| | +-- gecos
|
||||
| | +-- nick
|
||||
| +-- log/
|
||||
| | +-- 2015-01-29T19:56:27Z.log
|
||||
| | +-- 2015-01-29T20:01:15Z.log
|
||||
| | +-- 2015-01-29T20:41:40Z.log
|
||||
| | +-- 2015-01-29T20:41:48Z.log
|
||||
| | +-- 2015-01-29T20:41:56Z.log
|
||||
| | +-- 2015-01-29T20:42:44Z.log
|
||||
| +-- outq/
|
||||
+-- oftc
|
||||
+-- server3
|
||||
+-- server4
|
||||
|
||||
|
||||
`config` directory
|
||||
------------------
|
||||
|
||||
The `config` directory in a server directory must have certain files:
|
||||
|
||||
* `server` is a list of servers to try and connect to, in the form `hostname:port`
|
||||
* `gecos` is your "Real Name"
|
||||
* `nick` is a list of nicknames you'd like to use
|
||||
|
||||
The lists are gone through starting with the first entry until one sticks.
|
||||
|
||||
|
||||
`outq` directory
|
||||
----------------
|
||||
|
||||
The `outq` directory is monitored by spongy.
|
||||
When a new file shows up, its contents are spit out verbatim
|
||||
over the server connection.
|
||||
|
||||
So if you want to send a message to a channel,
|
||||
do something like this:
|
||||
|
||||
$ echo 'PRIVMSG #channel :hello world' > outq/$$.$(date +%s)
|
||||
|
||||
|
||||
Starting up
|
||||
-----------
|
||||
|
||||
Pretty easy:
|
||||
|
||||
$ cd BASE_DIRECTORY; /path/to/spongy
|
||||
|
||||
Spongy will go off and connect to every configured server in BASE_DIRECTORY.
|
||||
|
||||
|
||||
Spongy CGI Configuration
|
||||
========================
|
||||
|
||||
If you'd like to run `spongy.cgi`,
|
||||
that's fine,
|
||||
but you have to create a file
|
||||
called `spongy.basedir`
|
||||
in the same directory as the CGI.
|
||||
You can do it like this:
|
||||
|
||||
$ echo '/home/neale/BASE_DIRECTORY' > spongy.basedir
|
||||
|
||||
And then,
|
||||
in `BASE_DIRECTORY`,
|
||||
you need a file called `auth`
|
||||
with a sha256 checksum of the authorization token
|
||||
you want to use in the client.
|
||||
|
||||
You can make it like this:
|
||||
|
||||
$ printf 'my fabulous token' | sha256sum | cut -d\ -f1 > BASE_DIRECTORY
|
||||
|
||||
|
||||
Permissions
|
||||
-----------
|
||||
|
||||
There are a lot of different ways to set up permissions.
|
||||
Here's what I suggest:
|
||||
make `spongy.cgi` setuid to you.
|
||||
|
||||
$ chmod +s spongy.cgi
|
||||
|
||||
If it's setuid,
|
||||
you don't need to make your config file
|
||||
(or any other files)
|
||||
readable by the user that
|
||||
runs the web server.
|
||||
|
||||
Sadly,
|
||||
Apache has a whole bunch of weirdness in place
|
||||
which prevents setuid CGI from working
|
||||
without a lot of configuration twiddling.
|
||||
But it also has its own mechanism for running CGI
|
||||
as the user who owns it.
|
||||
So if you're using Apache,
|
||||
please send me a recipe for your solution,
|
||||
and I'll add it to the distribution :)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Neale Pickett
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
wirc protocol
|
||||
===========
|
||||
|
||||
This document attempts to describe the wirc protocol.
|
||||
The source code will always be authoritative,
|
||||
but this protocol has been around for a while now so should be changed often, if ever.
|
||||
|
||||
|
||||
Out Queue
|
||||
---------
|
||||
|
||||
|
||||
Any files that appear in the directory outq/ are written verbatim to the IRC server.
|
||||
|
||||
You may put multiple lines in a single file.
|
||||
|
||||
Filenames beginning with "." are ignored.
|
||||
|
||||
You are advised to create files beginning with ".",
|
||||
then rename them on completion of the write,
|
||||
to avoid race conditions.
|
||||
|
||||
|
||||
Log
|
||||
---
|
||||
|
||||
### Log Filenames
|
||||
|
||||
TBD
|
||||
|
||||
|
||||
### Log Messages
|
||||
|
||||
IRC messages are written to the log, one message per line.
|
||||
Messages are translated to an easier-to-parse format:
|
||||
|
||||
timestamp fullname command sender forum [args...] :text
|
||||
|
||||
Where:
|
||||
|
||||
* `timestamp` is in Unix epoch time.
|
||||
* `fullname` is the full name of the message origin (typically `nick!user@host.name`)
|
||||
* `command` is the IRC command
|
||||
* `sender` is the IRC name of the entity that sent the message
|
||||
* `forum` is the IRC name of the audience of the message
|
||||
* `args` are any additional arguments not otherwise covered
|
||||
* `text` is the text of the message
|
||||
|
||||
`sender` and `forum` are provided in every message, for the convenience of the client.
|
||||
A PRIVMSG to `sender` will make it back to whomever sent the message,
|
||||
a PRIVMSG to `forum` will be seen by everyone in the audience.
|
||||
|
||||
For example, a "private message" will have `sender` equal to `forum`.
|
||||
But a "channel message" will have `forum` set to the channel.
|
||||
|
||||
See `wirc.go` for details of each message type.
|
||||
|
||||
|
||||
### Initial Messages
|
||||
|
||||
Each log file will contain the following initial messages,
|
||||
to facilitate stateful clients:
|
||||
|
||||
TBD
|
|
@ -1,7 +0,0 @@
|
|||
Todo list
|
||||
=========
|
||||
|
||||
* One server instance for all networks
|
||||
* One cgi for all networks?
|
||||
* Make README suck less
|
||||
* nickname name= attribute set to uhost
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
@ -1,87 +0,0 @@
|
|||
package logfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Logfile struct {
|
||||
file *os.File
|
||||
name string
|
||||
nlines int
|
||||
maxlines int
|
||||
}
|
||||
|
||||
func NewLogfile(maxlines int) (*Logfile) {
|
||||
return &Logfile{nil, "", 0, maxlines}
|
||||
}
|
||||
|
||||
func (lf *Logfile) Close() {
|
||||
if lf.file != nil {
|
||||
lf.writeln("EXIT")
|
||||
lf.file.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (lf *Logfile) writeln(s string) error {
|
||||
_, err := fmt.Fprintf(lf.file, "%d %s\n", time.Now().Unix(), s)
|
||||
if err == nil {
|
||||
lf.nlines += 1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (lf *Logfile) rotate() error {
|
||||
fn := fmt.Sprintf("%s.log", time.Now().UTC().Format(time.RFC3339))
|
||||
newf, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if lf.file == nil {
|
||||
// Set lf.file just so we can write out NEXTLOG.
|
||||
// If this fails, that's okay
|
||||
lf.file, _ = os.OpenFile("current", os.O_WRONLY|os.O_APPEND, 0666)
|
||||
}
|
||||
|
||||
if lf.file != nil {
|
||||
// Note location of new log
|
||||
logmsg := fmt.Sprintf(". NEXTLOG %s", fn)
|
||||
lf.writeln(logmsg)
|
||||
|
||||
// All done with the current log
|
||||
lf.file.Close()
|
||||
}
|
||||
|
||||
// Point to new log file
|
||||
lf.file = newf
|
||||
|
||||
// Record symlink to new log
|
||||
os.Remove("current")
|
||||
os.Symlink(fn, "current")
|
||||
|
||||
logmsg := fmt.Sprintf(". PREVLOG %s", lf.name)
|
||||
lf.writeln(logmsg)
|
||||
|
||||
lf.name = fn
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lf *Logfile) Log(s string) error {
|
||||
if lf.file == nil {
|
||||
lf.rotate()
|
||||
}
|
||||
|
||||
err := lf.writeln(s)
|
||||
if err == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if lf.nlines >= lf.maxlines {
|
||||
return lf.rotate()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-fsnotify/fsnotify"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"bufio"
|
||||
"strconv"
|
||||
"strings"
|
||||
"net/http"
|
||||
"net/http/cgi"
|
||||
"time"
|
||||
"path"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
cgi.Handler
|
||||
}
|
||||
|
||||
var NetworkDir string
|
||||
|
||||
func ReadString(fn string) string {
|
||||
octets, err := ioutil.ReadFile(fn)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return strings.TrimSpace(string(octets))
|
||||
}
|
||||
|
||||
func tail(w http.ResponseWriter, filename string, pos int64) {
|
||||
var err error
|
||||
|
||||
currentfn := path.Join(NetworkDir, "current")
|
||||
if filename == "" {
|
||||
filename, err = os.Readlink(currentfn)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
filepath := path.Join(NetworkDir, filename)
|
||||
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
watcher.Add(filepath)
|
||||
|
||||
for {
|
||||
printid := false
|
||||
|
||||
newpos, err := f.Seek(pos, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if newpos != pos {
|
||||
log.Fatal("Lost my position in the log, somehow (log truncated?)")
|
||||
}
|
||||
|
||||
bf := bufio.NewScanner(f)
|
||||
for bf.Scan() {
|
||||
t := bf.Text()
|
||||
pos += int64(len(t)) + 1 // XXX: this breaks if we ever see \r\n
|
||||
|
||||
parts := strings.Split(t, " ")
|
||||
if (len(parts) >= 4) && (parts[2] == "NEXTLOG") {
|
||||
watcher.Remove(filepath)
|
||||
filename = parts[3]
|
||||
filepath = path.Join(NetworkDir, filename)
|
||||
f.Close()
|
||||
f, err = os.Open(filepath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
watcher.Add(filepath)
|
||||
pos = 0
|
||||
}
|
||||
fmt.Fprintf(w, "data: %s\n", t)
|
||||
printid = true
|
||||
}
|
||||
if printid {
|
||||
_, err = fmt.Fprintf(w, "id: %s/%d\n\n", filename, pos)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
w.(http.Flusher).Flush()
|
||||
|
||||
select {
|
||||
case _ = <-watcher.Events:
|
||||
// Somethin' happened!
|
||||
case err := <-watcher.Errors:
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleCommand(w http.ResponseWriter, text string, target string) {
|
||||
fn := path.Join(NetworkDir, fmt.Sprintf("outq/cgi.%d", time.Now().Unix()))
|
||||
f, err := os.Create(fn)
|
||||
if err != nil {
|
||||
fmt.Fprintln(w, "NO: Cannot create outq file")
|
||||
fmt.Fprintln(w, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(text, "/quote "):
|
||||
fmt.Fprintln(f, text[7:])
|
||||
case strings.HasPrefix(text, "/me "):
|
||||
fmt.Fprintf(f, "PRIVMSG %s :\001ACTION %s\001\n", target, text[4:])
|
||||
default:
|
||||
fmt.Fprintf(f, "PRIVMSG %s :%s\n", target, text)
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "OK")
|
||||
}
|
||||
|
||||
|
||||
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
BaseDir := "networks"
|
||||
DefaultDir := path.Join(BaseDir, "default")
|
||||
NetworkDir = path.Join(BaseDir, r.FormValue("network"))
|
||||
|
||||
if path.Dir(DefaultDir) != path.Dir(NetworkDir) {
|
||||
NetworkDir = DefaultDir
|
||||
}
|
||||
|
||||
authtok := ReadString(path.Join(NetworkDir, "authtok"))
|
||||
if r.FormValue("auth") != authtok {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintln(w, "NO: Invalid authtok")
|
||||
return
|
||||
}
|
||||
switch r.FormValue("type") {
|
||||
case "command":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
handleCommand(w, r.Form.Get("text"), r.FormValue("target"))
|
||||
default:
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
parts := strings.Split(os.Getenv("HTTP_LAST_EVENT_ID"), "/")
|
||||
if len(parts) == 2 {
|
||||
filename := path.Base(parts[0])
|
||||
pos, _ := strconv.ParseInt(parts[1], 0, 64)
|
||||
tail(w, filename, pos)
|
||||
} else {
|
||||
tail(w, "", 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
h := Handler{}
|
||||
if err := cgi.Serve(h); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
282
spongy/spongy.go
282
spongy/spongy.go
|
@ -1,282 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"logfile"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Command string
|
||||
FullSender string
|
||||
Sender string
|
||||
Forum string
|
||||
Args []string
|
||||
Text string
|
||||
}
|
||||
|
||||
var running bool = true
|
||||
var nick string
|
||||
var gecos string
|
||||
var maxlogsize uint
|
||||
var logq chan Message
|
||||
|
||||
func isChannel(s string) bool {
|
||||
if (s == "") {
|
||||
return false
|
||||
}
|
||||
|
||||
switch s[0] {
|
||||
case '#', '&', '!', '+', '.', '-':
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (m Message) String() string {
|
||||
args := strings.Join(m.Args, " ")
|
||||
return fmt.Sprintf("%s %s %s %s %s :%s", m.FullSender, m.Command, m.Sender, m.Forum, args, m.Text)
|
||||
}
|
||||
|
||||
func logLoop() {
|
||||
logf := logfile.NewLogfile(int(maxlogsize))
|
||||
defer logf.Close()
|
||||
|
||||
for m := range logq {
|
||||
logf.Log(m.String())
|
||||
}
|
||||
}
|
||||
|
||||
func nuhost(s string) (string, string, string) {
|
||||
var parts []string
|
||||
|
||||
parts = strings.SplitN(s, "!", 2)
|
||||
if len(parts) == 1 {
|
||||
return s, "", ""
|
||||
}
|
||||
n := parts[0]
|
||||
parts = strings.SplitN(parts[1], "@", 2)
|
||||
if len(parts) == 1 {
|
||||
return s, "", ""
|
||||
}
|
||||
return n, parts[0], parts[1]
|
||||
}
|
||||
|
||||
func connect(host string, dotls bool) (net.Conn, error) {
|
||||
if dotls {
|
||||
config := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
return tls.Dial("tcp", host, config)
|
||||
} else {
|
||||
return net.Dial("tcp", host)
|
||||
}
|
||||
}
|
||||
|
||||
func readLoop(conn net.Conn, inq chan<- string) {
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
inq <- scanner.Text()
|
||||
}
|
||||
close(inq)
|
||||
}
|
||||
|
||||
func writeLoop(conn net.Conn, outq <-chan string) {
|
||||
for v := range outq {
|
||||
m, _ := parse(v)
|
||||
logq <- m
|
||||
fmt.Fprintln(conn, v)
|
||||
}
|
||||
}
|
||||
|
||||
func parse(v string) (Message, error) {
|
||||
var m Message
|
||||
var parts []string
|
||||
var lhs string
|
||||
|
||||
parts = strings.SplitN(v, " :", 2)
|
||||
if len(parts) == 2 {
|
||||
lhs = parts[0]
|
||||
m.Text = parts[1]
|
||||
} else {
|
||||
lhs = v
|
||||
m.Text = ""
|
||||
}
|
||||
|
||||
m.FullSender = "."
|
||||
m.Forum = "."
|
||||
m.Sender = "."
|
||||
|
||||
parts = strings.Split(lhs, " ")
|
||||
if parts[0][0] == ':' {
|
||||
m.FullSender = parts[0][1:]
|
||||
parts = parts[1:]
|
||||
|
||||
n, u, _ := nuhost(m.FullSender)
|
||||
if u != "" {
|
||||
m.Sender = n
|
||||
}
|
||||
}
|
||||
|
||||
m.Command = strings.ToUpper(parts[0])
|
||||
switch m.Command {
|
||||
case "PRIVMSG", "NOTICE":
|
||||
switch {
|
||||
case isChannel(parts[1]):
|
||||
m.Forum = parts[1]
|
||||
case m.FullSender == ".":
|
||||
m.Forum = parts[1]
|
||||
default:
|
||||
m.Forum = m.Sender
|
||||
}
|
||||
case "PART", "MODE", "TOPIC", "KICK":
|
||||
m.Forum = parts[1]
|
||||
m.Args = parts[2:]
|
||||
case "JOIN":
|
||||
if len(parts) == 1 {
|
||||
m.Forum = m.Text
|
||||
m.Text = ""
|
||||
} else {
|
||||
m.Forum = parts[1]
|
||||
}
|
||||
case "INVITE":
|
||||
if m.Text != "" {
|
||||
m.Forum = m.Text
|
||||
m.Text = ""
|
||||
} else {
|
||||
m.Forum = parts[2]
|
||||
}
|
||||
case "NICK":
|
||||
if len(parts) > 1 {
|
||||
m.Sender = parts[1]
|
||||
m.Args = parts[2:]
|
||||
} else {
|
||||
m.Sender = m.Text
|
||||
m.Text = ""
|
||||
m.Args = parts[1:]
|
||||
}
|
||||
m.Forum = m.Sender
|
||||
case "353":
|
||||
m.Forum = parts[3]
|
||||
default:
|
||||
numeric, _ := strconv.Atoi(m.Command)
|
||||
if numeric >= 300 {
|
||||
if len(parts) > 2 {
|
||||
m.Forum = parts[2]
|
||||
}
|
||||
}
|
||||
m.Args = parts[1:]
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func dispatch(outq chan<- string, m Message) {
|
||||
logq <- m
|
||||
switch m.Command {
|
||||
case "PING":
|
||||
outq <- "PONG :" + m.Text
|
||||
case "433":
|
||||
nick = nick + "_"
|
||||
outq <- fmt.Sprintf("NICK %s", nick)
|
||||
}
|
||||
}
|
||||
|
||||
func handleInfile(path string, outq chan<- string) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
os.Remove(path)
|
||||
inf := bufio.NewScanner(f)
|
||||
for inf.Scan() {
|
||||
txt := inf.Text()
|
||||
outq <- txt
|
||||
}
|
||||
}
|
||||
|
||||
func monitorDirectory(dirname string, dir *os.File, outq chan<- string) {
|
||||
latest := time.Unix(0, 0)
|
||||
for running {
|
||||
fi, err := dir.Stat()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
current := fi.ModTime()
|
||||
if current.After(latest) {
|
||||
latest = current
|
||||
dn, _ := dir.Readdirnames(0)
|
||||
for _, fn := range dn {
|
||||
path := dirname + string(os.PathSeparator) + fn
|
||||
handleInfile(path, outq)
|
||||
}
|
||||
_, _ = dir.Seek(0, 0)
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] HOST:PORT\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func main() {
|
||||
dotls := flag.Bool("notls", true, "Disable TLS security")
|
||||
outqdir := flag.String("outq", "outq", "Output queue directory")
|
||||
flag.UintVar(&maxlogsize, "logsize", 1000, "Log entries before rotating")
|
||||
flag.StringVar(&gecos, "gecos", "Bob The Merry Slug", "Gecos entry (full name)")
|
||||
|
||||
flag.Parse()
|
||||
if flag.NArg() != 2 {
|
||||
fmt.Fprintln(os.Stderr, "Error: must specify nickname and host")
|
||||
os.Exit(69)
|
||||
}
|
||||
|
||||
dir, err := os.Open(*outqdir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
nick := flag.Arg(0)
|
||||
host := flag.Arg(1)
|
||||
|
||||
conn, err := connect(host, *dotls)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
inq := make(chan string)
|
||||
outq := make(chan string)
|
||||
logq = make(chan Message)
|
||||
go logLoop()
|
||||
go readLoop(conn, inq)
|
||||
go writeLoop(conn, outq)
|
||||
go monitorDirectory(*outqdir, dir, outq)
|
||||
|
||||
outq <- fmt.Sprintf("NICK %s", nick)
|
||||
outq <- fmt.Sprintf("USER %s %s %s: %s", nick, nick, nick, gecos)
|
||||
for v := range inq {
|
||||
p, err := parse(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dispatch(outq, p)
|
||||
}
|
||||
|
||||
running = false
|
||||
|
||||
close(outq)
|
||||
close(logq)
|
||||
}
|
Loading…
Reference in New Issue