Skip to content

Commit

Permalink
feat: add a wireframe for push notifications
Browse files Browse the repository at this point in the history
Signed-off-by: Ales Verbic <[email protected]>
  • Loading branch information
verbotenj committed Sep 28, 2023
1 parent 99e9717 commit d9f5bc3
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 6 deletions.
99 changes: 99 additions & 0 deletions fcm/message.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package fcm

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"

"github.com/blinklabs-io/snek/internal/logging"
)

type Message struct {
MessageContent `json:"message"`
}

type MessageContent struct {
Token string `json:"token"`
Notification *NotificationContent `json:"notification,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
}

type NotificationContent struct {
Title string `json:"title"`
Body string `json:"body"`
}

type MessageOption func(*MessageContent)

func WithData(data map[string]interface{}) MessageOption {
return func(m *MessageContent) {
m.Data = data
}
}

func WithNotification(title string, body string) MessageOption {
return func(m *MessageContent) {
m.Notification = &NotificationContent{
Title: title,
Body: body,
}
}
}

func NewMessage(token string, opts ...MessageOption) *Message {
if token == "" {
logging.GetLogger().Fatalf("Token is mandatory for FCM message")
}

msg := &Message{
MessageContent: MessageContent{
Token: token,
},
}
for _, opt := range opts {
opt(&msg.MessageContent)
}
return msg
}

func Send(accessToken string, projectId string, msg *Message) error {

fcmEndpoint := fmt.Sprintf("https://fcm.googleapis.com/v1/projects/%s/messages:send", projectId)

// Convert the message to JSON
payload, err := json.Marshal(msg)
if err != nil {
return err
}

fmt.Println(string(payload))

// Create a new HTTP request
req, err := http.NewRequest("POST", fcmEndpoint, bytes.NewBuffer(payload))
if err != nil {
return err
}

// Set headers
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Content-Type", "application/json")

// Execute the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

// Check for errors in the response
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return errors.New(string(body))
}

return nil
}
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,28 @@ require (
github.com/gen2brain/beeep v0.0.0-20230602101333-f384c29b62dd
github.com/kelseyhightower/envconfig v1.4.0
go.uber.org/zap v1.26.0
golang.org/x/oauth2 v0.11.0
gopkg.in/yaml.v2 v2.4.0
)

// XXX: uncomment when testing local changes to gouroboros
// replace github.com/blinklabs-io/gouroboros v0.52.0 => ../gouroboros

require (
cloud.google.com/go/compute v1.20.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/x448/float16 v0.8.4 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.12.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
27 changes: 27 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg=
cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
github.com/blinklabs-io/gouroboros v0.53.0 h1:JRq7FJ2HP5Fv+RBREzHAskT74u3Es8Sra9ynW08sumo=
github.com/blinklabs-io/gouroboros v0.53.0/go.mod h1:2wCCNNsHNYMT4gQB+bXS0Y99Oeu8+EM96hi7hW22C2w=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand All @@ -9,6 +13,12 @@ github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/Oq
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
Expand All @@ -26,11 +36,28 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
Expand Down
12 changes: 12 additions & 0 deletions output/push/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@
package push

type PushOptionFunc func(*PushOutput)

func WithServiceAccountFilePath(serviceAccountFilePath string) PushOptionFunc {
return func(o *PushOutput) {
o.serviceAccountFilePath = serviceAccountFilePath
}
}

func WithAccessTokenUrl(url string) PushOptionFunc {
return func(o *PushOutput) {
o.accessTokenUrl = url
}
}
27 changes: 25 additions & 2 deletions output/push/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,42 @@ import (
"github.com/blinklabs-io/snek/plugin"
)

var cmdlineOptions struct {
serviceAccountFilePath string
accessTokenUrl string
}

func init() {
plugin.Register(
plugin.PluginEntry{
Type: plugin.PluginTypeOutput,
Name: "push",
Description: "Send push notifications for events",
NewFromOptionsFunc: NewFromCmdlineOptions,
Options: []plugin.PluginOption{}, // Define any options if needed
Options: []plugin.PluginOption{ // Define any options if needed
{
Name: "serviceAccountFilePath",
Type: plugin.PluginOptionTypeString,
Description: "specifies the path to the service account file",
DefaultValue: "",
Dest: &(cmdlineOptions.serviceAccountFilePath),
},
{
Name: "accessTokenUrl",
Type: plugin.PluginOptionTypeString,
Description: "specifies the url to get access token from",
DefaultValue: "https://www.googleapis.com/auth/firebase.messaging",
Dest: &(cmdlineOptions.accessTokenUrl),
},
},
},
)
}

func NewFromCmdlineOptions() plugin.Plugin {
p := New()
p := New(
WithAccessTokenUrl(cmdlineOptions.accessTokenUrl),
WithServiceAccountFilePath(cmdlineOptions.serviceAccountFilePath),
)
return p
}
98 changes: 94 additions & 4 deletions output/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,26 @@
package push

import (
"context"
"encoding/json"
"fmt"
"os"

"github.com/blinklabs-io/snek/event"
"github.com/blinklabs-io/snek/fcm"
"github.com/blinklabs-io/snek/input/chainsync"
"github.com/blinklabs-io/snek/internal/logging"
"golang.org/x/oauth2/google"
)

type PushOutput struct {
errorChan chan error
eventChan chan event.Event
errorChan chan error
eventChan chan event.Event
accessToken string
accessTokenUrl string
projectID string
serviceAccountFilePath string
fcmTokens []string
}

type Notification struct {
Expand All @@ -44,17 +55,31 @@ func New(options ...PushOptionFunc) *PushOutput {
for _, option := range options {
option(p)
}
p.GetProjectId()

Check failure on line 58 in output/push/push.go

View workflow job for this annotation

GitHub Actions / lint (1.20.x, ubuntu-latest)

Error return value of `p.GetProjectId` is not checked (errcheck)
return p
}

func (p *PushOutput) Start() error {
logger := logging.GetLogger()
logger.Infof("starting push notification server")
var msg *fcm.Message
go func() {
for {
evt, ok := <-p.eventChan
// Channel has been closed, which means we're shutting down
if !ok {
return
}
// Get access token per each event
err := p.GetAccessToken()
if err != nil {
return
}

// TODO define where tokens will be fetched from
p.GetFcmTokens()

Check failure on line 80 in output/push/push.go

View workflow job for this annotation

GitHub Actions / lint (1.20.x, ubuntu-latest)

Error return value of `p.GetFcmTokens` is not checked (errcheck)
fcmToken := p.fcmTokens[0]

switch evt.Type {
case "chainsync.block":
payload := evt.Payload
Expand All @@ -69,6 +94,7 @@ func (p *PushOutput) Start() error {
be.SlotNumber,
be.BlockHash,
)

case "chainsync.rollback":
payload := evt.Payload
if payload == nil {
Expand All @@ -88,14 +114,28 @@ func (p *PushOutput) Start() error {
}

te := payload.(chainsync.TransactionEvent)
fmt.Println("Snek")
fmt.Printf("New Transaction!\nBlockNumber: %d, SlotNumber: %d\nInputs: %d, Outputs: %d\nHash: %s",

// Create notification message
title := "Snek"
body := fmt.Sprintf("New Transaction!\nBlockNumber: %d, SlotNumber: %d\nInputs: %d, Outputs: %d\nHash: %s",
te.BlockNumber,
te.SlotNumber,
len(te.Inputs),
len(te.Outputs),
te.TransactionHash,
)
msg = fcm.NewMessage(
fcmToken,
fcm.WithNotification(title, body),
)

// Send notification
err = fcm.Send(p.accessToken, p.projectID, msg)
if err != nil {
logger.Fatalf("Failed to send message: %v", err)
}
fmt.Println("Message sent successfully!")

default:
fmt.Println("Snek")
fmt.Printf("New Event!\nEvent: %v", evt)
Expand All @@ -105,6 +145,56 @@ func (p *PushOutput) Start() error {
return nil
}

func (p *PushOutput) GetAccessToken() error {
data, err := os.ReadFile(p.serviceAccountFilePath)
if err != nil {
logging.GetLogger().Fatalf("Failed to read the credential file: %v", err)
return err
}

conf, err := google.JWTConfigFromJSON(data, p.accessTokenUrl)
if err != nil {
logging.GetLogger().Fatalf("Failed to parse the credential file: %v", err)
return err
}

token, err := conf.TokenSource(context.Background()).Token()
if err != nil {
logging.GetLogger().Fatalf("Failed to get token: %v", err)
return err
}

fmt.Println(token.AccessToken)
p.accessToken = token.AccessToken
return nil
}

// Get FCM tokens from DB
func (p *PushOutput) GetFcmTokens() error {
// TODO define where tokens will be fetched from
p.fcmTokens = append(p.fcmTokens, "")
return nil
}

// Get project ID from file
func (p *PushOutput) GetProjectId() error {
data, err := os.ReadFile(p.serviceAccountFilePath)
if err != nil {
logging.GetLogger().Fatalf("Failed to read the credential file: %v", err)
return err
}

// Get project ID from file
var v map[string]any
if err := json.Unmarshal(data, &v); err != nil {
logging.GetLogger().Fatalf("Failed to parse the credential file: %v", err)
return err
}
p.projectID = v["project_id"].(string)

return nil
}

// Stop the embedded output
func (p *PushOutput) Stop() error {
close(p.eventChan)
Expand Down

0 comments on commit d9f5bc3

Please sign in to comment.