diff --git a/go.mod b/go.mod index 0ee026a6b..52bbed487 100644 --- a/go.mod +++ b/go.mod @@ -80,11 +80,13 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-mastodon v0.0.11 // indirect github.com/oapi-codegen/runtime v1.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/tomnomnom/linkheader v0.0.0-20250811210735-e5fe3b51442e // indirect go.mau.fi/util v0.9.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect diff --git a/go.sum b/go.sum index 535dae8d5..ec6f9302d 100644 --- a/go.sum +++ b/go.sum @@ -245,6 +245,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-mastodon v0.0.11 h1:Zcvc/8EHpf3os1mwAuUUB5es5VnfVdAeb4ed6ByJnCY= +github.com/mattn/go-mastodon v0.0.11/go.mod h1:0DcwYEkqigrvknMvjmfKXLP0vYyeYm+vBdUOvoHcczg= github.com/mileusna/viber v1.0.1 h1:gWB6/lKoWYVxkH0Jb8jRnGIRZ/9DEM7RBZRJHRfdYWs= github.com/mileusna/viber v1.0.1/go.mod h1:Pxu/iPMnYjnHgu+bEp3SiKWHWmlf/kDp/yOX8XUdYrQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -339,6 +341,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tomnomnom/linkheader v0.0.0-20250811210735-e5fe3b51442e h1:tD38/4xg4nuQCASJ/JxcvCHNb46w0cdAaJfkzQOO1bA= +github.com/tomnomnom/linkheader v0.0.0-20250811210735-e5fe3b51442e/go.mod h1:krvJ5AY/MjdPkTeRgMYbIDhbbbVvnPQPzsIsDJO8xrY= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4= diff --git a/service/mastodon/doc.go b/service/mastodon/doc.go new file mode 100644 index 000000000..476f36335 --- /dev/null +++ b/service/mastodon/doc.go @@ -0,0 +1,34 @@ +/* +Package mastodon provides message notification integration for Mastodon. + +Usage: + + package main + + import ( + "context" + "log" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/mastodon" + ) + + func main() { + mastodonSvc := mastodon.New( + "https://mastodon.social", + "your-client-id", + "your-client-secret", + "your-access-token", + ) + + mastodonSvc.AddReceivers("@user@mastodon.social") + + notifier := notify.New() + notifier.UseServices(mastodonSvc) + + if err := notifier.Send(context.Background(), "Subject", "Message"); err != nil { + log.Fatal(err) + } + } +*/ +package mastodon diff --git a/service/mastodon/mastodon.go b/service/mastodon/mastodon.go new file mode 100644 index 000000000..55d19d37e --- /dev/null +++ b/service/mastodon/mastodon.go @@ -0,0 +1,69 @@ +package mastodon + +import ( + "context" + "fmt" + + "github.com/mattn/go-mastodon" +) + +//go:generate mockery --name=mastodonClient --output=. --case=underscore --inpackage +type mastodonClient interface { + PostStatus(ctx context.Context, toot *mastodon.Toot) (*mastodon.Status, error) +} + +// Compile-time check +var _ mastodonClient = new(mastodon.Client) + +// Mastodon struct holds necessary data to communicate with the Mastodon API. +type Mastodon struct { + client mastodonClient + recipients []string +} + +// New returns a new instance of a Mastodon notification service. +// serverURL is the Mastodon instance URL (e.g. "https://mastodon.social"). +// clientID, clientSecret, and accessToken are OAuth2 credentials. +func New(serverURL, clientID, clientSecret, accessToken string) *Mastodon { + client := mastodon.NewClient(&mastodon.Config{ + Server: serverURL, + ClientID: clientID, + ClientSecret: clientSecret, + AccessToken: accessToken, + }) + + return &Mastodon{ + client: client, + recipients: []string{}, + } +} + +// AddReceivers takes Mastodon usernames (e.g. "@user@instance.social") and adds them to the +// internal recipient list. The Send method will send a direct message to all recipients. +func (m *Mastodon) AddReceivers(usernames ...string) { + m.recipients = append(m.recipients, usernames...) +} + +// Send takes a message subject and a message body and sends them as a direct message +// to all previously set recipients. Each recipient is mentioned in a separate direct toot. +func (m Mastodon) Send(ctx context.Context, subject, message string) error { + fullMessage := subject + "\n" + message + + for _, recipient := range m.recipients { + select { + case <-ctx.Done(): + return ctx.Err() + default: + toot := &mastodon.Toot{ + Status: recipient + " " + fullMessage, + Visibility: "direct", + } + _, err := m.client.PostStatus(ctx, toot) + if err != nil { + return fmt.Errorf("send message to Mastodon recipient %q: %w", recipient, err) + } + } + } + + return nil +} diff --git a/service/mastodon/usage.md b/service/mastodon/usage.md new file mode 100644 index 000000000..3f553d605 --- /dev/null +++ b/service/mastodon/usage.md @@ -0,0 +1,37 @@ +# Mastodon Usage + +Ensure you have a Mastodon account and have created an application +to obtain your client ID, client secret, and access token. + +## Sample Code + +```go +package main + +import ( + "context" + "log" + + "github.com/nikoksr/notify" + "github.com/nikoksr/notify/service/mastodon" +) + +func main() { + mastodonSvc := mastodon.New( + "https://mastodon.social", // Your instance URL + "your-client-id", + "your-client-secret", + "your-access-token", + ) + + mastodonSvc.AddReceivers("@user@mastodon.social") + + notifier := notify.New() + notifier.UseServices(mastodonSvc) + + err := notifier.Send(context.Background(), "Subject", "Message body") + if err != nil { + log.Fatal(err) + } +} +```