diff options
| author | Xe Iaso <me@xeiaso.net> | 2024-05-10 12:03:29 -0400 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2024-05-10 14:36:37 -0400 |
| commit | e8a099f150bf5bd7599a6a44298c101580b10b84 (patch) | |
| tree | 982e344f1f38ed455698373db28778f068e22349 /cmd | |
| parent | 5df84ba08d59ef9acf276f84080bf34e566ae648 (diff) | |
| download | x-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.go | 7 | ||||
| -rw-r--r-- | cmd/mimi/modules/irc/announcer.go | 96 | ||||
| -rw-r--r-- | cmd/mimi/modules/irc/irc.go | 68 |
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 +} |
