SendMessage()
This commit is contained in:
parent
fb37f94c0a
commit
85bfe79115
@ -2,9 +2,10 @@ package handler
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
"blackforestbytes.com/simplecloudnotifier/api/models"
|
||||
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
@ -14,6 +15,14 @@ import (
|
||||
|
||||
type APIHandler struct {
|
||||
app *logic.Application
|
||||
database *db.Database
|
||||
}
|
||||
|
||||
func NewAPIHandler(app *logic.Application) APIHandler {
|
||||
return APIHandler{
|
||||
app: app,
|
||||
database: app.Database,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUser swaggerdoc
|
||||
@ -69,24 +78,24 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
|
||||
sendKey := h.app.GenerateRandomAuthKey()
|
||||
adminKey := h.app.GenerateRandomAuthKey()
|
||||
|
||||
err := h.app.Database.ClearFCMTokens(ctx, b.FCMToken)
|
||||
err := h.database.ClearFCMTokens(ctx, b.FCMToken)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
|
||||
}
|
||||
|
||||
if b.ProToken != nil {
|
||||
err := h.app.Database.ClearProTokens(ctx, *b.ProToken)
|
||||
err := h.database.ClearProTokens(ctx, *b.ProToken)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
|
||||
}
|
||||
}
|
||||
|
||||
userobj, err := h.app.Database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, b.Username)
|
||||
userobj, err := h.database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, b.Username)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
|
||||
}
|
||||
|
||||
_, err = h.app.Database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion)
|
||||
_, err = h.database.CreateClient(ctx, userobj.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
|
||||
}
|
||||
@ -124,7 +133,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
|
||||
return *permResp
|
||||
}
|
||||
|
||||
user, err := h.app.Database.GetUser(ctx, u.UserID)
|
||||
user, err := h.database.GetUser(ctx, u.UserID)
|
||||
if err == sql.ErrNoRows {
|
||||
return ginresp.InternAPIError(404, apierr.USER_NOT_FOUND, "User not found", err)
|
||||
}
|
||||
@ -177,7 +186,7 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
|
||||
username = nil
|
||||
}
|
||||
|
||||
err := h.app.Database.UpdateUserUsername(ctx, u.UserID, b.Username)
|
||||
err := h.database.UpdateUserUsername(ctx, u.UserID, b.Username)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update user", err)
|
||||
}
|
||||
@ -193,18 +202,18 @@ func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
|
||||
return ginresp.InternAPIError(400, apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil)
|
||||
}
|
||||
|
||||
err = h.app.Database.ClearProTokens(ctx, *b.ProToken)
|
||||
err = h.database.ClearProTokens(ctx, *b.ProToken)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.UpdateUserProToken(ctx, u.UserID, b.ProToken)
|
||||
err = h.database.UpdateUserProToken(ctx, u.UserID, b.ProToken)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update user", err)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := h.app.Database.GetUser(ctx, u.UserID)
|
||||
user, err := h.database.GetUser(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err)
|
||||
}
|
||||
@ -245,7 +254,7 @@ func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
|
||||
return *permResp
|
||||
}
|
||||
|
||||
clients, err := h.app.Database.ListClients(ctx, u.UserID)
|
||||
clients, err := h.database.ListClients(ctx, u.UserID)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query clients", err)
|
||||
}
|
||||
@ -287,7 +296,7 @@ func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
|
||||
return *permResp
|
||||
}
|
||||
|
||||
client, err := h.app.Database.GetClient(ctx, u.UserID, u.ClientID)
|
||||
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
|
||||
if err == sql.ErrNoRows {
|
||||
return ginresp.InternAPIError(404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
|
||||
}
|
||||
@ -346,7 +355,7 @@ func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
|
||||
return *permResp
|
||||
}
|
||||
|
||||
client, err := h.app.Database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion)
|
||||
client, err := h.database.CreateClient(ctx, u.UserID, clientType, b.FCMToken, b.AgentModel, b.AgentVersion)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
|
||||
}
|
||||
@ -386,7 +395,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
|
||||
return *permResp
|
||||
}
|
||||
|
||||
client, err := h.app.Database.GetClient(ctx, u.UserID, u.ClientID)
|
||||
client, err := h.database.GetClient(ctx, u.UserID, u.ClientID)
|
||||
if err == sql.ErrNoRows {
|
||||
return ginresp.InternAPIError(404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
|
||||
}
|
||||
@ -394,7 +403,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query client", err)
|
||||
}
|
||||
|
||||
err = h.app.Database.DeleteClient(ctx, u.ClientID)
|
||||
err = h.database.DeleteClient(ctx, u.ClientID)
|
||||
if err != nil {
|
||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to delete client", err)
|
||||
}
|
||||
@ -446,8 +455,6 @@ func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
|
||||
return ginresp.NotImplemented()
|
||||
}
|
||||
|
||||
func NewAPIHandler(app *logic.Application) APIHandler {
|
||||
return APIHandler{
|
||||
app: app,
|
||||
}
|
||||
func (h APIHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
|
||||
return ginresp.NotImplemented()
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/models"
|
||||
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
|
@ -1,21 +1,232 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/dataext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/mathext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MessageHandler struct {
|
||||
app *logic.Application
|
||||
}
|
||||
|
||||
func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
|
||||
return ginresp.NotImplemented()
|
||||
database *db.Database
|
||||
}
|
||||
|
||||
func NewMessageHandler(app *logic.Application) MessageHandler {
|
||||
return MessageHandler{
|
||||
app: app,
|
||||
database: app.Database,
|
||||
}
|
||||
}
|
||||
|
||||
// SendMessage swaggerdoc
|
||||
//
|
||||
// @Summary Send a new message
|
||||
// @Description All parameter can be set via query-parameter or the json body. Only UserID, UserKey and Title are required
|
||||
//
|
||||
// @Param query_data query handler.SendMessage.query false " "
|
||||
// @Param post_body body handler.SendMessage.body false " "
|
||||
//
|
||||
// @Success 200 {object} handler.SendMessage.response
|
||||
// @Failure 400 {object} ginresp.apiError
|
||||
// @Failure 401 {object} ginresp.apiError
|
||||
// @Failure 403 {object} ginresp.apiError
|
||||
// @Failure 404 {object} ginresp.apiError
|
||||
// @Failure 500 {object} ginresp.apiError
|
||||
//
|
||||
// @Router / [POST]
|
||||
// @Router /send [POST]
|
||||
func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
|
||||
type query struct {
|
||||
UserID *int64 `form:"user_id"`
|
||||
UserKey *string `form:"user_key"`
|
||||
Channel *string `form:"channel"`
|
||||
ChanKey *string `form:"chan_key"`
|
||||
Title *string `form:"message_title"`
|
||||
Content *string `form:"message_content"`
|
||||
Priority *int `form:"priority"`
|
||||
UserMessageID *string `form:"msg_id"`
|
||||
SendTimestamp *float64 `form:"timestamp"`
|
||||
}
|
||||
type body struct {
|
||||
UserID *int64 `json:"user_id"`
|
||||
UserKey *string `json:"user_key"`
|
||||
Channel *string `json:"channel"`
|
||||
ChanKey *string `form:"chan_key"`
|
||||
Title *string `json:"message_title"`
|
||||
Content *string `json:"message_content"`
|
||||
Priority *int `json:"priority"`
|
||||
UserMessageID *string `json:"msg_id"`
|
||||
SendTimestamp *float64 `json:"timestamp"`
|
||||
}
|
||||
type response struct {
|
||||
Success bool `json:"success"`
|
||||
ErrorID apierr.APIError `json:"error"`
|
||||
ErrorHighlight int `json:"errhighlight"`
|
||||
Message string `json:"message"`
|
||||
SuppressSend bool `json:"suppress_send"`
|
||||
Response string `json:"response"`
|
||||
MessageCount int `json:"messagecount"`
|
||||
Quota int `json:"quota"`
|
||||
IsPro bool `json:"is_pro"`
|
||||
QuotaMax int `json:"quota_max"`
|
||||
SCNMessageID int64 `json:"scn_msg_id"`
|
||||
}
|
||||
|
||||
var b body
|
||||
var q query
|
||||
ctx, errResp := h.app.StartRequest(g, nil, &q, &b)
|
||||
if errResp != nil {
|
||||
return *errResp
|
||||
}
|
||||
defer ctx.Cancel()
|
||||
|
||||
data := dataext.ObjectMerge(b, q)
|
||||
|
||||
if data.UserID == nil {
|
||||
return ginresp.SendAPIError(400, apierr.MISSING_UID, 101, "Missing parameter [[user_id]]")
|
||||
}
|
||||
if data.UserKey == nil {
|
||||
return ginresp.SendAPIError(400, apierr.MISSING_UID, 102, "Missing parameter [[user_token]]")
|
||||
}
|
||||
if data.Title == nil {
|
||||
return ginresp.SendAPIError(400, apierr.MISSING_UID, 103, "Missing parameter [[title]]")
|
||||
}
|
||||
if data.SendTimestamp != nil && mathext.Abs(*data.SendTimestamp-float64(time.Now().Unix())) > (24*time.Hour).Seconds() {
|
||||
return ginresp.SendAPIError(400, apierr.TIMESTAMP_OUT_OF_RANGE, -1, "The timestamp mus be within 24 hours of now()")
|
||||
}
|
||||
if data.Priority != nil && (*data.Priority != 0 && *data.Priority != 1 && *data.Priority != 2) {
|
||||
return ginresp.SendAPIError(400, apierr.INVALID_PRIO, 105, "Invalid priority")
|
||||
}
|
||||
if len(strings.TrimSpace(*data.Title)) == 0 {
|
||||
return ginresp.SendAPIError(400, apierr.NO_TITLE, 103, "No title specified")
|
||||
}
|
||||
if data.UserMessageID != nil && len(strings.TrimSpace(*data.UserMessageID)) > 64 {
|
||||
return ginresp.SendAPIError(400, apierr.USR_MSG_ID_TOO_LONG, -1, "MessageID too long (64 characters)")
|
||||
}
|
||||
|
||||
channelName := "main"
|
||||
if data.Channel != nil {
|
||||
channelName = strings.ToLower(strings.TrimSpace(*data.Channel))
|
||||
}
|
||||
|
||||
user, err := h.database.GetUser(ctx, *data.UserID)
|
||||
if err == sql.ErrNoRows {
|
||||
return ginresp.SendAPIError(400, apierr.USER_NOT_FOUND, -1, "User not found")
|
||||
}
|
||||
if err != nil {
|
||||
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query user")
|
||||
}
|
||||
|
||||
if len(strings.TrimSpace(*data.Title)) > 120 {
|
||||
return ginresp.SendAPIError(400, apierr.TITLE_TOO_LONG, 103, "Title too long (120 characters)")
|
||||
}
|
||||
if data.Content != nil && len(strings.TrimSpace(*data.Content)) > user.MaxContentLength() {
|
||||
return ginresp.SendAPIError(400, apierr.CONTENT_TOO_LONG, 104, fmt.Sprintf("Content too long (%d characters; max := %d characters)", len(strings.TrimSpace(*data.Content)), user.MaxContentLength()))
|
||||
}
|
||||
|
||||
if data.UserMessageID != nil {
|
||||
msg, err := h.database.GetMessageByUserMessageID(ctx, *data.UserMessageID)
|
||||
if err != nil {
|
||||
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query existing message")
|
||||
}
|
||||
if msg != nil {
|
||||
return ginresp.JSON(http.StatusOK, response{
|
||||
Success: true,
|
||||
ErrorID: apierr.NO_ERROR,
|
||||
ErrorHighlight: 0,
|
||||
Message: "Message already sent",
|
||||
SuppressSend: true,
|
||||
Response: "",
|
||||
MessageCount: user.MessagesSent,
|
||||
Quota: user.QuotaUsedToday(),
|
||||
IsPro: user.IsPro,
|
||||
QuotaMax: user.QuotaPerDay(),
|
||||
SCNMessageID: msg.SCNMessageID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if user.QuotaRemainingToday() <= 0 {
|
||||
return ginresp.SendAPIError(403, apierr.QUOTA_REACHED, -1, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()))
|
||||
}
|
||||
|
||||
channel, err := h.app.GetOrCreateChannel(ctx, *data.UserID, channelName)
|
||||
if err != nil {
|
||||
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query/create channel")
|
||||
}
|
||||
|
||||
selfChanAdmin := *data.UserID == channel.OwnerUserID && *data.UserKey == user.AdminKey
|
||||
selfChanSend := *data.UserID == channel.OwnerUserID && *data.UserKey == user.SendKey
|
||||
forgChanSend := *data.UserID != channel.OwnerUserID && data.ChanKey != nil && *data.ChanKey == channel.SendKey
|
||||
|
||||
if !selfChanAdmin && !selfChanSend && !forgChanSend {
|
||||
return ginresp.SendAPIError(401, apierr.USER_AUTH_FAILED, 102, fmt.Sprintf("Daily quota reached (%d)", user.QuotaPerDay()))
|
||||
}
|
||||
|
||||
var sendTimestamp *time.Time = nil
|
||||
if data.SendTimestamp != nil {
|
||||
sendTimestamp = langext.Ptr(timeext.UnixFloatSeconds(*data.SendTimestamp))
|
||||
}
|
||||
|
||||
priority := langext.Coalesce(data.Priority, 1)
|
||||
|
||||
msg, err := h.database.CreateMessage(ctx, *data.UserID, channel, sendTimestamp, *data.Title, data.Content, priority, data.UserMessageID)
|
||||
if err != nil {
|
||||
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create message in db")
|
||||
}
|
||||
|
||||
subscriptions, err := h.database.ListChannelSubscriptions(ctx, channel.ChannelID)
|
||||
if err != nil {
|
||||
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to query subscriptions")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
for _, client := range clients {
|
||||
|
||||
fcmDelivID, err := h.deliverMessage(ctx, client, msg)
|
||||
if err != nil {
|
||||
_, err = h.database.CreateRetryDelivery(ctx, client, msg)
|
||||
if err != nil {
|
||||
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create delivery")
|
||||
}
|
||||
} else {
|
||||
_, err = h.database.CreateSuccessDelivery(ctx, client, msg, *fcmDelivID)
|
||||
if err != nil {
|
||||
return ginresp.SendAPIError(500, apierr.DATABASE_ERROR, -1, "Failed to create delivery")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return ginresp.NotImplemented()
|
||||
}
|
||||
|
||||
func (h MessageHandler) deliverMessage(ctx *logic.AppContext, client models.Client, msg models.Message) (*string, error) {
|
||||
if client.FCMToken != nil {
|
||||
fcmDelivID, err := h.app.Firebase.SendNotification(ctx, client, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return langext.Ptr(fcmDelivID), nil
|
||||
} else {
|
||||
return langext.Ptr(""), nil
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ func (r *Router) Init(e *gin.Engine) {
|
||||
apiv2.GET("/messages/:mid", ginresp.Wrap(r.apiHandler.GetMessage))
|
||||
apiv2.DELETE("/messages/:mid", ginresp.Wrap(r.apiHandler.DeleteMessage))
|
||||
|
||||
apiv2.POST("/messages", ginresp.Wrap(r.messageHandler.SendMessage))
|
||||
apiv2.POST("/messages", ginresp.Wrap(r.apiHandler.SendMessage))
|
||||
}
|
||||
|
||||
// ================ Send API ================
|
||||
@ -126,7 +126,7 @@ func (r *Router) Init(e *gin.Engine) {
|
||||
{
|
||||
sendAPI.POST("/", ginresp.Wrap(r.messageHandler.SendMessage))
|
||||
sendAPI.POST("/send", ginresp.Wrap(r.messageHandler.SendMessage))
|
||||
sendAPI.POST("/send.php")
|
||||
sendAPI.POST("/send.php", ginresp.Wrap(r.messageHandler.SendMessage))
|
||||
}
|
||||
|
||||
if r.app.Config.ReturnRawErrors {
|
||||
|
@ -85,8 +85,8 @@ func InternAPIError(status int, errorid apierr.APIError, msg string, e error) HT
|
||||
}
|
||||
}
|
||||
|
||||
func SendAPIError(errorid apierr.APIError, highlight int, msg string) HTTPResponse {
|
||||
return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}}
|
||||
func SendAPIError(status int, errorid apierr.APIError, highlight int, msg string) HTTPResponse {
|
||||
return &errHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}}
|
||||
}
|
||||
|
||||
func NotImplemented() HTTPResponse {
|
||||
|
@ -1,7 +1,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/api/models"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"database/sql"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"time"
|
||||
@ -42,8 +42,8 @@ func (db *Database) CreateUser(ctx TxContext, readKey string, sendKey string, ad
|
||||
TimestampLastRead: nil,
|
||||
TimestampLastSent: nil,
|
||||
MessagesSent: 0,
|
||||
QuotaToday: 0,
|
||||
QuotaDay: nil,
|
||||
QuotaUsed: 0,
|
||||
QuotaUsedDay: nil,
|
||||
IsPro: protoken != nil,
|
||||
ProToken: protoken,
|
||||
}, nil
|
||||
@ -254,3 +254,261 @@ func (db *Database) DeleteClient(ctx TxContext, clientid int64) error {
|
||||
|
||||
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) ListChannelSubscriptions(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) 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
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ CREATE TABLE users
|
||||
|
||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||
|
||||
quota_today INTEGER NOT NULL DEFAULT '0',
|
||||
quota_day TEXT NULL DEFAULT NULL,
|
||||
quota_used INTEGER NOT NULL DEFAULT '0',
|
||||
quota_used_day TEXT NULL DEFAULT NULL,
|
||||
|
||||
is_pro INTEGER CHECK(is_pro IN (0, 1)) NOT NULL DEFAULT 0,
|
||||
pro_token TEXT NULL DEFAULT NULL
|
||||
@ -66,8 +66,11 @@ CREATE TABLE subscriptions
|
||||
subscriber_user_id INTEGER NOT NULL,
|
||||
channel_owner_user_id INTEGER NOT NULL,
|
||||
channel_name TEXT NOT NULL,
|
||||
channel_id INTEGER NOT NULL,
|
||||
|
||||
confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL DEFAULT 0
|
||||
timestamp_created INTEGER NOT NULL,
|
||||
|
||||
confirmed INTEGER CHECK(confirmed IN (0, 1)) NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX "idx_subscriptions_ref" ON subscriptions (subscriber_user_id, channel_owner_user_id, channel_name);
|
||||
|
||||
@ -76,8 +79,8 @@ CREATE TABLE messages
|
||||
(
|
||||
scn_message_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sender_user_id INTEGER NOT NULL,
|
||||
owner_user_id INTEGER NOT NULL,
|
||||
channel_name TEXT NOT NULL,
|
||||
|
||||
channel_id INTEGER NOT NULL,
|
||||
|
||||
timestamp_real INTEGER NOT NULL,
|
||||
@ -88,8 +91,8 @@ CREATE TABLE messages
|
||||
priority INTEGER CHECK(priority IN (0, 1, 2)) NOT NULL,
|
||||
usr_message_id TEXT NULL
|
||||
);
|
||||
CREATE INDEX "idx_messages_channel" ON messages (sender_user_id, channel_name);
|
||||
CREATE INDEX "idx_messages_idempotency" ON messages (sender_user_id, usr_message_id);
|
||||
CREATE INDEX "idx_messages_channel" ON messages (owner_user_id, channel_name);
|
||||
CREATE INDEX "idx_messages_idempotency" ON messages (owner_user_id, usr_message_id);
|
||||
|
||||
|
||||
CREATE TABLE deliveries
|
||||
|
@ -1,6 +1,9 @@
|
||||
package db
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"time"
|
||||
)
|
||||
|
||||
func bool2DB(b bool) int {
|
||||
if b {
|
||||
@ -13,3 +16,10 @@ func bool2DB(b bool) int {
|
||||
func time2DB(t time.Time) int64 {
|
||||
return t.UnixMilli()
|
||||
}
|
||||
|
||||
func time2DBOpt(t *time.Time) *int64 {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
return langext.Ptr(t.UnixMilli())
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
package firebase
|
||||
|
||||
import (
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"context"
|
||||
_ "embed"
|
||||
fb "firebase.google.com/go"
|
||||
"firebase.google.com/go/messaging"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"google.golang.org/api/option"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//go:embed scnserviceaccountkey.json
|
||||
@ -43,26 +46,26 @@ type Notification struct {
|
||||
Priority int
|
||||
}
|
||||
|
||||
func (fb App) SendNotification(ctx context.Context, notification Notification) (string, error) {
|
||||
func (fb App) SendNotification(ctx context.Context, client models.Client, msg models.Message) (string, error) {
|
||||
n := messaging.Message{
|
||||
Data: map[string]string{"scn_msg_id": notification.Id},
|
||||
Data: map[string]string{"scn_msg_id": strconv.FormatInt(msg.SCNMessageID, 10)},
|
||||
Notification: &messaging.Notification{
|
||||
Title: notification.Title,
|
||||
Body: notification.Body,
|
||||
Title: msg.Title,
|
||||
Body: langext.Coalesce(msg.Content, ""),
|
||||
},
|
||||
Android: nil,
|
||||
APNS: nil,
|
||||
Webpush: nil,
|
||||
FCMOptions: nil,
|
||||
Token: notification.Token,
|
||||
Token: *client.FCMToken,
|
||||
Topic: "",
|
||||
Condition: "",
|
||||
}
|
||||
if notification.Platform == "ios" {
|
||||
if client.Type == models.ClientTypeIOS {
|
||||
n.APNS = nil
|
||||
}
|
||||
|
||||
if notification.Platform == "android" {
|
||||
if client.Type == models.ClientTypeAndroid {
|
||||
n.Android = nil
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ require (
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/swaggo/swag v1.8.7
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.18
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.20
|
||||
google.golang.org/api v0.103.0
|
||||
)
|
||||
|
||||
|
@ -162,6 +162,12 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.17 h1:jsfbvII7aa0SH9qY0fnXBdtNnQe1YY3DgXDThEwLICc=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.17/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.18 h1:fprrLoAPGdI4ObveHR1DjiP9WhlTJppWtjqMA6ZkyS8=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.18/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.19 h1:IvCHlIHDviHQXntZFTNdV7qNq5yQnSEMxF8LA0Tf3IY=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.19/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.20 h1:HxJ0iZ838TQnp/a+/DNajdZjZkV43OsK4VbHarOiHTs=
|
||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.20/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||
"blackforestbytes.com/simplecloudnotifier/db"
|
||||
"blackforestbytes.com/simplecloudnotifier/firebase"
|
||||
"blackforestbytes.com/simplecloudnotifier/models"
|
||||
"context"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
@ -174,3 +175,31 @@ func (app *Application) getPermissions(ctx *AppContext, hdr string) (PermissionS
|
||||
|
||||
return NewEmptyPermissions(), nil
|
||||
}
|
||||
|
||||
func (app *Application) GetOrCreateChannel(ctx *AppContext, userid int64, chanName string) (models.Channel, error) {
|
||||
chanName = strings.ToLower(strings.TrimSpace(chanName))
|
||||
|
||||
existingChan, err := app.Database.GetChannelByName(ctx, userid, chanName)
|
||||
if err != nil {
|
||||
return models.Channel{}, err
|
||||
}
|
||||
|
||||
if existingChan != nil {
|
||||
return *existingChan, nil
|
||||
}
|
||||
|
||||
subscribeKey := app.GenerateRandomAuthKey()
|
||||
sendKey := app.GenerateRandomAuthKey()
|
||||
|
||||
newChan, err := app.Database.CreateChannel(ctx, userid, chanName, subscribeKey, sendKey)
|
||||
if err != nil {
|
||||
return models.Channel{}, err
|
||||
}
|
||||
|
||||
_, err = app.Database.CreateSubscribtion(ctx, userid, userid, newChan.Name, newChan.ChannelID, true)
|
||||
if err != nil {
|
||||
return models.Channel{}, err
|
||||
}
|
||||
|
||||
return newChan, nil
|
||||
}
|
||||
|
103
server/models/delivery.go
Normal file
103
server/models/delivery.go
Normal file
@ -0,0 +1,103 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/blockloop/scan"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"time"
|
||||
)
|
||||
|
||||
type DeliveryStatus string
|
||||
|
||||
const (
|
||||
DeliveryStatusRetry DeliveryStatus = "RETRY"
|
||||
DeliveryStatusSuccess DeliveryStatus = "SUCCESS"
|
||||
DeliveryStatusFailed DeliveryStatus = "FAILED"
|
||||
)
|
||||
|
||||
type Delivery struct {
|
||||
DeliveryID int64
|
||||
SCNMessageID int64
|
||||
ReceiverUserID int64
|
||||
ReceiverClientID int64
|
||||
TimestampCreated time.Time
|
||||
TimestampFinalized *time.Time
|
||||
Status DeliveryStatus
|
||||
RetryCount int
|
||||
NextDelivery *time.Time
|
||||
FCMMessageID *string
|
||||
}
|
||||
|
||||
func (d Delivery) JSON() DeliveryJSON {
|
||||
return DeliveryJSON{
|
||||
DeliveryID: d.DeliveryID,
|
||||
SCNMessageID: d.SCNMessageID,
|
||||
ReceiverUserID: d.ReceiverUserID,
|
||||
ReceiverClientID: d.ReceiverClientID,
|
||||
TimestampCreated: d.TimestampCreated.Format(time.RFC3339Nano),
|
||||
TimestampFinalized: timeOptFmt(d.TimestampFinalized, time.RFC3339Nano),
|
||||
Status: d.Status,
|
||||
RetryCount: d.RetryCount,
|
||||
NextDelivery: timeOptFmt(d.NextDelivery, time.RFC3339Nano),
|
||||
FCMMessageID: d.FCMMessageID,
|
||||
}
|
||||
}
|
||||
|
||||
type DeliveryJSON struct {
|
||||
DeliveryID int64 `json:"delivery_id"`
|
||||
SCNMessageID int64 `json:"scn_message_id"`
|
||||
ReceiverUserID int64 `json:"receiver_user_id"`
|
||||
ReceiverClientID int64 `json:"receiver_client_id"`
|
||||
TimestampCreated string `json:"timestamp_created"`
|
||||
TimestampFinalized *string `json:"tiestamp_finalized"`
|
||||
Status DeliveryStatus `json:"status"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
NextDelivery *string `json:"next_delivery"`
|
||||
FCMMessageID *string `json:"fcm_message_id"`
|
||||
}
|
||||
|
||||
type DeliveryDB struct {
|
||||
DeliveryID int64 `db:"delivery_id"`
|
||||
SCNMessageID int64 `db:"scn_message_id"`
|
||||
ReceiverUserID int64 `db:"receiver_user_id"`
|
||||
ReceiverClientID int64 `db:"receiver_client_id"`
|
||||
TimestampCreated int64 `db:"timestamp_created"`
|
||||
TimestampFinalized *int64 `db:"tiestamp_finalized"`
|
||||
Status DeliveryStatus `db:"status"`
|
||||
RetryCount int `db:"retry_count"`
|
||||
NextDelivery *int64 `db:"next_delivery"`
|
||||
FCMMessageID *string `db:"fcm_message_id"`
|
||||
}
|
||||
|
||||
func (d DeliveryDB) Model() Delivery {
|
||||
return Delivery{
|
||||
DeliveryID: d.DeliveryID,
|
||||
SCNMessageID: d.SCNMessageID,
|
||||
ReceiverUserID: d.ReceiverUserID,
|
||||
ReceiverClientID: d.ReceiverClientID,
|
||||
TimestampCreated: time.UnixMilli(d.TimestampCreated),
|
||||
TimestampFinalized: timeOptFromMilli(d.TimestampFinalized),
|
||||
Status: d.Status,
|
||||
RetryCount: d.RetryCount,
|
||||
NextDelivery: timeOptFromMilli(d.NextDelivery),
|
||||
FCMMessageID: d.FCMMessageID,
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeDelivery(r *sql.Rows) (Delivery, error) {
|
||||
var data DeliveryDB
|
||||
err := scan.RowStrict(&data, r)
|
||||
if err != nil {
|
||||
return Delivery{}, err
|
||||
}
|
||||
return data.Model(), nil
|
||||
}
|
||||
|
||||
func DecodeDeliveries(r *sql.Rows) ([]Delivery, error) {
|
||||
var data []DeliveryDB
|
||||
err := scan.RowsStrict(&data, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return langext.ArrMap(data, func(v DeliveryDB) Delivery { return v.Model() }), nil
|
||||
}
|
100
server/models/message.go
Normal file
100
server/models/message.go
Normal file
@ -0,0 +1,100 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/blockloop/scan"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
SCNMessageID int64
|
||||
SenderUserID int64
|
||||
OwnerUserID int64
|
||||
ChannelName string
|
||||
ChannelID int64
|
||||
TimestampReal time.Time
|
||||
TimestampClient *time.Time
|
||||
Title string
|
||||
Content *string
|
||||
Priority int
|
||||
UserMessageID *string
|
||||
}
|
||||
|
||||
func (m Message) JSON() MessageJSON {
|
||||
return MessageJSON{
|
||||
SCNMessageID: m.SCNMessageID,
|
||||
SenderUserID: m.SenderUserID,
|
||||
OwnerUserID: m.OwnerUserID,
|
||||
ChannelName: m.ChannelName,
|
||||
ChannelID: m.ChannelID,
|
||||
Timestamp: langext.Coalesce(m.TimestampClient, m.TimestampReal).Format(time.RFC3339Nano),
|
||||
Title: m.Title,
|
||||
Content: m.Content,
|
||||
Priority: m.Priority,
|
||||
UserMessageID: m.UserMessageID,
|
||||
Trimmed: false,
|
||||
}
|
||||
}
|
||||
|
||||
type MessageJSON struct {
|
||||
SCNMessageID int64 `json:"scn_message_id"`
|
||||
SenderUserID int64 `json:"sender_user_id"`
|
||||
OwnerUserID int64 `json:"owner_user_id"`
|
||||
ChannelName string `json:"channel_name"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Title string `json:"title"`
|
||||
Content *string `json:"body"`
|
||||
Priority int `json:"priority"`
|
||||
UserMessageID *string `json:"usr_message_id"`
|
||||
Trimmed bool `json:"trimmed"`
|
||||
}
|
||||
|
||||
type MessageDB struct {
|
||||
SCNMessageID int64 `db:"scn_message_id"`
|
||||
SenderUserID int64 `db:"sender_user_id"`
|
||||
OwnerUserID int64 `db:"owner_user_id"`
|
||||
ChannelName string `db:"channel_name"`
|
||||
ChannelID int64 `db:"channel_id"`
|
||||
TimestampReal int64 `db:"timestamp_real"`
|
||||
TimestampClient *int64 `db:"timestamp_client"`
|
||||
Title string `db:"title"`
|
||||
Content *string `db:"content"`
|
||||
Priority int `db:"priority"`
|
||||
UserMessageID *string `db:"usr_message_id"`
|
||||
}
|
||||
|
||||
func (m MessageDB) Model() Message {
|
||||
return Message{
|
||||
SCNMessageID: m.SCNMessageID,
|
||||
SenderUserID: m.SenderUserID,
|
||||
OwnerUserID: m.OwnerUserID,
|
||||
ChannelName: m.ChannelName,
|
||||
ChannelID: m.ChannelID,
|
||||
TimestampReal: time.UnixMilli(m.TimestampReal),
|
||||
TimestampClient: timeOptFromMilli(m.TimestampClient),
|
||||
Title: m.Title,
|
||||
Content: m.Content,
|
||||
Priority: m.Priority,
|
||||
UserMessageID: m.UserMessageID,
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeMessage(r *sql.Rows) (Message, error) {
|
||||
var data MessageDB
|
||||
err := scan.RowStrict(&data, r)
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
return data.Model(), nil
|
||||
}
|
||||
|
||||
func DecodeMessages(r *sql.Rows) ([]Message, error) {
|
||||
var data []MessageDB
|
||||
err := scan.RowsStrict(&data, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return langext.ArrMap(data, func(v MessageDB) Message { return v.Model() }), nil
|
||||
}
|
80
server/models/subscription.go
Normal file
80
server/models/subscription.go
Normal file
@ -0,0 +1,80 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/blockloop/scan"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Subscription struct {
|
||||
SubscriptionID int64
|
||||
SubscriberUserID int64
|
||||
ChannelOwnerUserID int64
|
||||
ChannelID int64
|
||||
ChannelName string
|
||||
TimestampCreated time.Time
|
||||
Confirmed bool
|
||||
}
|
||||
|
||||
func (s Subscription) JSON() SubscriptionJSON {
|
||||
return SubscriptionJSON{
|
||||
SubscriptionID: s.SubscriptionID,
|
||||
SubscriberUserID: s.SubscriberUserID,
|
||||
ChannelOwnerUserID: s.ChannelOwnerUserID,
|
||||
ChannelID: s.ChannelID,
|
||||
ChannelName: s.ChannelName,
|
||||
TimestampCreated: s.TimestampCreated.Format(time.RFC3339Nano),
|
||||
Confirmed: s.Confirmed,
|
||||
}
|
||||
}
|
||||
|
||||
type SubscriptionJSON struct {
|
||||
SubscriptionID int64 `json:"subscription_id"`
|
||||
SubscriberUserID int64 `json:"subscriber_user_id"`
|
||||
ChannelOwnerUserID int64 `json:"channel_owner_user_id"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
ChannelName string `json:"channel_name"`
|
||||
TimestampCreated string `json:"timestamp_created"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
}
|
||||
|
||||
type SubscriptionDB struct {
|
||||
SubscriptionID int64 `db:"subscription_id"`
|
||||
SubscriberUserID int64 `db:"subscriber_user_id"`
|
||||
ChannelOwnerUserID int64 `db:"channel_owner_user_id"`
|
||||
ChannelID int64 `db:"channel_id"`
|
||||
ChannelName string `db:"channel_name"`
|
||||
TimestampCreated int64 `db:"timestamp_created"`
|
||||
Confirmed int `db:"confirmed"`
|
||||
}
|
||||
|
||||
func (s SubscriptionDB) Model() Subscription {
|
||||
return Subscription{
|
||||
SubscriptionID: s.SubscriptionID,
|
||||
SubscriberUserID: s.SubscriberUserID,
|
||||
ChannelOwnerUserID: s.ChannelOwnerUserID,
|
||||
ChannelID: s.ChannelID,
|
||||
ChannelName: s.ChannelName,
|
||||
TimestampCreated: time.UnixMilli(s.TimestampCreated),
|
||||
Confirmed: s.Confirmed != 0,
|
||||
}
|
||||
}
|
||||
|
||||
func DecodeSubscription(r *sql.Rows) (Subscription, error) {
|
||||
var data SubscriptionDB
|
||||
err := scan.RowStrict(&data, r)
|
||||
if err != nil {
|
||||
return Subscription{}, err
|
||||
}
|
||||
return data.Model(), nil
|
||||
}
|
||||
|
||||
func DecodeSubscriptions(r *sql.Rows) ([]Subscription, error) {
|
||||
var data []SubscriptionDB
|
||||
err := scan.RowsStrict(&data, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return langext.ArrMap(data, func(v SubscriptionDB) Subscription { return v.Model() }), nil
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"github.com/blockloop/scan"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -17,8 +18,8 @@ type User struct {
|
||||
TimestampLastRead *time.Time
|
||||
TimestampLastSent *time.Time
|
||||
MessagesSent int
|
||||
QuotaToday int
|
||||
QuotaDay *string
|
||||
QuotaUsed int
|
||||
QuotaUsedDay *string
|
||||
IsPro bool
|
||||
ProToken *string
|
||||
}
|
||||
@ -34,12 +35,41 @@ func (u User) JSON() UserJSON {
|
||||
TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano),
|
||||
TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano),
|
||||
MessagesSent: u.MessagesSent,
|
||||
QuotaToday: u.QuotaToday,
|
||||
QuotaDay: u.QuotaDay,
|
||||
QuotaUsed: u.QuotaUsed,
|
||||
QuotaUsedDay: u.QuotaUsedDay,
|
||||
IsPro: u.IsPro,
|
||||
}
|
||||
}
|
||||
|
||||
func (u User) MaxContentLength() int {
|
||||
if u.IsPro {
|
||||
return 16384
|
||||
} else {
|
||||
return 2048
|
||||
}
|
||||
}
|
||||
|
||||
func (u User) QuotaPerDay() int {
|
||||
if u.IsPro {
|
||||
return 1000
|
||||
} else {
|
||||
return 50
|
||||
}
|
||||
}
|
||||
|
||||
func (u User) QuotaUsedToday() int {
|
||||
now := time.Now().In(timeext.TimezoneBerlin).Format("2006-01-02")
|
||||
if u.QuotaUsedDay != nil && *u.QuotaUsedDay == now {
|
||||
return u.QuotaUsed
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (u User) QuotaRemainingToday() int {
|
||||
return u.QuotaPerDay() - u.QuotaUsedToday()
|
||||
}
|
||||
|
||||
type UserJSON struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Username *string `json:"username"`
|
||||
@ -50,8 +80,8 @@ type UserJSON struct {
|
||||
TimestampLastRead *string `json:"timestamp_last_read"`
|
||||
TimestampLastSent *string `json:"timestamp_last_sent"`
|
||||
MessagesSent int `json:"messages_sent"`
|
||||
QuotaToday int `json:"quota_today"`
|
||||
QuotaDay *string `json:"quota_day"`
|
||||
QuotaUsed int `json:"quota_used"`
|
||||
QuotaUsedDay *string `json:"quota_used_day"`
|
||||
IsPro bool `json:"is_pro"`
|
||||
}
|
||||
|
||||
@ -65,8 +95,8 @@ type UserDB struct {
|
||||
TimestampLastRead *int64 `db:"timestamp_lastread"`
|
||||
TimestampLastSent *int64 `db:"timestamp_lastsent"`
|
||||
MessagesSent int `db:"messages_sent"`
|
||||
QuotaToday int `db:"quota_today"`
|
||||
QuotaDay *string `db:"quota_day"`
|
||||
QuotaUsed int `db:"quota_used"`
|
||||
QuotaUsedDay *string `db:"quota_used_day"`
|
||||
IsPro bool `db:"is_pro"`
|
||||
ProToken *string `db:"pro_token"`
|
||||
}
|
||||
@ -82,8 +112,8 @@ func (u UserDB) Model() User {
|
||||
TimestampLastRead: timeOptFromMilli(u.TimestampLastRead),
|
||||
TimestampLastSent: timeOptFromMilli(u.TimestampLastSent),
|
||||
MessagesSent: u.MessagesSent,
|
||||
QuotaToday: u.QuotaToday,
|
||||
QuotaDay: u.QuotaDay,
|
||||
QuotaUsed: u.QuotaUsed,
|
||||
QuotaUsedDay: u.QuotaUsedDay,
|
||||
IsPro: u.IsPro,
|
||||
}
|
||||
}
|
@ -9,6 +9,88 @@
|
||||
"host": "scn.blackforestbytes.com",
|
||||
"basePath": "/",
|
||||
"paths": {
|
||||
"/": {
|
||||
"post": {
|
||||
"summary": "Send a new message",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "message_content",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "message_title",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "priority",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "sendTimestamp",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "userMessageID",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "user_key",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "post_body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.SendMessage.body"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ClientJSON"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api-v2/user/": {
|
||||
"post": {
|
||||
"summary": "Create a new user",
|
||||
@ -688,6 +770,88 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/send": {
|
||||
"post": {
|
||||
"summary": "Send a new message",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "message_content",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "message_title",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "priority",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"name": "sendTimestamp",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "userMessageID",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "user_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "user_key",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"description": " ",
|
||||
"name": "post_body",
|
||||
"in": "body",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.SendMessage.body"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.ClientJSON"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Not Found",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ginresp.apiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
@ -894,6 +1058,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"handler.SendMessage.body": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message_content": {
|
||||
"type": "string"
|
||||
},
|
||||
"message_title": {
|
||||
"type": "string"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"sendTimestamp": {
|
||||
"type": "integer"
|
||||
},
|
||||
"userMessageID": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handler.Update.response": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -132,6 +132,23 @@ definitions:
|
||||
success:
|
||||
type: string
|
||||
type: object
|
||||
handler.SendMessage.body:
|
||||
properties:
|
||||
message_content:
|
||||
type: string
|
||||
message_title:
|
||||
type: string
|
||||
priority:
|
||||
type: integer
|
||||
sendTimestamp:
|
||||
type: integer
|
||||
user_id:
|
||||
type: string
|
||||
user_key:
|
||||
type: string
|
||||
userMessageID:
|
||||
type: string
|
||||
type: object
|
||||
handler.Update.response:
|
||||
properties:
|
||||
is_pro:
|
||||
@ -272,6 +289,57 @@ info:
|
||||
title: SimpleCloudNotifier API
|
||||
version: "2.0"
|
||||
paths:
|
||||
/:
|
||||
post:
|
||||
parameters:
|
||||
- in: query
|
||||
name: message_content
|
||||
type: string
|
||||
- in: query
|
||||
name: message_title
|
||||
type: string
|
||||
- in: query
|
||||
name: priority
|
||||
type: integer
|
||||
- in: query
|
||||
name: sendTimestamp
|
||||
type: integer
|
||||
- in: query
|
||||
name: userMessageID
|
||||
type: string
|
||||
- in: query
|
||||
name: user_id
|
||||
type: string
|
||||
- in: query
|
||||
name: user_key
|
||||
type: string
|
||||
- description: ' '
|
||||
in: body
|
||||
name: post_body
|
||||
schema:
|
||||
$ref: '#/definitions/handler.SendMessage.body'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.ClientJSON'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
summary: Send a new message
|
||||
/api-v2/user/:
|
||||
post:
|
||||
operationId: api-user-create
|
||||
@ -720,4 +788,55 @@ paths:
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
/send:
|
||||
post:
|
||||
parameters:
|
||||
- in: query
|
||||
name: message_content
|
||||
type: string
|
||||
- in: query
|
||||
name: message_title
|
||||
type: string
|
||||
- in: query
|
||||
name: priority
|
||||
type: integer
|
||||
- in: query
|
||||
name: sendTimestamp
|
||||
type: integer
|
||||
- in: query
|
||||
name: userMessageID
|
||||
type: string
|
||||
- in: query
|
||||
name: user_id
|
||||
type: string
|
||||
- in: query
|
||||
name: user_key
|
||||
type: string
|
||||
- description: ' '
|
||||
in: body
|
||||
name: post_body
|
||||
schema:
|
||||
$ref: '#/definitions/handler.SendMessage.body'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.ClientJSON'
|
||||
"400":
|
||||
description: Bad Request
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"401":
|
||||
description: Unauthorized
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"404":
|
||||
description: Not Found
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
schema:
|
||||
$ref: '#/definitions/ginresp.apiError'
|
||||
summary: Send a new message
|
||||
swagger: "2.0"
|
||||
|
Loading…
Reference in New Issue
Block a user