Skip to content

Commit 034b65b

Browse files
committed
feat: feedback with notifciations
1 parent 3b98c43 commit 034b65b

6 files changed

Lines changed: 173 additions & 78 deletions

File tree

config.sample.yaml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,17 @@ SERVER_URLS:
9494
# DEVELOPER_MODE
9595
# This is used to enable some extra logging useful during development.
9696
# DO NOT ENABLE THIS IN A PUBLIC INSTANCE!!!
97-
DEVELOPER_MODE: false
97+
DEVELOPER_MODE: false
98+
99+
100+
101+
####################################
102+
# Push Notifications #
103+
####################################
104+
105+
# This is the skyglow notification server that this will share notifications thru
106+
NOTIFICATION_TRUSTED_SERVER: ''
107+
108+
# This is used for getting removed push tokens from people who uninstalled the app.
109+
# Hex, max value is 256 bytes
110+
NOTIFICATION_FEEDBACK_SECRET: ''

config/config.go

Lines changed: 80 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -7,84 +7,98 @@
77
package config
88

99
import (
10-
"github.com/spf13/viper"
10+
"encoding/hex"
1111
"fmt"
12+
13+
"github.com/spf13/viper"
1214
)
1315

1416
type Config struct {
15-
// Version of the server
16-
Version string `mapstructure:"VERSION"`
17+
// Version of the server
18+
Version string `mapstructure:"VERSION"`
1719
// Accessible server address
1820
CdnURL string `mapstructure:"CDN_URL"`
1921
// The port to run the server on
2022
ServerPort int `mapstructure:"SERVER_PORT"`
2123
// This enables extra logging, INCLUDING SENSETIVE INFORMATION like BLUESKY TOKENS. Useful for debugging with tools like insomnia. DO NOT USE ON PUBLIC SERVERS
2224
// Also requires all passwords start with "dev_" to work
2325
DeveloperMode bool `mapstructure:"DEVELOPER_MODE"`
24-
// Collects analytics on users.
25-
TrackAnalytics bool `mapstructure:"TRACK_ANALYTICS"`
26-
// Database type (mysql, postgres, sqlite)
27-
DatabaseType string `mapstructure:"DATABASE_TYPE"`
28-
// Database path
29-
DatabasePath string `mapstructure:"DATABASE_PATH"`
30-
31-
UseXForwardedFor bool `mapstructure:"USE_X_FORWARDED_FOR"`
32-
33-
ImgDisplayText string `mapstructure:"IMG_DISPLAY_TEXT"`
34-
ImgURLText string `mapstructure:"IMG_URL_TEXT"`
35-
VidDisplayText string `mapstructure:"VID_DISPLAY_TEXT"`
36-
VidURLText string `mapstructure:"VID_URL_TEXT"`
37-
38-
// Secret key used for JWT. Must be at least 32 bytes long. Keep this secret!
39-
SecretKey string `mapstructure:"SECRET_KEY"`
40-
// The security key but in bytes.
41-
SecretKeyBytes []byte
42-
// Min Version token version the server will accept
43-
MinTokenVersion int `mapstructure:"MIN_TOKEN_VERSION"`
44-
// Server Identifier, used for knowing what server a token belongs to.
45-
ServerIdentifier string `mapstructure:"SERVER_IDENTIFIER"`
46-
// Server URLs used for contacting the server
47-
ServerURLs []string `mapstructure:"SERVER_URLS"`
26+
// Collects analytics on users.
27+
TrackAnalytics bool `mapstructure:"TRACK_ANALYTICS"`
28+
// Database type (mysql, postgres, sqlite)
29+
DatabaseType string `mapstructure:"DATABASE_TYPE"`
30+
// Database path
31+
DatabasePath string `mapstructure:"DATABASE_PATH"`
32+
33+
UseXForwardedFor bool `mapstructure:"USE_X_FORWARDED_FOR"`
34+
35+
ImgDisplayText string `mapstructure:"IMG_DISPLAY_TEXT"`
36+
ImgURLText string `mapstructure:"IMG_URL_TEXT"`
37+
VidDisplayText string `mapstructure:"VID_DISPLAY_TEXT"`
38+
VidURLText string `mapstructure:"VID_URL_TEXT"`
39+
40+
// Secret key used for JWT. Must be at least 32 bytes long. Keep this secret!
41+
SecretKey string `mapstructure:"SECRET_KEY"`
42+
// The security key but in bytes.
43+
SecretKeyBytes []byte
44+
// Min Version token version the server will accept
45+
MinTokenVersion int `mapstructure:"MIN_TOKEN_VERSION"`
46+
// Server Identifier, used for knowing what server a token belongs to.
47+
ServerIdentifier string `mapstructure:"SERVER_IDENTIFIER"`
48+
// Server URLs used for contacting the server
49+
ServerURLs []string `mapstructure:"SERVER_URLS"`
50+
51+
//notifications
52+
NotificationTrustedServer string `mapstructure:"NOTIFICATION_TRUSTED_SERVER"`
53+
NotificationFeedbackSecretString string `mapstructure:"NOTIFICATION_FEEDBACK_SECRET"`
54+
NotificationFeedbackSecret []byte
4855
}
4956

5057
// Loads our config files.
5158
func LoadConfig() (*Config, error) {
52-
viper.SetConfigName("config") // Name of the config file (without extension)
53-
viper.SetConfigType("yaml") // File type
54-
viper.AddConfigPath(".") // Look for config in the current directory
55-
viper.AddConfigPath("/config/") // Path for Docker setups
56-
57-
// Read environment variables with a specific prefix
58-
viper.SetEnvPrefix("TWITTER_BRIDGE")
59-
60-
// Set default values
61-
viper.SetDefault("VERSION", "1.0.6") // wait till i forget to update this
62-
viper.SetDefault("SERVER_PORT", "3000")
63-
viper.SetDefault("DEVELOPER_MODE", false)
64-
viper.SetDefault("DATABASE_TYPE", "sqlite")
65-
viper.SetDefault("DATABASE_PATH", "./db/twitterbridge.db")
66-
viper.SetDefault("TRACK_ANALYTICS", true)
67-
viper.SetDefault("CDN_URL", "http://127.0.0.1:3000")
68-
viper.SetDefault("USE_X_FORWARDED_FOR", false)
69-
viper.SetDefault("IMG_DISPLAY_TEXT", "pic.twitter.com/{shortblob}")
70-
viper.SetDefault("VID_DISPLAY_TEXT", "pic.twitter.com/{shortblob}")
71-
viper.SetDefault("IMG_URL_TEXT", "http://127.0.0.1:3000/img/{shortblob}")
72-
viper.SetDefault("VID_URL_TEXT", "http://127.0.0.1:3000/img/{shortblob}")
73-
viper.SetDefault("SECRET_KEY", "")
74-
viper.SetDefault("MIN_TOKEN_VERSION", 1)
75-
76-
// Read config file
77-
if err := viper.ReadInConfig(); err != nil {
78-
fmt.Println("No config file found, relying on environment variables")
79-
}
80-
81-
// Bind config to struct
82-
var config Config
83-
if err := viper.Unmarshal(&config); err != nil {
84-
return nil, err
85-
}
86-
87-
config.SecretKeyBytes = []byte(config.SecretKey)
88-
89-
return &config, nil
90-
}
59+
viper.SetConfigName("config") // Name of the config file (without extension)
60+
viper.SetConfigType("yaml") // File type
61+
viper.AddConfigPath(".") // Look for config in the current directory
62+
viper.AddConfigPath("/config/") // Path for Docker setups
63+
64+
// Read environment variables with a specific prefix
65+
viper.SetEnvPrefix("TWITTER_BRIDGE")
66+
67+
// Set default values
68+
viper.SetDefault("VERSION", "1.0.6") // wait till i forget to update this
69+
viper.SetDefault("SERVER_PORT", "3000")
70+
viper.SetDefault("DEVELOPER_MODE", false)
71+
viper.SetDefault("DATABASE_TYPE", "sqlite")
72+
viper.SetDefault("DATABASE_PATH", "./db/twitterbridge.db")
73+
viper.SetDefault("TRACK_ANALYTICS", true)
74+
viper.SetDefault("CDN_URL", "http://127.0.0.1:3000")
75+
viper.SetDefault("USE_X_FORWARDED_FOR", false)
76+
viper.SetDefault("IMG_DISPLAY_TEXT", "pic.twitter.com/{shortblob}")
77+
viper.SetDefault("VID_DISPLAY_TEXT", "pic.twitter.com/{shortblob}")
78+
viper.SetDefault("IMG_URL_TEXT", "http://127.0.0.1:3000/img/{shortblob}")
79+
viper.SetDefault("VID_URL_TEXT", "http://127.0.0.1:3000/img/{shortblob}")
80+
viper.SetDefault("SECRET_KEY", "")
81+
viper.SetDefault("MIN_TOKEN_VERSION", 1)
82+
viper.SetDefault("NOTIFICATION_TRUSTED_SERVER", "")
83+
viper.SetDefault("NOTIFICATION_SECRET_KEY", "")
84+
85+
// Read config file
86+
if err := viper.ReadInConfig(); err != nil {
87+
fmt.Println("No config file found, relying on environment variables")
88+
}
89+
90+
// Bind config to struct
91+
var config Config
92+
if err := viper.Unmarshal(&config); err != nil {
93+
return nil, err
94+
}
95+
96+
config.SecretKeyBytes = []byte(config.SecretKey)
97+
config.NotificationFeedbackSecret = make([]byte, hex.DecodedLen(len(config.NotificationFeedbackSecretString)))
98+
_, err := hex.Decode(config.NotificationFeedbackSecret, []byte(config.NotificationFeedbackSecretString))
99+
if err != nil {
100+
panic("you failed to set the notification feedback secret to a correct value. set it to be correct or not at all")
101+
}
102+
103+
return &config, nil
104+
}

