This commit is contained in:
a 2024-05-14 00:51:42 -05:00
commit 2aa412afee
Signed by: a
GPG Key ID: 374BC539FE795AF0
15 changed files with 960 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.env

1
cmd/lain/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.env

78
cmd/lain/main.go Normal file
View File

@ -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()
}

21
go.mod Normal file
View File

@ -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
)

28
go.sum Normal file
View File

@ -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=

69
pkg/ircconn/conn.go Normal file
View File

@ -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
}

197
pkg/ircdecoder/decoder.go Normal file
View File

@ -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)
}
}

View File

@ -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"])
}

83
pkg/ircmw/cap.go Normal file
View File

@ -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)
})
}

68
pkg/ircv3/handler.go Normal file
View File

@ -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
}

133
pkg/ircv3/message.go Normal file
View File

@ -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()
}

View File

@ -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)
}

76
pkg/ircv3/tags.go Normal file
View File

@ -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
}

67
plugins/auth/auth.go Normal file
View File

@ -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)
})
}

View File

@ -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)
})
}