aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2024-05-10 12:03:29 -0400
committerXe Iaso <me@xeiaso.net>2024-05-10 14:36:37 -0400
commite8a099f150bf5bd7599a6a44298c101580b10b84 (patch)
tree982e344f1f38ed455698373db28778f068e22349 /cmd
parent5df84ba08d59ef9acf276f84080bf34e566ae648 (diff)
downloadx-e8a099f150bf5bd7599a6a44298c101580b10b84.tar.xz
x-e8a099f150bf5bd7599a6a44298c101580b10b84.zip
cmd/mimi: add IRC announcer bot API call
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mimi/main.go7
-rw-r--r--cmd/mimi/modules/irc/announcer.go96
-rw-r--r--cmd/mimi/modules/irc/irc.go68
3 files changed, 171 insertions, 0 deletions
diff --git a/cmd/mimi/main.go b/cmd/mimi/main.go
index 8a386ce..398414f 100644
--- a/cmd/mimi/main.go
+++ b/cmd/mimi/main.go
@@ -11,6 +11,7 @@ import (
"within.website/x/cmd/mimi/internal"
"within.website/x/cmd/mimi/modules/discord"
"within.website/x/cmd/mimi/modules/discord/flyio"
+ "within.website/x/cmd/mimi/modules/irc"
"within.website/x/cmd/mimi/modules/scheduling"
)
@@ -39,6 +40,11 @@ func main() {
d.Open()
+ ircBot, err := irc.New(ctx)
+ if err != nil {
+ log.Fatalf("error creating irc module: %v", err)
+ }
+
slog.Info("bot started")
gs := grpc.NewServer()
@@ -48,6 +54,7 @@ func main() {
mux := http.NewServeMux()
b.RegisterHTTP(mux)
+ ircBot.RegisterHTTP(mux)
go func() {
log.Fatal(gs.Serve(lis))
diff --git a/cmd/mimi/modules/irc/announcer.go b/cmd/mimi/modules/irc/announcer.go
new file mode 100644
index 0000000..eb58798
--- /dev/null
+++ b/cmd/mimi/modules/irc/announcer.go
@@ -0,0 +1,96 @@
+package irc
+
+import (
+ "context"
+ "log/slog"
+ "net/http"
+ "os"
+ "path/filepath"
+ "sync"
+
+ "github.com/twitchtv/twirp"
+ "google.golang.org/protobuf/types/known/emptypb"
+ "tailscale.com/jsondb"
+ "within.website/x/cmd/mimi/internal"
+ "within.website/x/proto/external/jsonfeed"
+ "within.website/x/proto/mimi/announce"
+)
+
+func (m *Module) RegisterHTTP(mux *http.ServeMux) {
+ mux.Handle(announce.AnnouncePathPrefix, announce.NewAnnounceServer(&AnnounceService{Module: m}))
+}
+
+type AnnounceService struct {
+ *Module
+ db *jsondb.DB[State]
+ lock sync.Mutex
+}
+
+type State struct {
+ Announced map[string]struct{}
+}
+
+func (a *AnnounceService) initDB() error {
+ if a.db != nil {
+ slog.Debug("db already initialized")
+ return nil
+ }
+
+ fname := filepath.Join(internal.DataDir(), "announced.json")
+ slog.Debug("opening jsondb for announced items", "fname", fname)
+
+ if _, err := os.Stat(fname); os.IsNotExist(err) {
+ os.WriteFile(fname, []byte(`{"Announced":{}}`), 0644)
+ }
+
+ db, err := jsondb.Open[State](fname)
+ if err != nil {
+ return err
+ }
+ db.Save()
+
+ if db.Data == nil {
+ slog.Debug("creating empty state")
+ db.Data = &State{
+ Announced: map[string]struct{}{
+ "https://within.website/": struct{}{},
+ },
+ }
+ if err := db.Save(); err != nil {
+ return err
+ }
+ }
+
+ a.db = db
+
+ return nil
+}
+
+func (a *AnnounceService) Announce(ctx context.Context, msg *jsonfeed.Item) (*emptypb.Empty, error) {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+
+ if err := a.initDB(); err != nil {
+ return nil, twirp.InternalErrorWith(err)
+ }
+
+ if msg.Title == "" {
+ return nil, twirp.NewError(twirp.InvalidArgument, "missing title")
+ }
+ if msg.Url == "" {
+ return nil, twirp.NewError(twirp.InvalidArgument, "missing url")
+ }
+
+ if _, ok := a.db.Data.Announced[msg.Url]; ok {
+ return &emptypb.Empty{}, nil
+ }
+
+ a.db.Data.Announced[msg.Url] = struct{}{}
+ if err := a.db.Save(); err != nil {
+ return nil, twirp.InternalErrorWith(err)
+ }
+
+ a.conn.Privmsgf(*ircChannel, "New article :: %s :: %s", msg.Title, msg.Url)
+
+ return &emptypb.Empty{}, nil
+}
diff --git a/cmd/mimi/modules/irc/irc.go b/cmd/mimi/modules/irc/irc.go
new file mode 100644
index 0000000..03d2bb8
--- /dev/null
+++ b/cmd/mimi/modules/irc/irc.go
@@ -0,0 +1,68 @@
+package irc
+
+import (
+ "context"
+ "crypto/tls"
+ "flag"
+ "fmt"
+ "log/slog"
+
+ ircevent "github.com/thoj/go-ircevent"
+)
+
+var (
+ ircServer = flag.String("irc-server", "irc.libera.chat:6697", "IRC server to connect to")
+ ircUsername = flag.String("irc-username", "[Mara]", "IRC username")
+ ircIdent = flag.String("irc-ident", "mara", "IRC ident")
+ ircPassword = flag.String("irc-password", "", "IRC password")
+ ircChannel = flag.String("irc-channel", "#mimi", "IRC channel to join")
+)
+
+type Module struct {
+ conn *ircevent.Connection
+}
+
+func New(ctx context.Context) (*Module, error) {
+ conn := ircevent.IRC(*ircUsername, *ircIdent)
+ conn.UseTLS = true
+ conn.UseSASL = false
+ conn.SASLLogin = *ircUsername
+ conn.SASLPassword = *ircPassword
+ conn.SASLMech = "PLAIN"
+
+ conn.TLSConfig = &tls.Config{
+ ServerName: "irc.libera.chat",
+ }
+
+ conn.AddCallback("001", func(e *ircevent.Event) {
+ conn.Privmsgf("NickServ", "IDENTIFY %s %s", *ircUsername, *ircPassword)
+ })
+
+ conn.AddCallback("900", func(e *ircevent.Event) {
+ conn.Join(*ircChannel)
+ slog.Debug("joined channel", "channel", *ircChannel)
+ })
+ if err := conn.Connect(*ircServer); err != nil {
+ return nil, fmt.Errorf("irc: error connecting to IRC server: %w", err)
+ }
+
+ go func() {
+ <-ctx.Done()
+ conn.Quit()
+ }()
+
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case err := <-conn.Error:
+ slog.Error("error from IRC server", "err", err)
+ }
+ }
+ }()
+
+ return &Module{
+ conn: conn,
+ }, nil
+}