db_controller/db_controller.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package db_controller
22

33
import (
4+
"errors"
45
"fmt"
56
"os"
67
"path/filepath"
78
"time"
89

910
"strconv"
1011

12+
skyglownotificationlib "github.com/Preloading/SkyglowNotificationLibraries"
1113
"github.com/Preloading/TwitterAPIBridge/config"
1214
authcrypt "github.com/Preloading/TwitterAPIBridge/cryption"
1315
"github.com/google/uuid"
@@ -489,14 +491,26 @@ func GetAllActivePushNotifications() ([]NotificationTokens, error) {
489491
}
490492

491493
func CreateModifyRegisteredPushNotifications(t NotificationTokens) error {
492-
result := db.Clauses(clause.OnConflict{
493-
Columns: []clause.Column{
494-
{Name: "user_did"},
495-
},
496-
UpdateAll: true,
497-
}).Create(&t)
494+
// Check if a record already exists for this user_did
495+
var existing NotificationTokens
496+
if err := db.First(&existing, "user_did = ?", t.UserDID).Error; err != nil {
497+
if errors.Is(err, gorm.ErrRecordNotFound) {
498+
// It's a create
499+
if err := db.Create(&t).Error; err != nil {
500+
return err
501+
}
502+
503+
skyglownotificationlib.ConfigureTokenForFeedback(t.DeviceToken, cfg.NotificationFeedbackSecret)
504+
}
505+
// some other DB error
506+
return err
507+
}
498508

499-
return result.Error
509+
// It's an update: copy fields from t into existing (use Updates to only update non-zero fields)
510+
if err := db.Model(&existing).Updates(t).Error; err != nil {
511+
return err
512+
}
513+
return nil
500514
}
501515

502516
func GetPushTokensForDID(did string) ([]NotificationTokens, error) {
@@ -533,3 +547,12 @@ func DeleteeeeeeeeeeeeRegistrationForPushNotificationsWithDid(did string) error
533547
return nil
534548

535549
}
550+
551+
// this should not be did but i am lazy
552+
func DeleteeeeeeeeeeeeRegistrationForPushNotificationsWithRoutingInfo(routing_key []byte, routing_server_address string) error {
553+
if err := db.Delete(&NotificationTokens{}, "routing_key = ? AND server_address = ?", routing_key, routing_server_address).Error; err != nil {
554+
return err
555+
}
556+
return nil
557+
558+
}

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@ func main() {
3636
}
3737

3838
db_controller.InitDB(*configData)
39-
go notifications.RunNotifications()
39+
go notifications.RunNotifications(*configData)
4040
twitterv1.InitServer(configData)
4141
}

