noot
This commit is contained in:
commit
2aa412afee
|
@ -0,0 +1 @@
|
||||||
|
.env
|
|
@ -0,0 +1 @@
|
||||||
|
.env
|
|
@ -0,0 +1,78 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
"github.com/lmittmann/tint"
|
||||||
|
"go.uber.org/fx"
|
||||||
|
"go.uber.org/fx/fxevent"
|
||||||
|
"tuxpa.in/a/irc/pkg/ircconn"
|
||||||
|
"tuxpa.in/a/irc/pkg/ircmw"
|
||||||
|
"tuxpa.in/a/irc/pkg/ircv3"
|
||||||
|
"tuxpa.in/a/irc/plugins/auth"
|
||||||
|
"tuxpa.in/a/irc/plugins/useful"
|
||||||
|
)
|
||||||
|
|
||||||
|
func exec(log *slog.Logger) error {
|
||||||
|
tlsConfig := &tls.Config{}
|
||||||
|
conn, err := tls.Dial("tcp", "irc.libera.chat:6697", tlsConfig)
|
||||||
|
//conn, err := net.Dial("tcp", "irc.libera.chat:6667")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
irc := ircconn.New(log, conn, conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
name := os.Getenv("LAIN_NICKNAME")
|
||||||
|
saslPassword := os.Getenv("LAIN_PASSWORD")
|
||||||
|
c := &ircmw.Capabilities{}
|
||||||
|
handler := ircv3.Chain(
|
||||||
|
func(next ircv3.Handler) ircv3.Handler {
|
||||||
|
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||||
|
log.Info("in <<", "msg", m.String())
|
||||||
|
next.Handle(ctx, w, m)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
c.Middleware,
|
||||||
|
(&auth.SaslPlain{
|
||||||
|
Username: name,
|
||||||
|
Password: saslPassword,
|
||||||
|
}).Middleware,
|
||||||
|
(&auth.Nick{Nick: name}).Middleware,
|
||||||
|
(&auth.User{Username: "lain", Realname: "lain a", Hostname: "wired", Server: "wired"}).Middleware,
|
||||||
|
ircmw.CapabilityServerTime,
|
||||||
|
(&useful.Autojoin{Channels: []string{"#lainmaxxing"}}).Middleware,
|
||||||
|
).Handler(ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||||
|
}))
|
||||||
|
|
||||||
|
err = irc.Serve(ctx, handler)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
lain := fx.New(
|
||||||
|
fx.Provide(func() *slog.Logger {
|
||||||
|
return slog.New(
|
||||||
|
tint.NewHandler(os.Stderr, &tint.Options{
|
||||||
|
Level: slog.LevelDebug,
|
||||||
|
TimeFormat: time.Kitchen,
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
fx.WithLogger(func(s *slog.Logger) fxevent.Logger {
|
||||||
|
return &fxevent.SlogLogger{Logger: s}
|
||||||
|
}),
|
||||||
|
fx.Invoke(exec),
|
||||||
|
)
|
||||||
|
lain.Run()
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
module tuxpa.in/a/irc
|
||||||
|
|
||||||
|
go 1.22.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/lmittmann/tint v1.0.4
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0
|
||||||
|
go.uber.org/fx v1.21.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
go.uber.org/dig v1.17.1 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
go.uber.org/zap v1.26.0 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,28 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
|
||||||
|
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
|
||||||
|
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||||
|
go.uber.org/fx v1.21.1 h1:RqBh3cYdzZS0uqwVeEjOX2p73dddLpym315myy/Bpb0=
|
||||||
|
go.uber.org/fx v1.21.1/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48=
|
||||||
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0=
|
||||||
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,69 @@
|
||||||
|
package ircconn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/valyala/bytebufferpool"
|
||||||
|
"tuxpa.in/a/irc/pkg/ircdecoder"
|
||||||
|
"tuxpa.in/a/irc/pkg/ircv3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
w io.Writer
|
||||||
|
r io.Reader
|
||||||
|
log *slog.Logger
|
||||||
|
muWrite sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(log *slog.Logger, w io.Writer, r io.Reader) *Conn {
|
||||||
|
return &Conn{
|
||||||
|
log: log,
|
||||||
|
w: w,
|
||||||
|
r: r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// while serve is running, the conn owns the reader.
|
||||||
|
func (c *Conn) Serve(ctx context.Context, h ircv3.Handler) error {
|
||||||
|
// once serve is called, we call with an empty message.
|
||||||
|
h.Handle(ctx, c, &ircv3.Message{})
|
||||||
|
dec := &ircdecoder.Decoder{}
|
||||||
|
r := c.r
|
||||||
|
r = bufio.NewReaderSize(c.r, 10240)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
msg := &ircv3.Message{}
|
||||||
|
r = io.LimitReader(r, 8191+512)
|
||||||
|
err := dec.Decode(r, msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.Handle(ctx, c, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conn) WriteMessage(msg *ircv3.Message) error {
|
||||||
|
b := bytebufferpool.Get()
|
||||||
|
defer bytebufferpool.Put(b)
|
||||||
|
err := msg.Encode(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b.WriteString("\r\n")
|
||||||
|
c.muWrite.Lock()
|
||||||
|
defer c.muWrite.Unlock()
|
||||||
|
c.log.Info("out >", "msg", msg.String())
|
||||||
|
_, err = b.WriteTo(c.w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package ircdecoder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"tuxpa.in/a/irc/pkg/ircv3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Decoder struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) readByte(r io.Reader) (byte, error) {
|
||||||
|
var o [1]byte
|
||||||
|
if c, ok := r.(io.ByteReader); ok {
|
||||||
|
return c.ReadByte()
|
||||||
|
}
|
||||||
|
_, err := io.ReadFull(r, o[:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return o[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a message from the stream
|
||||||
|
func (d *Decoder) Decode(r io.Reader, msg *ircv3.Message) error {
|
||||||
|
return d.decode(r, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) decodeTags(r io.Reader, msg *ircv3.Message) error {
|
||||||
|
// we assume we have already read the @
|
||||||
|
if msg.Tags == nil {
|
||||||
|
msg.Tags = make(ircv3.Tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
kb := new(strings.Builder)
|
||||||
|
vb := new(strings.Builder)
|
||||||
|
readingValue := false
|
||||||
|
for {
|
||||||
|
// keep reading until space
|
||||||
|
b, err := d.readByte(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b == ';' {
|
||||||
|
msg.Tags.Set(kb.String(), vb.String())
|
||||||
|
readingValue = false
|
||||||
|
continue
|
||||||
|
} else if b == '=' {
|
||||||
|
readingValue = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if b == 0x20 {
|
||||||
|
kstr := kb.String()
|
||||||
|
if !utf8.ValidString(kstr) {
|
||||||
|
return fmt.Errorf("non utf-8 tag key")
|
||||||
|
}
|
||||||
|
msg.Tags.Set(kstr, ircv3.UnescapeTagValue(vb.String()))
|
||||||
|
readingValue = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if readingValue {
|
||||||
|
vb.WriteByte(b)
|
||||||
|
} else {
|
||||||
|
// TODO: technically we should check the validity of key bytes, not scan for utf8 at the end
|
||||||
|
// <key_name> ::= <non-empty sequence of ascii letters, digits, hyphens ('-')>
|
||||||
|
kb.WriteByte(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Decoder) decodeSource(r io.Reader, msg *ircv3.Message) error {
|
||||||
|
// we assume we have already read the :
|
||||||
|
buf := new(strings.Builder)
|
||||||
|
for {
|
||||||
|
// keep reading until space
|
||||||
|
b, err := d.readByte(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b == 0x20 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf.WriteByte(b)
|
||||||
|
}
|
||||||
|
nuh, err := ircv3.ParseNUH(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
msg.Source = &nuh
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a message from the stream
|
||||||
|
func (d *Decoder) decode(r io.Reader, msg *ircv3.Message) error {
|
||||||
|
b, err := d.readByte(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch b {
|
||||||
|
case '@':
|
||||||
|
if err := d.decodeTags(r, msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err = d.readByte(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b == ':' {
|
||||||
|
if err := d.decodeSource(r, msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err = d.readByte(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ':':
|
||||||
|
if err := d.decodeSource(r, msg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b, err = d.readByte(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
cb := new(strings.Builder)
|
||||||
|
// at this point we've no matter waht read the first byte of the command in b
|
||||||
|
cb.WriteByte(b)
|
||||||
|
// add a limit reader for the irc size limit
|
||||||
|
r = io.LimitReader(r, 511)
|
||||||
|
// read until first space
|
||||||
|
for {
|
||||||
|
b, err := d.readByte(r)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
msg.Command = cb.String()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b == 0x20 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cb.WriteByte(b)
|
||||||
|
}
|
||||||
|
msg.Command = cb.String()
|
||||||
|
cb.Reset()
|
||||||
|
// now read the params
|
||||||
|
|
||||||
|
var trailing bool
|
||||||
|
var lastCr bool
|
||||||
|
for {
|
||||||
|
b, err := d.readByte(r)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
if cb.Len() > 0 {
|
||||||
|
msg.Params = append(msg.Params, cb.String())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if cb.Len() == 0 {
|
||||||
|
if b == ':' {
|
||||||
|
trailing = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !trailing {
|
||||||
|
if b == 0x20 {
|
||||||
|
msg.Params = append(msg.Params, cb.String())
|
||||||
|
cb.Reset()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if b == '\r' {
|
||||||
|
lastCr = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lastCr {
|
||||||
|
if b == '\n' {
|
||||||
|
msg.Params = append(msg.Params, cb.String())
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
cb.WriteByte('\r')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb.WriteByte(b)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package ircdecoder_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"tuxpa.in/a/irc/pkg/ircdecoder"
|
||||||
|
"tuxpa.in/a/irc/pkg/ircv3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecoder(t *testing.T) {
|
||||||
|
dec := ircdecoder.Decoder{}
|
||||||
|
|
||||||
|
buf := strings.NewReader("@some :source HELLO")
|
||||||
|
msg := new(ircv3.Message)
|
||||||
|
err := dec.Decode(buf, msg)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.EqualValues(t, "HELLO", msg.Command)
|
||||||
|
require.EqualValues(t, "", msg.Tags["some"])
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package ircmw
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"tuxpa.in/a/irc/pkg/ircv3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Capabilities struct {
|
||||||
|
pending int
|
||||||
|
}
|
||||||
|
|
||||||
|
var capabilitiesKey struct{}
|
||||||
|
|
||||||
|
func AddPending(ctx context.Context, i int) {
|
||||||
|
val, ok := ctx.Value(capabilitiesKey).(*Capabilities)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val.pending += i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Capabilities) Middleware(next ircv3.Handler) ircv3.Handler {
|
||||||
|
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||||
|
ctx = context.WithValue(ctx, capabilitiesKey, c)
|
||||||
|
if m.Command == "" {
|
||||||
|
c.pending++
|
||||||
|
w.WriteMessage(ircv3.NewMessage("CAP", "LS", "302"))
|
||||||
|
}
|
||||||
|
next.Handle(ctx, w, m)
|
||||||
|
if m.Command == "CAP" && m.Param(0) == "*" && m.Param(1) == "LS" {
|
||||||
|
c.pending--
|
||||||
|
}
|
||||||
|
if c.pending == 0 {
|
||||||
|
c.pending = -1
|
||||||
|
w.WriteMessage(ircv3.NewMessage("CAP", "END"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func CapabilityExchange(next ircv3.Handler) ircv3.Handler {
|
||||||
|
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||||
|
next.Handle(ctx, w, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyServerTime struct{}
|
||||||
|
|
||||||
|
func ServerTime(ctx context.Context) time.Time {
|
||||||
|
val, ok := ctx.Value(keyServerTime).(time.Time)
|
||||||
|
if !ok {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func CapabilityServerTime(next ircv3.Handler) ircv3.Handler {
|
||||||
|
enabled := false
|
||||||
|
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||||
|
if m.Command == "CAP" && m.Param(0) == "*" && m.Param(1) == "LS" {
|
||||||
|
if strings.Contains(m.Param(2), "server-time") {
|
||||||
|
AddPending(ctx, 1)
|
||||||
|
w.WriteMessage(ircv3.NewMessage("CAP", "REQ", "server-time"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m.Command == "CAP" && m.Param(1) == "ACK" && m.Param(2) == "server-time" {
|
||||||
|
AddPending(ctx, -1)
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
if enabled {
|
||||||
|
tString := m.Tags.Get("time")
|
||||||
|
if tString != "" {
|
||||||
|
parsedTime, err := time.Parse("2006-01-02T15:04:05.000Z", tString)
|
||||||
|
if err == nil {
|
||||||
|
ctx = context.WithValue(ctx, keyServerTime, parsedTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next.Handle(ctx, w, m)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package ircv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
Handle(ctx context.Context, w MessageWriter, m *Message)
|
||||||
|
}
|
||||||
|
type HandlerFunc func(ctx context.Context, w MessageWriter, m *Message)
|
||||||
|
|
||||||
|
func (h HandlerFunc) Handle(ctx context.Context, w MessageWriter, m *Message) {
|
||||||
|
h(ctx, w, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageWriter interface {
|
||||||
|
WriteMessage(msg *Message) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Middleware func(next Handler) Handler
|
||||||
|
|
||||||
|
type Middlewares []func(Handler) Handler
|
||||||
|
|
||||||
|
// Chain returns a Middlewares type from a slice of middleware handlers.
|
||||||
|
func Chain(middlewares ...func(Handler) Handler) Middlewares {
|
||||||
|
return Middlewares(middlewares)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler builds and returns a http.Handler from the chain of middlewares,
|
||||||
|
// with `h http.Handler` as the final handler.
|
||||||
|
func (mws Middlewares) Handler(h Handler) Handler {
|
||||||
|
return &ChainHandler{h, chain(mws, h), mws}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlerFunc builds and returns a http.Handler from the chain of middlewares,
|
||||||
|
// with `h http.Handler` as the final handler.
|
||||||
|
func (mws Middlewares) HandlerFunc(h HandlerFunc) Handler {
|
||||||
|
return &ChainHandler{h, chain(mws, h), mws}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChainHandler is a http.Handler with support for handler composition and
|
||||||
|
// execution.
|
||||||
|
type ChainHandler struct {
|
||||||
|
Endpoint Handler
|
||||||
|
chain Handler
|
||||||
|
Middlewares Middlewares
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ChainHandler) Handle(ctx context.Context, w MessageWriter, m *Message) {
|
||||||
|
c.chain.Handle(ctx, w, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// chain builds a http.Handler composed of an inline middleware stack and endpoint
|
||||||
|
// handler in the order they are passed.
|
||||||
|
func chain(middlewares []func(Handler) Handler, endpoint Handler) Handler {
|
||||||
|
// Return ahead of time if there aren't any middlewares for the chain
|
||||||
|
if len(middlewares) == 0 {
|
||||||
|
return endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap the end handler with the middleware chain
|
||||||
|
h := middlewares[len(middlewares)-1](endpoint)
|
||||||
|
for i := len(middlewares) - 2; i >= 0; i-- {
|
||||||
|
h = middlewares[i](h)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package ircv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Source *NUH
|
||||||
|
Command string
|
||||||
|
Params []string
|
||||||
|
Tags Tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessage(command string, params ...string) *Message {
|
||||||
|
return &Message{
|
||||||
|
Source: nil,
|
||||||
|
Command: command,
|
||||||
|
Params: params,
|
||||||
|
Tags: make(Tags),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (m *Message) SetSource(nuh *NUH) *Message {
|
||||||
|
m.Source = nuh
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
func (m *Message) Param(i int) string {
|
||||||
|
if len(m.Params) > i {
|
||||||
|
return m.Params[i]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
func (msg *Message) String() string {
|
||||||
|
sb := new(strings.Builder)
|
||||||
|
msg.Encode(sb)
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg *Message) Encode(w io.Writer) error {
|
||||||
|
if msg.Command == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if msg.Tags != nil && len(msg.Tags) > 0 {
|
||||||
|
_, err := msg.Tags.WriteTo(w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = w.Write([]byte(" "))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msg.Source != nil {
|
||||||
|
if _, err := w.Write([]byte(msg.Source.String())); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := w.Write([]byte(" ")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now write command
|
||||||
|
if _, err := w.Write([]byte(msg.Command)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := w.Write([]byte(" ")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for idx, v := range msg.Params {
|
||||||
|
if idx != 0 {
|
||||||
|
if _, err := w.Write([]byte(" ")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now write any params
|
||||||
|
if len(msg.Params)-1 == idx && strings.Contains(v, " ") {
|
||||||
|
if _, err := w.Write([]byte(":")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := w.Write([]byte(v)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NUH struct {
|
||||||
|
Name string
|
||||||
|
User string
|
||||||
|
Host string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
MalformedNUH = errors.New("NUH is malformed")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseNUH parses a NUH source of an IRC message into its constituent parts;
|
||||||
|
// name (nickname or server name), username, and hostname.
|
||||||
|
func ParseNUH(in string) (out NUH, err error) {
|
||||||
|
if len(in) == 0 {
|
||||||
|
return out, MalformedNUH
|
||||||
|
}
|
||||||
|
hostStart := strings.IndexByte(in, '@')
|
||||||
|
if hostStart != -1 {
|
||||||
|
out.Host = in[hostStart+1:]
|
||||||
|
in = in[:hostStart]
|
||||||
|
}
|
||||||
|
userStart := strings.IndexByte(in, '!')
|
||||||
|
if userStart != -1 {
|
||||||
|
out.User = in[userStart+1:]
|
||||||
|
in = in[:userStart]
|
||||||
|
}
|
||||||
|
out.Name = in
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canonical returns the canonical string representation of the NUH.
|
||||||
|
func (nuh *NUH) String() (result string) {
|
||||||
|
var out strings.Builder
|
||||||
|
out.Grow(len(nuh.Name) + len(nuh.User) + len(nuh.Host) + 2)
|
||||||
|
out.WriteString(nuh.Name)
|
||||||
|
if len(nuh.User) != 0 {
|
||||||
|
out.WriteByte('!')
|
||||||
|
out.WriteString(nuh.User)
|
||||||
|
}
|
||||||
|
if len(nuh.Host) != 0 {
|
||||||
|
out.WriteByte('@')
|
||||||
|
out.WriteString(nuh.Host)
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
package ircv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// taken from https://github.com/ergochat/irc-go/blob/master/ircmsg/tags.go
|
||||||
|
|
||||||
|
var (
|
||||||
|
// valtoescape replaces real characters with message tag escapes.
|
||||||
|
valtoescape = strings.NewReplacer("\\", "\\\\", ";", "\\:", " ", "\\s", "\r", "\\r", "\n", "\\n")
|
||||||
|
|
||||||
|
escapedCharLookupTable [256]byte
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for i := 0; i < 256; i += 1 {
|
||||||
|
escapedCharLookupTable[i] = byte(i)
|
||||||
|
}
|
||||||
|
escapedCharLookupTable[':'] = ';'
|
||||||
|
escapedCharLookupTable['s'] = ' '
|
||||||
|
escapedCharLookupTable['r'] = '\r'
|
||||||
|
escapedCharLookupTable['n'] = '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapeTagValue takes a value, and returns an escaped message tag value.
|
||||||
|
func EscapeTagValue(inString string) string {
|
||||||
|
return valtoescape.Replace(inString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnescapeTagValue takes an escaped message tag value, and returns the raw value.
|
||||||
|
func UnescapeTagValue(inString string) string {
|
||||||
|
// buf.Len() == 0 is the fastpath where we have not needed to unescape any chars
|
||||||
|
var buf strings.Builder
|
||||||
|
remainder := inString
|
||||||
|
for {
|
||||||
|
backslashPos := strings.IndexByte(remainder, '\\')
|
||||||
|
|
||||||
|
if backslashPos == -1 {
|
||||||
|
if buf.Len() == 0 {
|
||||||
|
return inString
|
||||||
|
} else {
|
||||||
|
buf.WriteString(remainder)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else if backslashPos == len(remainder)-1 {
|
||||||
|
// trailing backslash, which we strip
|
||||||
|
if buf.Len() == 0 {
|
||||||
|
return inString[:len(inString)-1]
|
||||||
|
} else {
|
||||||
|
buf.WriteString(remainder[:len(remainder)-1])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-trailing backslash detected; we're now on the slowpath
|
||||||
|
// where we modify the string
|
||||||
|
if buf.Len() == 0 {
|
||||||
|
buf.Grow(len(inString)) // just an optimization
|
||||||
|
}
|
||||||
|
buf.WriteString(remainder[:backslashPos])
|
||||||
|
buf.WriteByte(escapedCharLookupTable[remainder[backslashPos+1]])
|
||||||
|
remainder = remainder[backslashPos+2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://ircv3.net/specs/extensions/message-tags.html#rules-for-naming-message-tags
|
||||||
|
func validateTagName(name string) bool {
|
||||||
|
if len(name) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if name[0] == '+' {
|
||||||
|
name = name[1:]
|
||||||
|
}
|
||||||
|
if len(name) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// let's err on the side of leniency here; allow -./ (45-47) in any position
|
||||||
|
for i := 0; i < len(name); i++ {
|
||||||
|
c := name[i]
|
||||||
|
if !(('-' <= c && c <= '/') || ('0' <= c && c <= '9') || ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Tag values MUST be encoded as UTF8."
|
||||||
|
func ValidateTagValue(value string) bool {
|
||||||
|
return utf8.ValidString(value)
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package ircv3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tags are stored as strings
|
||||||
|
type Tags map[string]string
|
||||||
|
|
||||||
|
func (h Tags) Set(key, value string) {
|
||||||
|
h[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Tags) Get(key string) string {
|
||||||
|
val, ok := h[key]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Tags) Keys() []string {
|
||||||
|
o := make([]string, 0, len(h))
|
||||||
|
for k := range h {
|
||||||
|
o = append(o, k)
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
func (h Tags) ClientOnlyKeys() []string {
|
||||||
|
o := make([]string, 0)
|
||||||
|
for k := range h {
|
||||||
|
if IsClientOnly(k) {
|
||||||
|
o = append(o, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsClientOnly(xs string) bool {
|
||||||
|
return strings.HasPrefix(xs, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Tags) WriteTo(w io.Writer) (int64, error) {
|
||||||
|
var nn int64
|
||||||
|
n, err := w.Write([]byte{'@'})
|
||||||
|
nn += int64(n)
|
||||||
|
if err != nil {
|
||||||
|
return nn, err
|
||||||
|
}
|
||||||
|
firstTag := true
|
||||||
|
for k, v := range h {
|
||||||
|
if !firstTag {
|
||||||
|
n, err = w.Write([]byte(";"))
|
||||||
|
nn += int64(n)
|
||||||
|
if err != nil {
|
||||||
|
return nn, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
firstTag = false
|
||||||
|
n, err := w.Write([]byte(k))
|
||||||
|
nn += int64(n)
|
||||||
|
if err != nil {
|
||||||
|
return nn, err
|
||||||
|
}
|
||||||
|
if len(v) > 0 {
|
||||||
|
n, err = w.Write([]byte("=" + EscapeTagValue(v)))
|
||||||
|
nn += int64(n)
|
||||||
|
if err != nil {
|
||||||
|
return nn, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nn, nil
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"tuxpa.in/a/irc/pkg/ircmw"
|
||||||
|
"tuxpa.in/a/irc/pkg/ircv3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SaslPlain struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (saslplain *SaslPlain) Middleware(next ircv3.Handler) ircv3.Handler {
|
||||||
|
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||||
|
if m.Command == "" {
|
||||||
|
ircmw.AddPending(ctx, 1)
|
||||||
|
w.WriteMessage(ircv3.NewMessage("CAP", "REQ", "sasl"))
|
||||||
|
}
|
||||||
|
if m.Command == "CAP" && m.Param(0) == "*" && m.Param(1) == "ACK" && m.Param(2) == "sasl" {
|
||||||
|
w.WriteMessage(ircv3.NewMessage("AUTHENTICATE", "PLAIN"))
|
||||||
|
}
|
||||||
|
if m.Command == "AUTHENTICATE" && m.Param(0) == "+" {
|
||||||
|
w.WriteMessage(ircv3.NewMessage("AUTHENTICATE", base64.StdEncoding.EncodeToString([]byte(
|
||||||
|
saslplain.Username+string([]byte{0})+
|
||||||
|
saslplain.Username+string([]byte{0})+
|
||||||
|
saslplain.Password,
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
switch m.Command {
|
||||||
|
case "903", "904", "905", "906", "907":
|
||||||
|
ircmw.AddPending(ctx, -1)
|
||||||
|
}
|
||||||
|
next.Handle(ctx, w, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Username string
|
||||||
|
Realname string
|
||||||
|
Hostname string
|
||||||
|
Server string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Middleware(next ircv3.Handler) ircv3.Handler {
|
||||||
|
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||||
|
if m.Command == "" {
|
||||||
|
w.WriteMessage(ircv3.NewMessage("USER", u.Username, u.Hostname, u.Server, u.Realname))
|
||||||
|
}
|
||||||
|
next.Handle(ctx, w, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Nick struct {
|
||||||
|
Nick string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Nick) Middleware(next ircv3.Handler) ircv3.Handler {
|
||||||
|
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||||
|
if m.Command == "" {
|
||||||
|
w.WriteMessage(ircv3.NewMessage("NICK", u.Nick))
|
||||||
|
}
|
||||||
|
next.Handle(ctx, w, m)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package useful
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"tuxpa.in/a/irc/pkg/ircv3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Autojoin struct {
|
||||||
|
Channels []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *Autojoin) Middleware(next ircv3.Handler) ircv3.Handler {
|
||||||
|
return ircv3.HandlerFunc(func(ctx context.Context, w ircv3.MessageWriter, m *ircv3.Message) {
|
||||||
|
if m.Command == "005" {
|
||||||
|
w.WriteMessage(ircv3.NewMessage("JOIN", strings.Join(u.Channels, ",")))
|
||||||
|
}
|
||||||
|
next.Handle(ctx, w, m)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue