diff --git a/server/README.md b/server/README.md index 4058f08..d61360b 100644 --- a/server/README.md +++ b/server/README.md @@ -4,10 +4,11 @@ - background job for re-delivery - - accept/decline subscribtions (PATCH subs) + - accept/decline subscriptions (PATCH subs) - (message.go) api routes - (compat.go) api routes - https://firebase.google.com/docs/cloud-messaging/send-message#rest - - List subscribtions on owned channels /RESTful?) + - List subscriptions on owned channels /RESTful?) - deploy - - Dockerfile \ No newline at end of file + - Dockerfile + - php in html \ No newline at end of file diff --git a/server/api/apierr/enums.go b/server/api/apierr/enums.go index ecf1d71..12abcbf 100644 --- a/server/api/apierr/enums.go +++ b/server/api/apierr/enums.go @@ -26,6 +26,7 @@ const ( CLIENT_NOT_FOUND APIError = 1302 CHANNEL_NOT_FOUND APIError = 1303 SUBSCRIPTION_NOT_FOUND APIError = 1304 + MESSAGE_NOT_FOUND APIError = 1305 USER_AUTH_FAILED APIError = 1311 NO_DEVICE_LINKED APIError = 1401 diff --git a/server/api/handler/api.go b/server/api/handler/api.go index 5259664..dcc0dec 100644 --- a/server/api/handler/api.go +++ b/server/api/handler/api.go @@ -10,7 +10,6 @@ import ( "github.com/gin-gonic/gin" "gogs.mikescher.com/BlackForestBytes/goext/langext" "net/http" - "regexp" ) 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 { 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 { - username := langext.Ptr(regexp.MustCompile(`[[:alnum:]\-_]`).ReplaceAllString(*b.Username, "")) + username := langext.Ptr(h.app.NormalizeUsername(*b.Username)) if *username == "" { 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) } 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 { @@ -672,7 +676,7 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse { } defer ctx.Cancel() - if permResp := ctx.CheckPermissionUserRead(u.UserID); permResp != nil { + if permResp := ctx.CheckPermissionUserAdmin(u.UserID); permResp != nil { 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) } 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) } @@ -696,24 +700,251 @@ func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse { 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 { - 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 { - 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 { + //also update last_read 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 { - 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 { - 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 { diff --git a/server/api/handler/message.go b/server/api/handler/message.go index 627f928..e91fe18 100644 --- a/server/api/handler/message.go +++ b/server/api/handler/message.go @@ -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") } + 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 { clients, err := h.database.ListClients(ctx, sub.SubscriberUserID) if err != nil { return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query clients") } + if !sub.Confirmed { + continue + } + for _, client := range clients { fcmDelivID, err := h.deliverMessage(ctx, client, msg) @@ -220,8 +234,8 @@ func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse { ErrorHighlight: -1, Message: "Message sent", SuppressSend: false, - MessageCount: user.MessagesSent, - Quota: user.QuotaUsedToday(), + MessageCount: user.MessagesSent + 1, + Quota: user.QuotaUsedToday() + 1, IsPro: user.IsPro, QuotaMax: user.QuotaPerDay(), SCNMessageID: msg.SCNMessageID, diff --git a/server/db/channels.go b/server/db/channels.go new file mode 100644 index 0000000..1b3c430 --- /dev/null +++ b/server/db/channels.go @@ -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 +} diff --git a/server/db/clients.go b/server/db/clients.go new file mode 100644 index 0000000..3762d96 --- /dev/null +++ b/server/db/clients.go @@ -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 +} diff --git a/server/db/deliveries.go b/server/db/deliveries.go new file mode 100644 index 0000000..24373e5 --- /dev/null +++ b/server/db/deliveries.go @@ -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 +} diff --git a/server/db/messages.go b/server/db/messages.go new file mode 100644 index 0000000..ce8326e --- /dev/null +++ b/server/db/messages.go @@ -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 +} diff --git a/server/db/methods.go b/server/db/methods.go deleted file mode 100644 index 45fb15c..0000000 --- a/server/db/methods.go +++ /dev/null @@ -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 -} diff --git a/server/db/schema_3.ddl b/server/db/schema_3.ddl index 59b1a2c..e371e19 100644 --- a/server/db/schema_3.ddl +++ b/server/db/schema_3.ddl @@ -52,7 +52,6 @@ CREATE TABLE channels send_key TEXT NOT NULL, timestamp_created INTEGER NOT NULL, - timestamp_lastread INTEGER NULL DEFAULT NULL, timestamp_lastsent INTEGER NULL DEFAULT NULL, messages_sent INTEGER NOT NULL DEFAULT '0' diff --git a/server/db/subscriptions.go b/server/db/subscriptions.go new file mode 100644 index 0000000..4fe4f1c --- /dev/null +++ b/server/db/subscriptions.go @@ -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 +} diff --git a/server/db/users.go b/server/db/users.go new file mode 100644 index 0000000..b2d0ee7 --- /dev/null +++ b/server/db/users.go @@ -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 +} diff --git a/server/firebase/firebase.go b/server/firebase/firebase.go index db527ac..2964e66 100644 --- a/server/firebase/firebase.go +++ b/server/firebase/firebase.go @@ -56,11 +56,11 @@ func (fb App) SendNotification(ctx context.Context, client models.Client, msg mo "priority": strconv.Itoa(msg.Priority), "trimmed": langext.Conditional(msg.NeedsTrim(), "true", "false"), "title": msg.Title, - "body": msg.TrimmedBody(), + "body": langext.Coalesce(msg.TrimmedContent(), ""), }, Notification: &messaging.Notification{ Title: msg.Title, - Body: msg.ShortBody(), + Body: msg.ShortContent(), }, Android: nil, APNS: nil, @@ -70,7 +70,7 @@ func (fb App) SendNotification(ctx context.Context, client models.Client, msg mo Topic: "", Condition: "", } - + if client.Type == models.ClientTypeIOS { n.APNS = nil } else if client.Type == models.ClientTypeAndroid { diff --git a/server/logic/application.go b/server/logic/application.go index ad3593f..a54e8a0 100644 --- a/server/logic/application.go +++ b/server/logic/application.go @@ -16,6 +16,7 @@ import ( "net/http" "os" "os/signal" + "regexp" "strings" "syscall" "time" @@ -152,32 +153,20 @@ func (app *Application) getPermissions(ctx *AppContext, hdr string) (PermissionS } 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 { - 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 { - return PermissionSet{ReferenceID: 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 PermissionSet{UserID: langext.Ptr(user.UserID), KeyType: PermKeyTypeUserAdmin}, nil } return NewEmptyPermissions(), nil } 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) if err != nil { @@ -196,10 +185,29 @@ func (app *Application) GetOrCreateChannel(ctx *AppContext, userid int64, chanNa 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 { return models.Channel{}, err } 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 +} diff --git a/server/logic/permissions.go b/server/logic/permissions.go index 6094937..eb0bf22 100644 --- a/server/logic/permissions.go +++ b/server/logic/permissions.go @@ -3,29 +3,28 @@ package logic import ( "blackforestbytes.com/simplecloudnotifier/api/apierr" "blackforestbytes.com/simplecloudnotifier/common/ginresp" + "blackforestbytes.com/simplecloudnotifier/models" "gogs.mikescher.com/BlackForestBytes/goext/langext" ) type PermKeyType string const ( - PermKeyTypeNone PermKeyType = "NONE" // (nothing) - PermKeyTypeUserSend PermKeyType = "USER_SEND" // send-messages - PermKeyTypeUserRead PermKeyType = "USER_READ" // send-messages, list-messages, read-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 + PermKeyTypeNone PermKeyType = "NONE" // (nothing) + PermKeyTypeUserSend PermKeyType = "USER_SEND" // send-messages + PermKeyTypeUserRead PermKeyType = "USER_READ" // send-messages, list-messages, read-user + PermKeyTypeUserAdmin PermKeyType = "USER_ADMIN" // send-messages, list-messages, read-user, delete-messages, update-user ) type PermissionSet struct { - ReferenceID *int64 - KeyType PermKeyType + UserID *int64 + KeyType PermKeyType } func NewEmptyPermissions() PermissionSet { return PermissionSet{ - ReferenceID: nil, - KeyType: PermKeyTypeNone, + UserID: nil, + KeyType: PermKeyTypeNone, } } @@ -33,10 +32,10 @@ var respoNotAuthorized = ginresp.InternAPIError(401, apierr.USER_AUTH_FAILED, "Y func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPResponse { 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 } - if p.ReferenceID != nil && *p.ReferenceID == userid && p.KeyType == PermKeyTypeUserAdmin { + if p.UserID != nil && *p.UserID == userid && p.KeyType == PermKeyTypeUserAdmin { return nil } @@ -45,9 +44,53 @@ func (ac *AppContext) CheckPermissionUserRead(userid int64) *ginresp.HTTPRespons func (ac *AppContext) CheckPermissionUserAdmin(userid int64) *ginresp.HTTPResponse { 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 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 +} diff --git a/server/models/channel.go b/server/models/channel.go index ce47f5b..13f063e 100644 --- a/server/models/channel.go +++ b/server/models/channel.go @@ -14,7 +14,6 @@ type Channel struct { SubscribeKey string SendKey string TimestampCreated time.Time - TimestampLastRead *time.Time TimestampLastSent *time.Time MessagesSent int } @@ -27,7 +26,6 @@ func (c Channel) JSON() ChannelJSON { SubscribeKey: c.SubscribeKey, SendKey: c.SendKey, TimestampCreated: c.TimestampCreated.Format(time.RFC3339Nano), - TimestampLastRead: timeOptFmt(c.TimestampLastRead, time.RFC3339Nano), TimestampLastSent: timeOptFmt(c.TimestampLastSent, time.RFC3339Nano), MessagesSent: c.MessagesSent, } @@ -40,7 +38,6 @@ type ChannelJSON struct { SubscribeKey string `json:"subscribe_key"` SendKey string `json:"send_key"` TimestampCreated string `json:"timestamp_created"` - TimestampLastRead *string `json:"timestamp_last_read"` TimestampLastSent *string `json:"timestamp_last_sent"` MessagesSent int `json:"messages_sent"` } @@ -65,7 +62,6 @@ func (c ChannelDB) Model() Channel { SubscribeKey: c.SubscribeKey, SendKey: c.SendKey, TimestampCreated: time.UnixMilli(c.TimestampCreated), - TimestampLastRead: timeOptFromMilli(c.TimestampLastRead), TimestampLastSent: timeOptFromMilli(c.TimestampLastSent), MessagesSent: c.MessagesSent, } diff --git a/server/models/message.go b/server/models/message.go index b33d5f6..263a0fa 100644 --- a/server/models/message.go +++ b/server/models/message.go @@ -7,6 +7,11 @@ import ( "time" ) +const ( + ContentLengthTrim = 1900 + ContentLengthShort = 200 +) + type Message struct { SCNMessageID int64 SenderUserID int64 @@ -21,7 +26,7 @@ type Message struct { UserMessageID *string } -func (m Message) JSON() MessageJSON { +func (m Message) FullJSON() MessageJSON { return MessageJSON{ SCNMessageID: m.SCNMessageID, SenderUserID: m.SenderUserID, @@ -33,6 +38,23 @@ func (m Message) JSON() MessageJSON { Content: m.Content, Priority: m.Priority, 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 { - return m.Content != nil && len(*m.Content) > 1900 + return m.Content != nil && len(*m.Content) > ContentLengthTrim } -func (m Message) TrimmedBody() string { - if !m.NeedsTrim() { - return langext.Coalesce(m.Content, "") +func (m Message) TrimmedContent() *string { + if m.Content == nil { + 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 { return "" } - if len(*m.Content) < 200 { + if len(*m.Content) < ContentLengthShort { return *m.Content } - return (*m.Content)[0:200-3] + "..." + return (*m.Content)[0:ContentLengthShort-3] + "..." } type MessageJSON struct { @@ -72,6 +97,7 @@ type MessageJSON struct { Content *string `json:"body"` Priority int `json:"priority"` UserMessageID *string `json:"usr_message_id"` + Trimmed bool `json:"trimmed"` } type MessageDB struct { diff --git a/server/models/user.go b/server/models/user.go index 9015b37..e68eba6 100644 --- a/server/models/user.go +++ b/server/models/user.go @@ -1,10 +1,10 @@ package models import ( + scn "blackforestbytes.com/simplecloudnotifier" "database/sql" "github.com/blockloop/scan" "gogs.mikescher.com/BlackForestBytes/goext/langext" - "gogs.mikescher.com/BlackForestBytes/goext/timeext" "time" ) @@ -58,7 +58,7 @@ func (u User) QuotaPerDay() 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 { return u.QuotaUsed } else { diff --git a/server/util.go b/server/util.go new file mode 100644 index 0000000..885d186 --- /dev/null +++ b/server/util.go @@ -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") +}