diff options
author | Simon Garrelou <simon@sixfoisneuf.fr> | 2023-11-09 20:48:06 +0100 |
---|---|---|
committer | Simon Garrelou <simon@sixfoisneuf.fr> | 2023-11-09 20:48:06 +0100 |
commit | ea6ef13df36ee220a336a83f4db92b3326b69bc4 (patch) | |
tree | c4624324e7d2572baa5e4083d26e77bd718feb6d | |
download | noticeme-ea6ef13df36ee220a336a83f4db92b3326b69bc4.tar.gz noticeme-ea6ef13df36ee220a336a83f4db92b3326b69bc4.zip |
First commit
-rw-r--r-- | .envrc | 1 | ||||
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | cmd/noticeme/main.go | 54 | ||||
-rw-r--r-- | go.mod | 9 | ||||
-rw-r--r-- | go.sum | 49 | ||||
-rw-r--r-- | irc.go | 107 | ||||
-rw-r--r-- | message.go | 64 | ||||
-rw-r--r-- | shell.nix | 10 |
8 files changed, 297 insertions, 0 deletions
@@ -0,0 +1 @@ | |||
use nix | |||
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d23f327 --- /dev/null +++ b/.gitignore | |||
@@ -0,0 +1,3 @@ | |||
1 | .direnv | ||
2 | *.toml | ||
3 | /noticeme | ||
diff --git a/cmd/noticeme/main.go b/cmd/noticeme/main.go new file mode 100644 index 0000000..f85ad5b --- /dev/null +++ b/cmd/noticeme/main.go | |||
@@ -0,0 +1,54 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "flag" | ||
5 | "io/ioutil" | ||
6 | "log" | ||
7 | "os/exec" | ||
8 | |||
9 | "git.sixfoisneuf.fr/noticeme" | ||
10 | "github.com/pelletier/go-toml" | ||
11 | ) | ||
12 | |||
13 | type Config struct { | ||
14 | Server string | ||
15 | Username string | ||
16 | Password string | ||
17 | |||
18 | Highlights []string | ||
19 | Command string | ||
20 | } | ||
21 | |||
22 | func monitorMessage(hl []string, cmd string, c chan noticeme.Message) { | ||
23 | for msg := range c { | ||
24 | if msg.IsHighlight(hl) { | ||
25 | c := exec.Command(cmd, msg.String()) | ||
26 | if err := c.Run(); err != nil { | ||
27 | log.Printf("Error running command: %v", err) | ||
28 | } | ||
29 | } | ||
30 | } | ||
31 | } | ||
32 | |||
33 | func main() { | ||
34 | configPath := flag.String("config", "config.toml", "Path to the configuration file") | ||
35 | flag.Parse() | ||
36 | |||
37 | var cfg Config | ||
38 | data, err := ioutil.ReadFile(*configPath) | ||
39 | if err != nil { | ||
40 | log.Fatalf("error reading '%s': %v", *configPath, err) | ||
41 | } | ||
42 | |||
43 | if err := toml.Unmarshal(data, &cfg); err != nil { | ||
44 | log.Fatalf("error parsing '%s': %v", *configPath, err) | ||
45 | } | ||
46 | |||
47 | bouncer := noticeme.NewBouncer(cfg.Server, cfg.Username, cfg.Password) | ||
48 | |||
49 | go monitorMessage(cfg.Highlights, cfg.Command, bouncer.Messages) | ||
50 | |||
51 | if err := bouncer.Connect(); err != nil { | ||
52 | panic(err) | ||
53 | } | ||
54 | } | ||
@@ -0,0 +1,9 @@ | |||
1 | module git.sixfoisneuf.fr/noticeme | ||
2 | |||
3 | go 1.20 | ||
4 | |||
5 | require ( | ||
6 | git.sr.ht/~taiite/senpai v0.2.0 // indirect | ||
7 | github.com/pelletier/go-toml v1.9.5 // indirect | ||
8 | golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect | ||
9 | ) | ||
@@ -0,0 +1,49 @@ | |||
1 | git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc/go.mod h1:t+Ww6SR24yYnXzEWiNlOY0AFo5E9B73X++10lrSpp4U= | ||
2 | git.sr.ht/~taiite/senpai v0.2.0 h1:a9ZzJj6gKFJ5gz+s6sVcq94DSCPrzFFR/hIh6yONS+U= | ||
3 | git.sr.ht/~taiite/senpai v0.2.0/go.mod h1:oSJWECrxhhe0IFO8mJLn5YtP5Ze/r70W6fuCHmhwqCE= | ||
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
5 | github.com/delthas/go-libnp v0.0.0-20221222161248-0e45ece1f878/go.mod h1:aGVXnhWpDlt5U4SphG97o1gszctZKvBTXy320E8Buw4= | ||
6 | github.com/delthas/go-localeinfo v0.0.0-20221116001557-686a1e185118/go.mod h1:sG54BxlyQgIskYURLrg7mvhoGBe0Qq12DNtYRALwNa4= | ||
7 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= | ||
8 | github.com/gdamore/tcell/v2 v2.5.4-0.20221017224006-ede1dd5ee680/go.mod h1:XmCynGHvvGG7UFI6az9zzoEHBvZB1PGf5APwOJMFUyE= | ||
9 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= | ||
10 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= | ||
11 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= | ||
12 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||
13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||
14 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | ||
15 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | ||
16 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= | ||
17 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= | ||
18 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= | ||
19 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||
20 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= | ||
21 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||
22 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||
23 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||
24 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||
25 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||
26 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||
27 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||
28 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
29 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||
30 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||
31 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||
32 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
33 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
34 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||
35 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||
36 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||
37 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||
38 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||
39 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||
40 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||
41 | golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs= | ||
42 | golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||
43 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||
44 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||
45 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||
46 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||
47 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
48 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= | ||
49 | mvdan.cc/xurls/v2 v2.3.0/go.mod h1:AjuTy7gEiUArFMjgBBDU4SMxlfUYsRokpJQgNWOt3e4= | ||
@@ -0,0 +1,107 @@ | |||
1 | package noticeme | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "log" | ||
6 | "net" | ||
7 | |||
8 | "git.sr.ht/~taiite/senpai/irc" | ||
9 | ) | ||
10 | |||
11 | type network struct { | ||
12 | NetID string | ||
13 | |||
14 | in <-chan irc.Message | ||
15 | out chan<- irc.Message | ||
16 | session *irc.Session | ||
17 | } | ||
18 | |||
19 | type Bouncer struct { | ||
20 | networks []*network | ||
21 | |||
22 | Server string | ||
23 | Username, Password string | ||
24 | |||
25 | Messages chan Message | ||
26 | } | ||
27 | |||
28 | func NewBouncer(server, username, password string) *Bouncer { | ||
29 | b := Bouncer{ | ||
30 | Server: server, | ||
31 | Username: username, | ||
32 | Password: password, | ||
33 | } | ||
34 | b.Messages = make(chan Message, 100) | ||
35 | |||
36 | return &b | ||
37 | } | ||
38 | |||
39 | func (b *Bouncer) Connect() error { | ||
40 | return b.newNetwork("", false) | ||
41 | } | ||
42 | |||
43 | func (b *Bouncer) newNetwork(netID string, background bool) error { | ||
44 | auth := irc.SASLPlain{ | ||
45 | Username: b.Username, | ||
46 | Password: b.Password, | ||
47 | } | ||
48 | |||
49 | params := irc.SessionParams{ | ||
50 | Nickname: "NoticeMe", | ||
51 | Username: b.Username, | ||
52 | RealName: b.Username, | ||
53 | NetID: netID, | ||
54 | Auth: &auth, | ||
55 | } | ||
56 | |||
57 | conn, err := net.Dial("tcp", b.Server) | ||
58 | if err != nil { | ||
59 | return fmt.Errorf("net.Dial: %w", err) | ||
60 | } | ||
61 | |||
62 | n := &network{ | ||
63 | NetID: netID, | ||
64 | } | ||
65 | |||
66 | n.in, n.out = irc.ChanInOut(conn) | ||
67 | n.session = irc.NewSession(n.out, params) | ||
68 | |||
69 | b.networks = append(b.networks, n) | ||
70 | |||
71 | if background { | ||
72 | go b.monitorNetwork(n) | ||
73 | } else { | ||
74 | b.monitorNetwork(n) | ||
75 | } | ||
76 | |||
77 | return nil | ||
78 | } | ||
79 | |||
80 | func (b *Bouncer) hasNetID(netID string) bool { | ||
81 | for _, net := range b.networks { | ||
82 | if net.NetID == netID { | ||
83 | return true | ||
84 | } | ||
85 | } | ||
86 | return false | ||
87 | } | ||
88 | |||
89 | func (b *Bouncer) monitorNetwork(n *network) { | ||
90 | log.Printf("Monitoring NetID<%s>", n.NetID) | ||
91 | |||
92 | for msg := range n.in { | ||
93 | switch msg.Command { | ||
94 | case "PRIVMSG": | ||
95 | b.Messages <- fromIrcMessage(msg, n.session) | ||
96 | case "BOUNCER": | ||
97 | if msg.Params[0] != "NETWORK" { | ||
98 | log.Printf("[%s] Invalid BOUNCER message, stopping. msg: %+v", n.NetID, msg) | ||
99 | break | ||
100 | } | ||
101 | |||
102 | if !b.hasNetID(msg.Params[1]) { | ||
103 | b.newNetwork(msg.Params[1], true) | ||
104 | } | ||
105 | } | ||
106 | } | ||
107 | } | ||
diff --git a/message.go b/message.go new file mode 100644 index 0000000..19c4e30 --- /dev/null +++ b/message.go | |||
@@ -0,0 +1,64 @@ | |||
1 | package noticeme | ||
2 | |||
3 | import ( | ||
4 | "fmt" | ||
5 | "strings" | ||
6 | |||
7 | "git.sr.ht/~taiite/senpai/irc" | ||
8 | ) | ||
9 | |||
10 | type Message struct { | ||
11 | Channel string // a #channel or a user | ||
12 | Author string | ||
13 | |||
14 | Text string | ||
15 | |||
16 | isMe bool | ||
17 | } | ||
18 | |||
19 | func fromIrcMessage(msg irc.Message, sess *irc.Session) Message { | ||
20 | user := msg.Prefix.Name | ||
21 | channel := msg.Params[0] | ||
22 | txt := msg.Params[1] | ||
23 | |||
24 | return Message{ | ||
25 | Channel: channel, | ||
26 | Author: user, | ||
27 | Text: txt, | ||
28 | isMe: sess.IsMe(user), | ||
29 | } | ||
30 | } | ||
31 | |||
32 | func (m Message) IsPrivate() bool { | ||
33 | return m.Channel[0] != '#' | ||
34 | } | ||
35 | |||
36 | func (m Message) IsHighlight(matches []string) bool { | ||
37 | // special case: if the message is a PM and is not | ||
38 | // sent by us, highlight | ||
39 | if m.IsPrivate() && !m.isMe { | ||
40 | return true | ||
41 | } | ||
42 | |||
43 | lower := make([]string, len(matches)) | ||
44 | |||
45 | for i, m := range matches { | ||
46 | lower[i] = strings.ToLower(m) | ||
47 | } | ||
48 | |||
49 | for _, l := range lower { | ||
50 | if strings.Contains(strings.ToLower(m.Text), l) { | ||
51 | return true | ||
52 | } | ||
53 | } | ||
54 | |||
55 | return false | ||
56 | } | ||
57 | func (m Message) String() string { | ||
58 | c := m.Channel | ||
59 | |||
60 | if m.IsPrivate() { | ||
61 | c = "private" | ||
62 | } | ||
63 | return fmt.Sprintf("[%s] <%s> %s", c, m.Author, m.Text) | ||
64 | } | ||
diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..867d0d3 --- /dev/null +++ b/shell.nix | |||
@@ -0,0 +1,10 @@ | |||
1 | let | ||
2 | pkgs = import <nixpkgs> {}; | ||
3 | in | ||
4 | pkgs.mkShell { | ||
5 | packages = [ | ||
6 | pkgs.go | ||
7 | pkgs.gopls | ||
8 | pkgs.gotools | ||
9 | ]; | ||
10 | } | ||