CreateSubscription(), UpdateSubscription(), GetMessage(), DeleteMessage()

This commit is contained in:
Mike Schwörer 2022-11-19 23:16:54 +01:00
parent 8278c059ad
commit 0d641b727f
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
19 changed files with 1155 additions and 669 deletions

View File

@ -4,10 +4,11 @@
- background job for re-delivery - background job for re-delivery
- accept/decline subscribtions (PATCH subs) - accept/decline subscriptions (PATCH subs)
- (message.go) api routes - (message.go) api routes
- (compat.go) api routes - (compat.go) api routes
- https://firebase.google.com/docs/cloud-messaging/send-message#rest - https://firebase.google.com/docs/cloud-messaging/send-message#rest
- List subscribtions on owned channels /RESTful?) - List subscriptions on owned channels /RESTful?)
- deploy - deploy
- Dockerfile - Dockerfile
- php in html

View File

@ -26,6 +26,7 @@ const (
CLIENT_NOT_FOUND APIError = 1302 CLIENT_NOT_FOUND APIError = 1302
CHANNEL_NOT_FOUND APIError = 1303 CHANNEL_NOT_FOUND APIError = 1303
SUBSCRIPTION_NOT_FOUND APIError = 1304 SUBSCRIPTION_NOT_FOUND APIError = 1304
MESSAGE_NOT_FOUND APIError = 1305
USER_AUTH_FAILED APIError = 1311 USER_AUTH_FAILED APIError = 1311
NO_DEVICE_LINKED APIError = 1401 NO_DEVICE_LINKED APIError = 1401

View File

@ -10,7 +10,6 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"net/http" "net/http"
"regexp"
) )
type APIHandler struct { type APIHandler struct {
@ -90,7 +89,12 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
} }
} }
userobj, err := h.database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, b.Username) username := b.Username
if username != nil {
username = langext.Ptr(h.app.NormalizeUsername(*username))
}
userobj, err := h.database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, username)
if err != nil { if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err) return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
} }
@ -181,7 +185,7 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
} }
if b.Username != nil { if b.Username != nil {
username := langext.Ptr(regexp.MustCompile(`[[:alnum:]\-_]`).ReplaceAllString(*b.Username, "")) username := langext.Ptr(h.app.NormalizeUsername(*b.Username))
if *username == "" { if *username == "" {
username = nil username = nil
} }
@ -634,7 +638,7 @@ func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
return ginresp.InternAPIError(404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) return ginresp.InternAPIError(404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
} }
if err != nil { if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
} }
if subscription.SubscriberUserID != u.UserID { if subscription.SubscriberUserID != u.UserID {
@ -672,7 +676,7 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
} }
defer ctx.Cancel() defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
return *permResp return *permResp
} }
@ -681,10 +685,10 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
return ginresp.InternAPIError(404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err) return ginresp.InternAPIError(404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
} }
if err != nil { if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err) return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
} }
if subscription.SubscriberUserID != u.UserID { if subscription.SubscriberUserID != u.UserID && subscription.ChannelOwnerUserID != u.UserID {
return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil) return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
} }
@ -696,24 +700,251 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON())) return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON()))
} }
// CreateSubscription swaggerdoc
//
// @Summary Creare/Request a subscription
// @ID api-subscriptions-create
//
// @Param uid path int true "UserID"
// @Param query_data query handler.CreateSubscription.query false " "
// @Param post_data body handler.CreateSubscription.body false " "
//
// @Success 200 {object} models.SubscriptionJSON
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError
// @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
//
// @Router /api-v2/users/{uid}/subscriptions [POST]
func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
return ginresp.NotImplemented() //TODO type uri struct {
UserID int64 `uri:"uid"`
}
type body struct {
ChannelOwnerUserID int64 `form:"channel_owner_user_id"`
Channel string `form:"channel_name"`
}
type query struct {
ChanSubscribeKey *string `form:"chan_subscribe_key"`
}
var u uri
var q query
var b body
ctx, errResp := h.app.StartRequest(g, &u, &q, &b)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
return *permResp
}
channel, err := h.database.GetChannelByName(ctx, b.ChannelOwnerUserID, h.app.NormalizeChannelName(b.Channel))
if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query channel", err)
}
if channel == nil {
return ginresp.InternAPIError(400, apierr.CHANNEL_NOT_FOUND, "Channel not found", err)
}
sub, err := h.database.CreateSubscription(ctx, u.UserID, *channel, channel.OwnerUserID == u.UserID)
if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create subscription", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, sub.JSON()))
} }
// UpdateSubscription swaggerdoc
//
// @Summary Update a subscription (e.g. confirm)
// @ID api-subscriptions-update
//
// @Param uid path int true "UserID"
// @Param sid path int true "SubscriptionID"
//
// @Success 200 {object} models.SubscriptionJSON
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError
// @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
//
// @Router /api-v2/users/{uid}/subscriptions/{sid} [PATCH]
func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) UpdateSubscription(g *gin.Context) ginresp.HTTPResponse {
return ginresp.NotImplemented() //TODO type uri struct {
UserID int64 `uri:"uid"`
SubscriptionID int64 `uri:"sid"`
}
type body struct {
Confirmed *bool `form:"confirmed"`
}
var u uri
var b body
ctx, errResp := h.app.StartRequest(g, &u, nil, &b)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil {
return *permResp
}
subscription, err := h.database.GetSubscription(ctx, u.SubscriptionID)
if err == sql.ErrNoRows {
return ginresp.InternAPIError(404, apierr.SUBSCRIPTION_NOT_FOUND, "Subscription not found", err)
}
if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if subscription.ChannelOwnerUserID != u.UserID {
return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
if b.Confirmed != nil {
err = h.database.UpdateSubscriptionConfirmed(ctx, u.SubscriptionID, *b.Confirmed)
if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update subscription", err)
}
}
subscription, err = h.database.GetSubscription(ctx, u.SubscriptionID)
if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, subscription.JSON()))
} }
func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
//also update last_read
return ginresp.NotImplemented() //TODO return ginresp.NotImplemented() //TODO
} }
// GetMessage swaggerdoc
//
// @Summary Get a single message (untrimmed)
// @Description The user must either own the message and request the resource with the READ or ADMIN Key
// @Description Or the user must subscribe to the corresponding channel (and be confirmed) and request the resource with the READ or ADMIN Key
// @Description The returned message is never trimmed
// @ID api-message-get
//
// @Param mid path int true "SCNMessageID"
//
// @Success 200 {object} models.MessageJSON
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError
// @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
//
// @Router /api-v2/messages/{mid} [PATCH]
func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
return ginresp.NotImplemented() //TODO type uri struct {
MessageID int64 `uri:"mid"`
}
var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
if permResp := ctx.CheckPermissionAny(); permResp != nil {
return *permResp
}
msg, err := h.database.GetMessage(ctx, u.MessageID)
if err == sql.ErrNoRows {
return ginresp.InternAPIError(404, apierr.MESSAGE_NOT_FOUND, "message not found", err)
}
if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query message", err)
}
if !ctx.CheckPermissionMessageReadDirect(msg) {
// either we have direct read permissions (it is our message + read/admin key)
// or we subscribe (+confirmed) to the channel and have read/admin key
if uid := ctx.GetPermissionUserID(); uid != nil && ctx.IsPermissionUserRead() {
sub, err := h.database.GetSubscriptionBySubscriber(ctx, *uid, msg.ChannelID)
if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query subscription", err)
}
if sub == nil {
// not subbed
return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
if !sub.Confirmed {
// sub not confirmed
return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
// => perm okay
} else {
// auth-key is not set or not a user:x variant
return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON()))
} }
// DeleteMessage swaggerdoc
//
// @Summary Delete a single message
// @Description The user must own the message and request the resource with the ADMIN Key
// @ID api-message-delete
//
// @Param mid path int true "SCNMessageID"
//
// @Success 200 {object} models.MessageJSON
// @Failure 400 {object} ginresp.apiError
// @Failure 401 {object} ginresp.apiError
// @Failure 404 {object} ginresp.apiError
// @Failure 500 {object} ginresp.apiError
//
// @Router /api-v2/messages/{mid} [PATCH]
func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
return ginresp.NotImplemented() //TODO type uri struct {
MessageID int64 `uri:"mid"`
}
var u uri
ctx, errResp := h.app.StartRequest(g, &u, nil, nil)
if errResp != nil {
return *errResp
}
defer ctx.Cancel()
if permResp := ctx.CheckPermissionAny(); permResp != nil {
return *permResp
}
msg, err := h.database.GetMessage(ctx, u.MessageID)
if err == sql.ErrNoRows {
return ginresp.InternAPIError(404, apierr.MESSAGE_NOT_FOUND, "message not found", err)
}
if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query message", err)
}
if !ctx.CheckPermissionMessageReadDirect(msg) {
return ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "You are not authorized for this action", nil)
}
err = h.database.DeleteMessage(ctx, msg.SCNMessageID)
if err != nil {
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to delete message", err)
}
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, msg.FullJSON()))
} }
func (h APIHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { func (h APIHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {

View File

@ -190,12 +190,26 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions")
} }
err = h.database.IncUserMessageCounter(ctx, user)
if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to inc user msg-counter")
}
err = h.database.IncChannelMessageCounter(ctx, channel)
if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to channel msg-counter")
}
for _, sub := range subscriptions { for _, sub := range subscriptions {
clients, err := h.database.ListClients(ctx, sub.SubscriberUserID) clients, err := h.database.ListClients(ctx, sub.SubscriberUserID)
if err != nil { if err != nil {
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query clients") return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query clients")
} }
if !sub.Confirmed {
continue
}
for _, client := range clients { for _, client := range clients {
fcmDelivID, err := h.deliverMessage(ctx, client, msg) fcmDelivID, err := h.deliverMessage(ctx, client, msg)
@ -220,8 +234,8 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
ErrorHighlight: -1, ErrorHighlight: -1,
Message: "Message sent", Message: "Message sent",
SuppressSend: false, SuppressSend: false,
MessageCount: user.MessagesSent, MessageCount: user.MessagesSent + 1,
Quota: user.QuotaUsedToday(), Quota: user.QuotaUsedToday() + 1,
IsPro: user.IsPro, IsPro: user.IsPro,
QuotaMax: user.QuotaPerDay(), QuotaMax: user.QuotaPerDay(),
SCNMessageID: msg.SCNMessageID, SCNMessageID: msg.SCNMessageID,

141
server/db/channels.go Normal file
View File

@ -0,0 +1,141 @@
package db
import (
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"time"
)
func (db *Database) GetChannelByKey(ctx TxContext, key string) (*models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE subscribe_key = ? OR send_key = ? LIMIT 1", key, key)
if err != nil {
return nil, err
}
channel, err := models.DecodeChannel(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &channel, nil
}
func (db *Database) GetChannelByName(ctx TxContext, userid int64, chanName string) (*models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE owner_user_id = ? OR name = ? LIMIT 1", userid, chanName)
if err != nil {
return nil, err
}
channel, err := models.DecodeChannel(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &channel, nil
}
func (db *Database) CreateChannel(ctx TxContext, userid int64, name string, subscribeKey string, sendKey string) (models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Channel{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO channels (owner_user_id, name, subscribe_key, send_key, timestamp_created) VALUES (?, ?, ?, ?, ?)",
userid,
name,
subscribeKey,
sendKey,
time2DB(now))
if err != nil {
return models.Channel{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Channel{}, err
}
return models.Channel{
ChannelID: liid,
OwnerUserID: userid,
Name: name,
SubscribeKey: subscribeKey,
SendKey: sendKey,
TimestampCreated: now,
TimestampLastSent: nil,
MessagesSent: 0,
}, nil
}
func (db *Database) ListChannels(ctx TxContext, userid int64) ([]models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE owner_user_id = ?", userid)
if err != nil {
return nil, err
}
data, err := models.DecodeChannels(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) GetChannel(ctx TxContext, userid int64, channelid int64) (models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Channel{}, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE owner_user_id = ? AND channel_id = ? LIMIT 1", userid, channelid)
if err != nil {
return models.Channel{}, err
}
client, err := models.DecodeChannel(rows)
if err != nil {
return models.Channel{}, err
}
return client, nil
}
func (db *Database) IncChannelMessageCounter(ctx TxContext, channel models.Channel) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE channels SET messages_sent = ? AND timestamp_lastsent = ? WHERE channel_id = ?",
channel.MessagesSent+1,
time2DB(time.Now()),
channel.ChannelID)
if err != nil {
return err
}
return nil
}

108
server/db/clients.go Normal file
View File

@ -0,0 +1,108 @@
package db
import (
"blackforestbytes.com/simplecloudnotifier/models"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"time"
)
func (db *Database) CreateClient(ctx TxContext, userid int64, ctype models.ClientType, fcmToken string, agentModel string, agentVersion string) (models.Client, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Client{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO clients (user_id, type, fcm_token, timestamp_created, agent_model, agent_version) VALUES (?, ?, ?, ?, ?, ?)",
userid,
string(ctype),
fcmToken,
time2DB(now),
agentModel,
agentVersion)
if err != nil {
return models.Client{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Client{}, err
}
return models.Client{
ClientID: liid,
UserID: userid,
Type: ctype,
FCMToken: langext.Ptr(fcmToken),
TimestampCreated: now,
AgentModel: agentModel,
AgentVersion: agentVersion,
}, nil
}
func (db *Database) ClearFCMTokens(ctx TxContext, fcmtoken string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "DELETE FROM clients WHERE fcm_token = ?", fcmtoken)
if err != nil {
return err
}
return nil
}
func (db *Database) ListClients(ctx TxContext, userid int64) ([]models.Client, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM clients WHERE user_id = ?", userid)
if err != nil {
return nil, err
}
data, err := models.DecodeClients(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) GetClient(ctx TxContext, userid int64, clientid int64) (models.Client, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Client{}, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM clients WHERE user_id = ? AND client_id = ? LIMIT 1", userid, clientid)
if err != nil {
return models.Client{}, err
}
client, err := models.DecodeClient(rows)
if err != nil {
return models.Client{}, err
}
return client, nil
}
func (db *Database) DeleteClient(ctx TxContext, clientid int64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "DELETE FROM clients WHERE client_id = ?", clientid)
if err != nil {
return err
}
return nil
}

88
server/db/deliveries.go Normal file
View File

@ -0,0 +1,88 @@
package db
import (
"blackforestbytes.com/simplecloudnotifier/models"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"time"
)
func (db *Database) CreateRetryDelivery(ctx TxContext, client models.Client, msg models.Message) (models.Delivery, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Delivery{}, err
}
now := time.Now().UTC()
next := now.Add(5 * time.Second)
res, err := tx.ExecContext(ctx, "INSERT INTO deliveries (scn_message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (?, ?, ?, ?, ?, ?, ?)",
msg.SCNMessageID,
client.UserID,
client.ClientID,
time2DB(now),
nil,
models.DeliveryStatusRetry,
nil,
time2DB(next))
if err != nil {
return models.Delivery{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Delivery{}, err
}
return models.Delivery{
DeliveryID: liid,
SCNMessageID: msg.SCNMessageID,
ReceiverUserID: client.UserID,
ReceiverClientID: client.ClientID,
TimestampCreated: now,
TimestampFinalized: nil,
Status: models.DeliveryStatusRetry,
RetryCount: 0,
NextDelivery: langext.Ptr(next),
FCMMessageID: nil,
}, nil
}
func (db *Database) CreateSuccessDelivery(ctx TxContext, client models.Client, msg models.Message, fcmDelivID string) (models.Delivery, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Delivery{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO deliveries (scn_message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (?, ?, ?, ?, ?, ?, ?)",
msg.SCNMessageID,
client.UserID,
client.ClientID,
time2DB(now),
time2DB(now),
models.DeliveryStatusSuccess,
fcmDelivID,
nil)
if err != nil {
return models.Delivery{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Delivery{}, err
}
return models.Delivery{
DeliveryID: liid,
SCNMessageID: msg.SCNMessageID,
ReceiverUserID: client.UserID,
ReceiverClientID: client.ClientID,
TimestampCreated: now,
TimestampFinalized: langext.Ptr(now),
Status: models.DeliveryStatusSuccess,
RetryCount: 0,
NextDelivery: nil,
FCMMessageID: langext.Ptr(fcmDelivID),
}, nil
}

105
server/db/messages.go Normal file
View File

@ -0,0 +1,105 @@
package db
import (
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"time"
)
func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*models.Message, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM messages WHERE usr_message_id = ? LIMIT 1", usrMsgId)
if err != nil {
return nil, err
}
msg, err := models.DecodeMessage(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &msg, nil
}
func (db *Database) GetMessage(ctx TxContext, scnMessageID int64) (models.Message, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Message{}, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM messages WHERE scn_message_id = ? LIMIT 1", scnMessageID)
if err != nil {
return models.Message{}, err
}
msg, err := models.DecodeMessage(rows)
if err != nil {
return models.Message{}, err
}
return msg, nil
}
func (db *Database) CreateMessage(ctx TxContext, senderUserID int64, channel models.Channel, timestampSend *time.Time, title string, content *string, priority int, userMsgId *string) (models.Message, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Message{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO messages (sender_user_id, owner_user_id, channel_name, channel_id, timestamp_real, timestamp_client, title, content, priority, usr_message_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
senderUserID,
channel.OwnerUserID,
channel.Name,
channel.ChannelID,
time2DB(now),
time2DBOpt(timestampSend),
title,
content,
priority,
userMsgId)
if err != nil {
return models.Message{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Message{}, err
}
return models.Message{
SCNMessageID: liid,
SenderUserID: senderUserID,
OwnerUserID: channel.OwnerUserID,
ChannelName: channel.Name,
ChannelID: channel.ChannelID,
TimestampReal: now,
TimestampClient: timestampSend,
Title: title,
Content: content,
Priority: priority,
UserMessageID: userMsgId,
}, nil
}
func (db *Database) DeleteMessage(ctx TxContext, scnMessageID int64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "DELETE FROM messages WHERE scn_message_id = ?", scnMessageID)
if err != nil {
return err
}
return nil
}

View File

@ -1,604 +0,0 @@
package db
import (
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"gogs.mikescher.com/BlackForestBytes/goext/langext"
"time"
)
func (db *Database) CreateUser(ctx TxContext, readKey string, sendKey string, adminKey string, protoken *string, username *string) (models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.User{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO users (username, read_key, send_key, admin_key, is_pro, pro_token, timestamp_created) VALUES (?, ?, ?, ?, ?, ?, ?)",
username,
readKey,
sendKey,
adminKey,
bool2DB(protoken != nil),
protoken,
time2DB(now))
if err != nil {
return models.User{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.User{}, err
}
return models.User{
UserID: liid,
Username: username,
ReadKey: readKey,
SendKey: sendKey,
AdminKey: adminKey,
TimestampCreated: now,
TimestampLastRead: nil,
TimestampLastSent: nil,
MessagesSent: 0,
QuotaUsed: 0,
QuotaUsedDay: nil,
IsPro: protoken != nil,
ProToken: protoken,
}, nil
}
func (db *Database) CreateClient(ctx TxContext, userid int64, ctype models.ClientType, fcmToken string, agentModel string, agentVersion string) (models.Client, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Client{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO clients (user_id, type, fcm_token, timestamp_created, agent_model, agent_version) VALUES (?, ?, ?, ?, ?, ?)",
userid,
string(ctype),
fcmToken,
time2DB(now),
agentModel,
agentVersion)
if err != nil {
return models.Client{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Client{}, err
}
return models.Client{
ClientID: liid,
UserID: userid,
Type: ctype,
FCMToken: langext.Ptr(fcmToken),
TimestampCreated: now,
AgentModel: agentModel,
AgentVersion: agentVersion,
}, nil
}
func (db *Database) ClearFCMTokens(ctx TxContext, fcmtoken string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "DELETE FROM clients WHERE fcm_token = ?", fcmtoken)
if err != nil {
return err
}
return nil
}
func (db *Database) ClearProTokens(ctx TxContext, protoken string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE users SET is_pro=0, pro_token=NULL WHERE pro_token = ?", protoken)
if err != nil {
return err
}
return nil
}
func (db *Database) GetUserByKey(ctx TxContext, key string) (*models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM users WHERE admin_key = ? OR send_key = ? OR read_key = ? LIMIT 1", key, key, key)
if err != nil {
return nil, err
}
user, err := models.DecodeUser(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &user, nil
}
func (db *Database) GetChannelByKey(ctx TxContext, key string) (*models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE subscribe_key = ? OR send_key = ? LIMIT 1", key, key)
if err != nil {
return nil, err
}
channel, err := models.DecodeChannel(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &channel, nil
}
func (db *Database) GetUser(ctx TxContext, userid int64) (models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.User{}, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM users WHERE user_id = ? LIMIT 1", userid)
if err != nil {
return models.User{}, err
}
user, err := models.DecodeUser(rows)
if err != nil {
return models.User{}, err
}
return user, nil
}
func (db *Database) UpdateUserUsername(ctx TxContext, userid int64, username *string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE users SET username = ? WHERE user_id = ?", username, userid)
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateUserProToken(ctx TxContext, userid int64, protoken *string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE users SET pro_token = ? AND is_pro = ? WHERE user_id = ?", protoken, bool2DB(protoken != nil), userid)
if err != nil {
return err
}
return nil
}
func (db *Database) ListClients(ctx TxContext, userid int64) ([]models.Client, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM clients WHERE user_id = ?", userid)
if err != nil {
return nil, err
}
data, err := models.DecodeClients(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) GetClient(ctx TxContext, userid int64, clientid int64) (models.Client, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Client{}, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM clients WHERE user_id = ? AND client_id = ? LIMIT 1", userid, clientid)
if err != nil {
return models.Client{}, err
}
client, err := models.DecodeClient(rows)
if err != nil {
return models.Client{}, err
}
return client, nil
}
func (db *Database) DeleteClient(ctx TxContext, clientid int64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "DELETE FROM clients WHERE client_id = ?", clientid)
if err != nil {
return err
}
return nil
}
func (db *Database) GetMessageByUserMessageID(ctx TxContext, usrMsgId string) (*models.Message, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM messages WHERE usr_message_id = ? LIMIT 1", usrMsgId)
if err != nil {
return nil, err
}
msg, err := models.DecodeMessage(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &msg, nil
}
func (db *Database) GetChannelByName(ctx TxContext, userid int64, chanName string) (*models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE owner_user_id = ? OR name = ? LIMIT 1", userid, chanName)
if err != nil {
return nil, err
}
channel, err := models.DecodeChannel(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &channel, nil
}
func (db *Database) CreateChannel(ctx TxContext, userid int64, name string, subscribeKey string, sendKey string) (models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Channel{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO channels (owner_user_id, name, subscribe_key, send_key, timestamp_created) VALUES (?, ?, ?, ?, ?)",
userid,
name,
subscribeKey,
sendKey,
time2DB(now))
if err != nil {
return models.Channel{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Channel{}, err
}
return models.Channel{
ChannelID: liid,
OwnerUserID: userid,
Name: name,
SubscribeKey: subscribeKey,
SendKey: sendKey,
TimestampCreated: now,
TimestampLastRead: nil,
TimestampLastSent: nil,
MessagesSent: 0,
}, nil
}
func (db *Database) CreateSubscribtion(ctx TxContext, subscriberUID int64, ownerUID int64, chanName string, chanID int64, confirmed bool) (models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Subscription{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO subscriptions (subscriber_user_id, channel_owner_user_id, channel_name, channel_id, timestamp_created, confirmed) VALUES (?, ?, ?, ?, ?, ?)",
subscriberUID,
ownerUID,
chanName,
chanID,
time2DB(now),
confirmed)
if err != nil {
return models.Subscription{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Subscription{}, err
}
return models.Subscription{
SubscriptionID: liid,
SubscriberUserID: subscriberUID,
ChannelOwnerUserID: ownerUID,
ChannelID: chanID,
ChannelName: chanName,
TimestampCreated: now,
Confirmed: confirmed,
}, nil
}
func (db *Database) CreateMessage(ctx TxContext, senderUserID int64, channel models.Channel, timestampSend *time.Time, title string, content *string, priority int, userMsgId *string) (models.Message, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Message{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO messages (sender_user_id, owner_user_id, channel_name, channel_id, timestamp_real, timestamp_client, title, content, priority, usr_message_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
senderUserID,
channel.OwnerUserID,
channel.Name,
channel.ChannelID,
time2DB(now),
time2DBOpt(timestampSend),
title,
content,
priority,
userMsgId)
if err != nil {
return models.Message{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Message{}, err
}
return models.Message{
SCNMessageID: liid,
SenderUserID: senderUserID,
OwnerUserID: channel.OwnerUserID,
ChannelName: channel.Name,
ChannelID: channel.ChannelID,
TimestampReal: now,
TimestampClient: timestampSend,
Title: title,
Content: content,
Priority: priority,
UserMessageID: userMsgId,
}, nil
}
func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID int64) ([]models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM subscriptions WHERE channel_id = ?", channelID)
if err != nil {
return nil, err
}
data, err := models.DecodeSubscriptions(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) ListSubscriptionsByOwner(ctx TxContext, ownerUserID int64) ([]models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM subscriptions WHERE channel_owner_user_id = ?", ownerUserID)
if err != nil {
return nil, err
}
data, err := models.DecodeSubscriptions(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) CreateRetryDelivery(ctx TxContext, client models.Client, msg models.Message) (models.Delivery, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Delivery{}, err
}
now := time.Now().UTC()
next := now.Add(5 * time.Second)
res, err := tx.ExecContext(ctx, "INSERT INTO deliveries (scn_message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (?, ?, ?, ?, ?, ?, ?)",
msg.SCNMessageID,
client.UserID,
client.ClientID,
time2DB(now),
nil,
models.DeliveryStatusRetry,
nil,
time2DB(next))
if err != nil {
return models.Delivery{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Delivery{}, err
}
return models.Delivery{
DeliveryID: liid,
SCNMessageID: msg.SCNMessageID,
ReceiverUserID: client.UserID,
ReceiverClientID: client.ClientID,
TimestampCreated: now,
TimestampFinalized: nil,
Status: models.DeliveryStatusRetry,
RetryCount: 0,
NextDelivery: langext.Ptr(next),
FCMMessageID: nil,
}, nil
}
func (db *Database) CreateSuccessDelivery(ctx TxContext, client models.Client, msg models.Message, fcmDelivID string) (models.Delivery, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Delivery{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO deliveries (scn_message_id, receiver_user_id, receiver_client_id, timestamp_created, timestamp_finalized, status, fcm_message_id, next_delivery) VALUES (?, ?, ?, ?, ?, ?, ?)",
msg.SCNMessageID,
client.UserID,
client.ClientID,
time2DB(now),
time2DB(now),
models.DeliveryStatusSuccess,
fcmDelivID,
nil)
if err != nil {
return models.Delivery{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Delivery{}, err
}
return models.Delivery{
DeliveryID: liid,
SCNMessageID: msg.SCNMessageID,
ReceiverUserID: client.UserID,
ReceiverClientID: client.ClientID,
TimestampCreated: now,
TimestampFinalized: langext.Ptr(now),
Status: models.DeliveryStatusSuccess,
RetryCount: 0,
NextDelivery: nil,
FCMMessageID: langext.Ptr(fcmDelivID),
}, nil
}
func (db *Database) ListChannels(ctx TxContext, userid int64) ([]models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE owner_user_id = ?", userid)
if err != nil {
return nil, err
}
data, err := models.DecodeChannels(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) GetChannel(ctx TxContext, userid int64, channelid int64) (models.Channel, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Channel{}, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM channels WHERE owner_user_id = ? AND channel_id = ? LIMIT 1", userid, channelid)
if err != nil {
return models.Channel{}, err
}
client, err := models.DecodeChannel(rows)
if err != nil {
return models.Channel{}, err
}
return client, nil
}
func (db *Database) GetSubscription(ctx TxContext, subid int64) (models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Subscription{}, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM subscriptions WHERE subscription_id = ? LIMIT 1", subid)
if err != nil {
return models.Subscription{}, err
}
sub, err := models.DecodeSubscription(rows)
if err != nil {
return models.Subscription{}, err
}
return sub, nil
}
func (db *Database) DeleteSubscription(ctx TxContext, subid int64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "DELETE FROM subscriptions WHERE subscription_id = ?", subid)
if err != nil {
return err
}
return nil
}

View File

@ -52,7 +52,6 @@ CREATE TABLE channels
send_key TEXT NOT NULL, send_key TEXT NOT NULL,
timestamp_created INTEGER NOT NULL, timestamp_created INTEGER NOT NULL,
timestamp_lastread INTEGER NULL DEFAULT NULL,
timestamp_lastsent INTEGER NULL DEFAULT NULL, timestamp_lastsent INTEGER NULL DEFAULT NULL,
messages_sent INTEGER NOT NULL DEFAULT '0' messages_sent INTEGER NOT NULL DEFAULT '0'

149
server/db/subscriptions.go Normal file
View File

@ -0,0 +1,149 @@
package db
import (
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"time"
)
func (db *Database) CreateSubscription(ctx TxContext, subscriberUID int64, channel models.Channel, confirmed bool) (models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Subscription{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO subscriptions (subscriber_user_id, channel_owner_user_id, channel_name, channel_id, timestamp_created, confirmed) VALUES (?, ?, ?, ?, ?, ?)",
subscriberUID,
channel.OwnerUserID,
channel.Name,
channel.ChannelID,
time2DB(now),
confirmed)
if err != nil {
return models.Subscription{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.Subscription{}, err
}
return models.Subscription{
SubscriptionID: liid,
SubscriberUserID: subscriberUID,
ChannelOwnerUserID: channel.OwnerUserID,
ChannelID: channel.ChannelID,
ChannelName: channel.Name,
TimestampCreated: now,
Confirmed: confirmed,
}, nil
}
func (db *Database) ListSubscriptionsByChannel(ctx TxContext, channelID int64) ([]models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM subscriptions WHERE channel_id = ?", channelID)
if err != nil {
return nil, err
}
data, err := models.DecodeSubscriptions(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) ListSubscriptionsByOwner(ctx TxContext, ownerUserID int64) ([]models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM subscriptions WHERE channel_owner_user_id = ?", ownerUserID)
if err != nil {
return nil, err
}
data, err := models.DecodeSubscriptions(rows)
if err != nil {
return nil, err
}
return data, nil
}
func (db *Database) GetSubscription(ctx TxContext, subid int64) (models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.Subscription{}, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM subscriptions WHERE subscription_id = ? LIMIT 1", subid)
if err != nil {
return models.Subscription{}, err
}
sub, err := models.DecodeSubscription(rows)
if err != nil {
return models.Subscription{}, err
}
return sub, nil
}
func (db *Database) GetSubscriptionBySubscriber(ctx TxContext, subscriberId int64, channelId int64) (*models.Subscription, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM subscriptions WHERE subscriber_user_id = ? AND channel_id = ? LIMIT 1", subscriberId, channelId)
if err != nil {
return nil, err
}
user, err := models.DecodeSubscription(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &user, nil
}
func (db *Database) DeleteSubscription(ctx TxContext, subid int64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "DELETE FROM subscriptions WHERE subscription_id = ?", subid)
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateSubscriptionConfirmed(ctx TxContext, subscriptionID int64, confirmed bool) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE subscriptions SET confirmed = ? WHERE subscription_id = ?", confirmed, subscriptionID)
if err != nil {
return err
}
return nil
}

170
server/db/users.go Normal file
View File

@ -0,0 +1,170 @@
package db
import (
scn "blackforestbytes.com/simplecloudnotifier"
"blackforestbytes.com/simplecloudnotifier/models"
"database/sql"
"time"
)
func (db *Database) CreateUser(ctx TxContext, readKey string, sendKey string, adminKey string, protoken *string, username *string) (models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.User{}, err
}
now := time.Now().UTC()
res, err := tx.ExecContext(ctx, "INSERT INTO users (username, read_key, send_key, admin_key, is_pro, pro_token, timestamp_created) VALUES (?, ?, ?, ?, ?, ?, ?)",
username,
readKey,
sendKey,
adminKey,
bool2DB(protoken != nil),
protoken,
time2DB(now))
if err != nil {
return models.User{}, err
}
liid, err := res.LastInsertId()
if err != nil {
return models.User{}, err
}
return models.User{
UserID: liid,
Username: username,
ReadKey: readKey,
SendKey: sendKey,
AdminKey: adminKey,
TimestampCreated: now,
TimestampLastRead: nil,
TimestampLastSent: nil,
MessagesSent: 0,
QuotaUsed: 0,
QuotaUsedDay: nil,
IsPro: protoken != nil,
ProToken: protoken,
}, nil
}
func (db *Database) ClearProTokens(ctx TxContext, protoken string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE users SET is_pro=0, pro_token=NULL WHERE pro_token = ?", protoken)
if err != nil {
return err
}
return nil
}
func (db *Database) GetUserByKey(ctx TxContext, key string) (*models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return nil, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM users WHERE admin_key = ? OR send_key = ? OR read_key = ? LIMIT 1", key, key, key)
if err != nil {
return nil, err
}
user, err := models.DecodeUser(rows)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &user, nil
}
func (db *Database) GetUser(ctx TxContext, userid int64) (models.User, error) {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return models.User{}, err
}
rows, err := tx.QueryContext(ctx, "SELECT * FROM users WHERE user_id = ? LIMIT 1", userid)
if err != nil {
return models.User{}, err
}
user, err := models.DecodeUser(rows)
if err != nil {
return models.User{}, err
}
return user, nil
}
func (db *Database) UpdateUserUsername(ctx TxContext, userid int64, username *string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE users SET username = ? WHERE user_id = ?", username, userid)
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateUserProToken(ctx TxContext, userid int64, protoken *string) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE users SET pro_token = ? AND is_pro = ? WHERE user_id = ?", protoken, bool2DB(protoken != nil), userid)
if err != nil {
return err
}
return nil
}
func (db *Database) IncUserMessageCounter(ctx TxContext, user models.User) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
quota := user.QuotaUsedToday() + 1
_, err = tx.ExecContext(ctx, "UPDATE users SET timestamp_lastsent = ? AND messages_sent = ? AND quota_used = ? AND quota_used_day = ? WHERE user_id = ?",
time2DB(time.Now()),
user.MessagesSent+1,
quota,
scn.QuotaDayString(),
user.UserID)
if err != nil {
return err
}
return nil
}
func (db *Database) UpdateUserLastRead(ctx TxContext, userid int64) error {
tx, err := ctx.GetOrCreateTransaction(db)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, "UPDATE users SET timestamp_lastread = ? WHERE user_id = ?",
time2DB(time.Now()),
userid)
if err != nil {
return err
}
return nil
}

View File

@ -56,11 +56,11 @@ func (fb App) SendNotification(ctx context.Context, client models.Client, msg mo
"priority": strconv.Itoa(msg.Priority), "priority": strconv.Itoa(msg.Priority),
"trimmed": langext.Conditional(msg.NeedsTrim(), "true", "false"), "trimmed": langext.Conditional(msg.NeedsTrim(), "true", "false"),
"title": msg.Title, "title": msg.Title,
"body": msg.TrimmedBody(), "body": langext.Coalesce(msg.TrimmedContent(), ""),
}, },
Notification: &messaging.Notification{ Notification: &messaging.Notification{
Title: msg.Title, Title: msg.Title,
Body: msg.ShortBody(), Body: msg.ShortContent(),
}, },
Android: nil, Android: nil,
APNS: nil, APNS: nil,

View File

@ -16,6 +16,7 @@ import (
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"regexp"
"strings" "strings"
"syscall" "syscall"
"time" "time"
@ -152,32 +153,20 @@ func (app *Application) getPermissions(ctx *AppContext, hdr string) (PermissionS
} }
if user != nil && user.SendKey == key { if user != nil && user.SendKey == key {
return PermissionSet{ReferenceID: langext.Ptr(user.UserID), KeyType: PermKeyTypeUserSend}, nil return PermissionSet{UserID: langext.Ptr(user.UserID), KeyType: PermKeyTypeUserSend}, nil
} }
if user != nil && user.ReadKey == key { if user != nil && user.ReadKey == key {
return PermissionSet{ReferenceID: langext.Ptr(user.UserID), KeyType: PermKeyTypeUserRead}, nil return PermissionSet{UserID: langext.Ptr(user.UserID), KeyType: PermKeyTypeUserRead}, nil
} }
if user != nil && user.AdminKey == key { if user != nil && user.AdminKey == key {
return PermissionSet{ReferenceID: langext.Ptr(user.UserID), KeyType: PermKeyTypeUserAdmin}, nil return PermissionSet{UserID: langext.Ptr(user.UserID), KeyType: PermKeyTypeUserAdmin}, nil
}
channel, err := app.Database.GetChannelByKey(ctx, key)
if err != nil {
return PermissionSet{}, err
}
if channel != nil && channel.SendKey == key {
return PermissionSet{ReferenceID: langext.Ptr(channel.ChannelID), KeyType: PermKeyTypeChannelSend}, nil
}
if channel != nil && channel.SubscribeKey == key {
return PermissionSet{ReferenceID: langext.Ptr(channel.ChannelID), KeyType: PermKeyTypeChannelSub}, nil
} }
return NewEmptyPermissions(), nil return NewEmptyPermissions(), nil
} }
func (app *Application) GetOrCreateChannel(ctx *AppContext, userid int64, chanName string) (models.Channel, error) { func (app *Application) GetOrCreateChannel(ctx *AppContext, userid int64, chanName string) (models.Channel, error) {
chanName = strings.ToLower(strings.TrimSpace(chanName)) chanName = app.NormalizeChannelName(chanName)
existingChan, err := app.Database.GetChannelByName(ctx, userid, chanName) existingChan, err := app.Database.GetChannelByName(ctx, userid, chanName)
if err != nil { if err != nil {
@ -196,10 +185,29 @@ func (app *Application) GetOrCreateChannel(ctx *AppContext, userid int64, chanNa
return models.Channel{}, err return models.Channel{}, err
} }
_, err = app.Database.CreateSubscribtion(ctx, userid, userid, newChan.Name, newChan.ChannelID, true) _, err = app.Database.CreateSubscription(ctx, userid, newChan, true)
if err != nil { if err != nil {
return models.Channel{}, err return models.Channel{}, err
} }
return newChan, nil return newChan, nil
} }
func (app *Application) NormalizeChannelName(v string) string {
rex := regexp.MustCompile("[^[:alnum:]\\-_]")
v = strings.TrimSpace(v)
v = strings.ToLower(v)
v = rex.ReplaceAllString(v, "")
return v
}
func (app *Application) NormalizeUsername(v string) string {
rex := regexp.MustCompile("[^[:alnum:]\\-_ ]")
v = strings.TrimSpace(v)
v = rex.ReplaceAllString(v, "")
return v
}

View File

@ -3,6 +3,7 @@ package logic
import ( import (
"blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/api/apierr"
"blackforestbytes.com/simplecloudnotifier/common/ginresp" "blackforestbytes.com/simplecloudnotifier/common/ginresp"
"blackforestbytes.com/simplecloudnotifier/models"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
) )
@ -13,18 +14,16 @@ const (
PermKeyTypeUserSend PermKeyType = "USER_SEND" // send-messages PermKeyTypeUserSend PermKeyType = "USER_SEND" // send-messages
PermKeyTypeUserRead PermKeyType = "USER_READ" // send-messages, list-messages, read-user PermKeyTypeUserRead PermKeyType = "USER_READ" // send-messages, list-messages, read-user
PermKeyTypeUserAdmin PermKeyType = "USER_ADMIN" // send-messages, list-messages, read-user, delete-messages, update-user PermKeyTypeUserAdmin PermKeyType = "USER_ADMIN" // send-messages, list-messages, read-user, delete-messages, update-user
PermKeyTypeChannelSub PermKeyType = "CHAN_SUBSCRIBE" // subscribe-channel
PermKeyTypeChannelSend PermKeyType = "CHAN_SEND" // send-messages
) )
type PermissionSet struct { type PermissionSet struct {
ReferenceID *int64 UserID *int64
KeyType PermKeyType KeyType PermKeyType
} }
func NewEmptyPermissions() PermissionSet { func NewEmptyPermissions() PermissionSet {
return PermissionSet{ return PermissionSet{
ReferenceID: nil, UserID: nil,
KeyType: PermKeyTypeNone, KeyType: PermKeyTypeNone,
} }
} }
@ -33,10 +32,10 @@ var respoNotAuthorized = ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "Y
func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPResponse { func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.ReferenceID != nil && *p.ReferenceID == userid && p.KeyType == PermKeyTypeUserRead { if p.UserID != nil && *p.UserID == userid && p.KeyType == PermKeyTypeUserRead {
return nil return nil
} }
if p.ReferenceID != nil && *p.ReferenceID == userid && p.KeyType == PermKeyTypeUserAdmin { if p.UserID != nil && *p.UserID == userid && p.KeyType == PermKeyTypeUserAdmin {
return nil return nil
} }
@ -45,9 +44,53 @@ func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPRespons
func (ac *AppContext) CheckPermissionUserAdmin(userid int64) *ginresp.HTTPResponse { func (ac *AppContext) CheckPermissionUserAdmin(userid int64) *ginresp.HTTPResponse {
p := ac.permissions p := ac.permissions
if p.ReferenceID != nil && *p.ReferenceID == userid && p.KeyType == PermKeyTypeUserAdmin { if p.UserID != nil && *p.UserID == userid && p.KeyType == PermKeyTypeUserAdmin {
return nil return nil
} }
return langext.Ptr(respoNotAuthorized) return langext.Ptr(respoNotAuthorized)
} }
func (ac *AppContext) CheckPermissionAny() *ginresp.HTTPResponse {
p := ac.permissions
if p.KeyType == PermKeyTypeNone {
return langext.Ptr(respoNotAuthorized)
}
return nil
}
func (ac *AppContext) CheckPermissionMessageReadDirect(msg models.Message) bool {
p := ac.permissions
if p.UserID != nil && msg.OwnerUserID == *p.UserID && p.KeyType == PermKeyTypeUserRead {
return true
}
if p.UserID != nil && msg.OwnerUserID == *p.UserID && p.KeyType == PermKeyTypeUserAdmin {
return true
}
return false
}
func (ac *AppContext) GetPermissionUserID() *int64 {
if ac.permissions.UserID == nil {
return nil
} else {
return langext.Ptr(*ac.permissions.UserID)
}
}
func (ac *AppContext) IsPermissionUserRead() bool {
p := ac.permissions
return p.KeyType == PermKeyTypeUserRead || p.KeyType == PermKeyTypeUserAdmin
}
func (ac *AppContext) IsPermissionUserSend() bool {
p := ac.permissions
return p.KeyType == PermKeyTypeUserSend || p.KeyType == PermKeyTypeUserAdmin
}
func (ac *AppContext) IsPermissionUserAdmin() bool {
p := ac.permissions
return p.KeyType == PermKeyTypeUserAdmin
}

View File

@ -14,7 +14,6 @@ type Channel struct {
SubscribeKey string SubscribeKey string
SendKey string SendKey string
TimestampCreated time.Time TimestampCreated time.Time
TimestampLastRead *time.Time
TimestampLastSent *time.Time TimestampLastSent *time.Time
MessagesSent int MessagesSent int
} }
@ -27,7 +26,6 @@ func (c Channel) JSON() ChannelJSON {
SubscribeKey: c.SubscribeKey, SubscribeKey: c.SubscribeKey,
SendKey: c.SendKey, SendKey: c.SendKey,
TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano), TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano),
TimestampLastRead: timeOptFmt(c.TimestampLastRead, time.RFC3339Nano),
TimestampLastSent: timeOptFmt(c.TimestampLastSent, time.RFC3339Nano), TimestampLastSent: timeOptFmt(c.TimestampLastSent, time.RFC3339Nano),
MessagesSent: c.MessagesSent, MessagesSent: c.MessagesSent,
} }
@ -40,7 +38,6 @@ type ChannelJSON struct {
SubscribeKey string `json:"subscribe_key"` SubscribeKey string `json:"subscribe_key"`
SendKey string `json:"send_key"` SendKey string `json:"send_key"`
TimestampCreated string `json:"timestamp_created"` TimestampCreated string `json:"timestamp_created"`
TimestampLastRead *string `json:"timestamp_last_read"`
TimestampLastSent *string `json:"timestamp_last_sent"` TimestampLastSent *string `json:"timestamp_last_sent"`
MessagesSent int `json:"messages_sent"` MessagesSent int `json:"messages_sent"`
} }
@ -65,7 +62,6 @@ func (c ChannelDB) Model() Channel {
SubscribeKey: c.SubscribeKey, SubscribeKey: c.SubscribeKey,
SendKey: c.SendKey, SendKey: c.SendKey,
TimestampCreated: time.UnixMilli(c.TimestampCreated), TimestampCreated: time.UnixMilli(c.TimestampCreated),
TimestampLastRead: timeOptFromMilli(c.TimestampLastRead),
TimestampLastSent: timeOptFromMilli(c.TimestampLastSent), TimestampLastSent: timeOptFromMilli(c.TimestampLastSent),
MessagesSent: c.MessagesSent, MessagesSent: c.MessagesSent,
} }

View File

@ -7,6 +7,11 @@ import (
"time" "time"
) )
const (
ContentLengthTrim = 1900
ContentLengthShort = 200
)
type Message struct { type Message struct {
SCNMessageID int64 SCNMessageID int64
SenderUserID int64 SenderUserID int64
@ -21,7 +26,7 @@ type Message struct {
UserMessageID *string UserMessageID *string
} }
func (m Message) JSON() MessageJSON { func (m Message) FullJSON() MessageJSON {
return MessageJSON{ return MessageJSON{
SCNMessageID: m.SCNMessageID, SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID, SenderUserID: m.SenderUserID,
@ -33,6 +38,23 @@ func (m Message) JSON() MessageJSON {
Content: m.Content, Content: m.Content,
Priority: m.Priority, Priority: m.Priority,
UserMessageID: m.UserMessageID, UserMessageID: m.UserMessageID,
Trimmed: false,
}
}
func (m Message) TrimmedJSON() MessageJSON {
return MessageJSON{
SCNMessageID: m.SCNMessageID,
SenderUserID: m.SenderUserID,
OwnerUserID: m.OwnerUserID,
ChannelName: m.ChannelName,
ChannelID: m.ChannelID,
Timestamp: m.Timestamp().Format(time.RFC3339Nano),
Title: m.Title,
Content: m.TrimmedContent(),
Priority: m.Priority,
UserMessageID: m.UserMessageID,
Trimmed: m.NeedsTrim(),
} }
} }
@ -41,24 +63,27 @@ func (m Message) Timestamp() time.Time {
} }
func (m Message) NeedsTrim() bool { func (m Message) NeedsTrim() bool {
return m.Content != nil && len(*m.Content) > 1900 return m.Content != nil && len(*m.Content) > ContentLengthTrim
} }
func (m Message) TrimmedBody() string { func (m Message) TrimmedContent() *string {
if !m.NeedsTrim() { if m.Content == nil {
return langext.Coalesce(m.Content, "") return nil
} }
return langext.Coalesce(m.Content, "")[0:1900-3] + "..." if !m.NeedsTrim() {
return m.Content
}
return langext.Ptr(langext.Coalesce(m.Content, "")[0:ContentLengthTrim-3] + "...")
} }
func (m Message) ShortBody() string { func (m Message) ShortContent() string {
if m.Content == nil { if m.Content == nil {
return "" return ""
} }
if len(*m.Content) < 200 { if len(*m.Content) < ContentLengthShort {
return *m.Content return *m.Content
} }
return (*m.Content)[0:200-3] + "..." return (*m.Content)[0:ContentLengthShort-3] + "..."
} }
type MessageJSON struct { type MessageJSON struct {
@ -72,6 +97,7 @@ type MessageJSON struct {
Content *string `json:"body"` Content *string `json:"body"`
Priority int `json:"priority"` Priority int `json:"priority"`
UserMessageID *string `json:"usr_message_id"` UserMessageID *string `json:"usr_message_id"`
Trimmed bool `json:"trimmed"`
} }
type MessageDB struct { type MessageDB struct {

View File

@ -1,10 +1,10 @@
package models package models
import ( import (
scn "blackforestbytes.com/simplecloudnotifier"
"database/sql" "database/sql"
"github.com/blockloop/scan" "github.com/blockloop/scan"
"gogs.mikescher.com/BlackForestBytes/goext/langext" "gogs.mikescher.com/BlackForestBytes/goext/langext"
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"time" "time"
) )
@ -58,7 +58,7 @@ func (u User) QuotaPerDay() int {
} }
func (u User) QuotaUsedToday() int { func (u User) QuotaUsedToday() int {
now := time.Now().In(timeext.TimezoneBerlin).Format("2006-01-02") now := scn.QuotaDayString()
if u.QuotaUsedDay != nil && *u.QuotaUsedDay == now { if u.QuotaUsedDay != nil && *u.QuotaUsedDay == now {
return u.QuotaUsed return u.QuotaUsed
} else { } else {

10
server/util.go Normal file
View File

@ -0,0 +1,10 @@
package server
import (
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
"time"
)
func QuotaDayString() string {
return time.Now().In(timeext.TimezoneBerlin).Format("2006-01-02")
}