notifications/notification_manager.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88
"time"
99

1010
sgn "github.com/Preloading/SkyglowNotificationLibraries"
11+
skyglownotificationlib "github.com/Preloading/SkyglowNotificationLibraries"
1112
blueskyapi "github.com/Preloading/TwitterAPIBridge/bluesky"
13+
"github.com/Preloading/TwitterAPIBridge/config"
1214
"github.com/Preloading/TwitterAPIBridge/db_controller"
1315
"github.com/Preloading/TwitterAPIBridge/twitterv1"
1416
"golang.org/x/net/websocket"
@@ -49,11 +51,15 @@ var (
4951
favouritesFollowingOnlyDIDs []string
5052
newFollowers []string
5153
// posterDIDs []string
52-
lastUpdatedNotificationTime time.Time
54+
lastUpdatedNotificationTime time.Time
55+
lastCheckedForPushNotificationFeedback time.Time
5356
)
5457

55-
func RunNotifications() {
56-
sgn.ConfigureSession("d.preloading.dev") // todo make this configureable
58+
func RunNotifications(cfg config.Config) {
59+
if cfg.NotificationTrustedServer == "" {
60+
return
61+
}
62+
sgn.ConfigureSession(cfg.NotificationTrustedServer) // todo make this configureable
5763

5864
incomingMessages := make(chan JetstreamPostOutput)
5965

@@ -118,6 +124,33 @@ func RunNotifications() {
118124
}
119125
}()
120126

127+
go func() {
128+
for {
129+
if cfg.NotificationFeedbackSecretString == "" {
130+
return
131+
}
132+
133+
feedback, err := skyglownotificationlib.GetFeedback(cfg.NotificationFeedbackSecret, lastCheckedForPushNotificationFeedback)
134+
if err != nil {
135+
continue
136+
}
137+
138+
lastCheckedForPushNotificationFeedback = time.Now()
139+
140+
for _, f := range feedback {
141+
switch f.Reason {
142+
case "token-removed":
143+
{
144+
db_controller.DeleteeeeeeeeeeeeRegistrationForPushNotificationsWithRoutingInfo(f.RoutingToken, f.RoutingTokenServer)
145+
146+
}
147+
}
148+
}
149+
150+
time.Sleep(2 * time.Hour)
151+
}
152+
}()
153+
121154
go func() {
122155
for {
123156
ws, err := websocket.Dial("wss://jetstream1.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post&wantedCollections=app.bsky.feed.like&wantedCollections=app.bsky.feed.repost&wantedCollections=app.bsky.graph.follow", "", "https://jetstream1.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post")

twitterv1/account.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ func DevicePushDestinations(c *fiber.Ctx) error {
3535
return MissingAuth(c, err)
3636
}
3737

38+
if configData.NotificationTrustedServer == "" {
39+
return ReturnError(c, "push notifications are disabled on this server", 1000, 404)
40+
}
41+
3842
notificationTokens, err := db_controller.GetPushTokensForDID(*my_did)
3943
if err != nil {
4044
return EncodeAndSend(c, bridge.PushDestination{
@@ -76,6 +80,10 @@ func UpdatePushNotifications(c *fiber.Ctx) error {
7680
return MissingAuth(c, err)
7781
}
7882

83+
if configData.NotificationTrustedServer == "" {
84+
return ReturnError(c, "push notifications are disabled on this server", 1000, 404)
85+
}
86+
7987
enabledFor, err := strconv.Atoi(c.FormValue("enabled_for"))
8088
if err != nil {
8189
return ReturnError(c, "enabled_for is missing", 0, 400)
@@ -136,6 +144,10 @@ func RemovePush(c *fiber.Ctx) error {
136144
return MissingAuth(c, err)
137145
}
138146

147+
if configData.NotificationTrustedServer == "" {
148+
return ReturnError(c, "push notifications are disabled on this server", 1000, 404)
149+
}
150+
139151
err = db_controller.DeleteeeeeeeeeeeeRegistrationForPushNotificationsWithDid(*my_did)
140152

141153
if err != nil {

0 commit comments

Comments
 (0)