Initial commit
This commit is contained in:
commit
1cd56f1525
76
bus/bus.go
Normal file
76
bus/bus.go
Normal file
@ -0,0 +1,76 @@
|
||||
package bus
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type NotifyBus struct {
|
||||
conn *dbus.Conn
|
||||
}
|
||||
|
||||
type BusNotification struct {
|
||||
ID uint32 `json:"id,omitempty"`
|
||||
Summary string `json:"summary"`
|
||||
Body string `json:"body,omitempty"`
|
||||
Timeout time.Duration `json:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
type NotifyServerInfo struct {
|
||||
Name string
|
||||
Vendor string
|
||||
Version string
|
||||
SpecVersion string
|
||||
}
|
||||
|
||||
func NewNotifyBus() (*NotifyBus, error) {
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &NotifyBus{conn: conn}, nil
|
||||
}
|
||||
|
||||
func (n *NotifyBus) Close() {
|
||||
n.conn.Close()
|
||||
}
|
||||
|
||||
func (n *NotifyBus) ServerInfo() (*NotifyServerInfo, error) {
|
||||
obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
|
||||
call := obj.Call(
|
||||
"org.freedesktop.Notifications.GetServerInformation", // Method
|
||||
0, // Flags
|
||||
)
|
||||
if call.Err != nil {
|
||||
return nil, call.Err
|
||||
}
|
||||
|
||||
srvInfo := &NotifyServerInfo{}
|
||||
call.Store(&srvInfo.Name, &srvInfo.Vendor, &srvInfo.Version, &srvInfo.SpecVersion)
|
||||
return srvInfo, nil
|
||||
}
|
||||
|
||||
func (n *NotifyBus) Notify(notification BusNotification) (uint32, error) {
|
||||
obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
|
||||
var ret uint32
|
||||
call := obj.Call(
|
||||
"org.freedesktop.Notifications.Notify", // Method
|
||||
0, // Flags
|
||||
"natstonotify", // App name
|
||||
notification.ID, // Notification ID
|
||||
"", // Icon
|
||||
notification.Summary, // Summary
|
||||
notification.Body, // Body
|
||||
[]string{}, // Actions
|
||||
map[string]dbus.Variant{}, // Hints
|
||||
int32(notification.Timeout.Milliseconds()), // Timeout
|
||||
)
|
||||
if call.Err != nil {
|
||||
return ret, call.Err
|
||||
}
|
||||
|
||||
call.Store(&ret)
|
||||
|
||||
return ret, nil
|
||||
}
|
27
flake.lock
generated
Normal file
27
flake.lock
generated
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1739020877,
|
||||
"narHash": "sha256-mIvECo/NNdJJ/bXjNqIh8yeoSjVLAuDuTUzAo7dzs8Y=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a79cfe0ebd24952b580b1cf08cd906354996d547",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
67
flake.nix
Normal file
67
flake.nix
Normal file
@ -0,0 +1,67 @@
|
||||
{
|
||||
description = "NATS to notification service";
|
||||
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs }:
|
||||
let
|
||||
allSystems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
forAllSystems =
|
||||
f:
|
||||
nixpkgs.lib.genAttrs allSystems (
|
||||
system:
|
||||
f {
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
overlays.default = final: prev: {
|
||||
natstonotify = self.packages.${prev.system}.default;
|
||||
};
|
||||
|
||||
packages = forAllSystems (
|
||||
{ pkgs }:
|
||||
{
|
||||
default =
|
||||
let
|
||||
src = pkgs.lib.sourceFilesBySuffices ./. [
|
||||
"go.mod"
|
||||
"go.sum"
|
||||
".go"
|
||||
];
|
||||
version = pkgs.lib.strings.removePrefix "v" (
|
||||
builtins.elemAt (pkgs.lib.strings.split "\"" (
|
||||
pkgs.lib.lists.findFirst (x: pkgs.lib.strings.hasInfix "Version" x) null (
|
||||
pkgs.lib.strings.splitString "\n" (builtins.readFile ./main.go)
|
||||
)
|
||||
)) 2
|
||||
);
|
||||
in
|
||||
pkgs.buildGoModule {
|
||||
version = version;
|
||||
pname = "natstonotify";
|
||||
src = src;
|
||||
vendorHash = pkgs.lib.fakeHash;
|
||||
};
|
||||
}
|
||||
);
|
||||
devShells = forAllSystems (
|
||||
{ pkgs }:
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
go
|
||||
golangci-lint
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
15
go.mod
Normal file
15
go.mod
Normal file
@ -0,0 +1,15 @@
|
||||
module git.t-juice.club/torjus/natstonotify
|
||||
|
||||
go 1.23.4
|
||||
|
||||
require (
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/nats-io/nats.go v1.39.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.9 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
18
go.sum
Normal file
18
go.sum
Normal file
@ -0,0 +1,18 @@
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/nats-io/nats.go v1.39.0 h1:2/yg2JQjiYYKLwDuBzV0FbB2sIV+eFNkEevlRi4n9lI=
|
||||
github.com/nats-io/nats.go v1.39.0/go.mod h1:MgRb8oOdigA6cYpEPhXJuRVH6UE/V4jblJ2jQ27IXYM=
|
||||
github.com/nats-io/nkeys v0.4.9 h1:qe9Faq2Gxwi6RZnZMXfmGMZkg3afLLOtrU+gDZJ35b0=
|
||||
github.com/nats-io/nkeys v0.4.9/go.mod h1:jcMqs+FLG+W5YO36OX6wFIFcmpdAns+w1Wm6D3I/evE=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
178
main.go
Normal file
178
main.go
Normal file
@ -0,0 +1,178 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"git.t-juice.club/torjus/natstonotify/bus"
|
||||
"git.t-juice.club/torjus/natstonotify/server"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nkeys"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const Version = "v0.1.0"
|
||||
|
||||
func connectNats() (*nats.Conn, error) {
|
||||
natsURL, ok := os.LookupEnv("NATS_URL")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("NATS_URL not set")
|
||||
}
|
||||
nkey, ok := os.LookupEnv("NATS_NKEY")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("NATS_NKEY not set")
|
||||
}
|
||||
|
||||
kp, err := nkeys.FromSeed([]byte(nkey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pub, err := kp.PublicKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opt := nats.Nkey(pub, kp.Sign)
|
||||
|
||||
nc, err := nats.Connect(natsURL, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Setup logging
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
}))
|
||||
|
||||
slog.SetDefault(logger)
|
||||
|
||||
cmd := &cli.Command{
|
||||
EnableShellCompletion: true,
|
||||
Usage: "NATS-powered notification service",
|
||||
Commands: []*cli.Command{
|
||||
{
|
||||
Name: "server",
|
||||
Usage: "Start the server",
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
b, err := bus.NewNotifyBus()
|
||||
if err != nil {
|
||||
fmt.Println("Error creating notification bus: ", err)
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
|
||||
info, err := b.ServerInfo()
|
||||
if err != nil {
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
|
||||
logger.Info("Connected to notification bus", "bus_name", info.Name, "bus_version", info.Version)
|
||||
|
||||
defer b.Close()
|
||||
|
||||
nc, err := connectNats()
|
||||
if err != nil {
|
||||
logger.Error("Error connecting to NATS", "error", err)
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
srv, err := server.New(nc, b)
|
||||
if err != nil {
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
srv.Close()
|
||||
}()
|
||||
|
||||
slog.Info("Server started", "version", Version)
|
||||
if err := srv.Start(); err != nil {
|
||||
slog.Info("Server exited with error", "error", err)
|
||||
return cli.Exit(err, 2)
|
||||
}
|
||||
|
||||
slog.Info("Server stopped")
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "notify",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "Send a notification",
|
||||
ArgsUsage: "SUMMARY [BODY]",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "local",
|
||||
Usage: "Send a local notification",
|
||||
},
|
||||
},
|
||||
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||
if cmd.NArg() < 1 {
|
||||
return cli.Exit("notify requires exactly one argument", 1)
|
||||
}
|
||||
summary := cmd.Args().Get(0)
|
||||
body := cmd.Args().Get(1)
|
||||
|
||||
bn := bus.BusNotification{
|
||||
Summary: summary,
|
||||
Body: body,
|
||||
}
|
||||
|
||||
local := cmd.Bool("local")
|
||||
if local {
|
||||
b, err := bus.NewNotifyBus()
|
||||
if err != nil {
|
||||
fmt.Println("Error creating notification bus: ", err)
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
_, err = b.Notify(bn)
|
||||
if err != nil {
|
||||
fmt.Println("Sending message: ", err)
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
} else {
|
||||
nc, err := connectNats()
|
||||
if err != nil {
|
||||
fmt.Printf("Error connecting to NATS: %s\n", err)
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
defer nc.Drain()
|
||||
|
||||
data, err := json.Marshal(bn)
|
||||
if err != nil {
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
|
||||
if err := nc.PublishMsg(&nats.Msg{
|
||||
Subject: server.DefaultSubject,
|
||||
Data: data,
|
||||
}); err != nil {
|
||||
fmt.Printf("Error sending message: %s\n", err)
|
||||
}
|
||||
nc.Flush()
|
||||
fmt.Printf("Published message to %s: %s", server.DefaultSubject, data)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
if err := cmd.Run(ctx, os.Args); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
86
server/server.go
Normal file
86
server/server.go
Normal file
@ -0,0 +1,86 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"git.t-juice.club/torjus/natstonotify/bus"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
|
||||
const DefaultSubject = "home2rjusnet.notifications"
|
||||
|
||||
type Server struct {
|
||||
conn *nats.Conn
|
||||
bus *bus.NotifyBus
|
||||
Subject string
|
||||
}
|
||||
|
||||
func New(nc *nats.Conn, b *bus.NotifyBus) (*Server, error) {
|
||||
return &Server{conn: nc, Subject: DefaultSubject, bus: b}, nil
|
||||
}
|
||||
|
||||
func (s *Server) Close() {
|
||||
s.conn.Drain()
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
js, err := jetstream.New(s.conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
stream, err := js.CreateOrUpdateStream(timeoutCtx, jetstream.StreamConfig{
|
||||
Name: "notifications",
|
||||
Subjects: []string{s.Subject},
|
||||
MaxBytes: 1024 * 1024 * 5,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cons, err := stream.CreateOrUpdateConsumer(timeoutCtx, jetstream.ConsumerConfig{
|
||||
Name: "notifications-consumer",
|
||||
Durable: "notifications-consumer",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
consCtx, err := cons.Consume(func(msg jetstream.Msg) {
|
||||
slog.Info("Got message")
|
||||
|
||||
bn := bus.BusNotification{}
|
||||
|
||||
if err := json.Unmarshal(msg.Data(), &bn); err != nil {
|
||||
slog.Warn("Error unmarshalling message", "error", err)
|
||||
msg.TermWithReason("failed to decode json")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := s.bus.Notify(bn)
|
||||
if err != nil {
|
||||
slog.Warn("Error sending notification", "error", err)
|
||||
msg.NakWithDelay(1 * time.Minute)
|
||||
return
|
||||
}
|
||||
slog.Info("Sent notification", "id", id)
|
||||
|
||||
if err := msg.DoubleAck(context.Background()); err != nil {
|
||||
slog.Error("Error acking message", "error", err)
|
||||
}
|
||||
slog.Debug("Acked message")
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-consCtx.Closed()
|
||||
|
||||
return nil
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user