SendMessage()
This commit is contained in:
parent
fb37f94c0a
commit
85bfe79115
@ -2,9 +2,10 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/models"
|
|
||||||
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/db"
|
||||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
@ -13,7 +14,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type APIHandler struct {
|
type APIHandler struct {
|
||||||
app *logic.Application
|
app *logic.Application
|
||||||
|
database *db.Database
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIHandler(app *logic.Application) APIHandler {
|
||||||
|
return APIHandler{
|
||||||
|
app: app,
|
||||||
|
database: app.Database,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser swaggerdoc
|
// CreateUser swaggerdoc
|
||||||
@ -69,24 +78,24 @@ func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
sendKey := h.app.GenerateRandomAuthKey()
|
sendKey := h.app.GenerateRandomAuthKey()
|
||||||
adminKey := 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 {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
|
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.ProToken != nil {
|
if b.ProToken != nil {
|
||||||
err := h.app.Database.ClearProTokens(ctx, *b.ProToken)
|
err := h.database.ClearProTokens(ctx, *b.ProToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
|
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 {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
|
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, 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 {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
|
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
|
||||||
}
|
}
|
||||||
@ -124,7 +133,7 @@ func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
return *permResp
|
return *permResp
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.app.Database.GetUser(ctx, u.UserID)
|
user, err := h.database.GetUser(ctx, u.UserID)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return ginresp.InternAPIError(404, apierr.USER_NOT_FOUND, "User not found", err)
|
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
|
username = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.app.Database.UpdateUserUsername(ctx, u.UserID, b.Username)
|
err := h.database.UpdateUserUsername(ctx, u.UserID, b.Username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update user", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
|
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 {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to update user", err)
|
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 {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query (updated) user", err)
|
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
|
return *permResp
|
||||||
}
|
}
|
||||||
|
|
||||||
clients, err := h.app.Database.ListClients(ctx, u.UserID)
|
clients, err := h.database.ListClients(ctx, u.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to query clients", err)
|
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
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return ginresp.InternAPIError(404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
|
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
|
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 {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
|
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to create user in db", err)
|
||||||
}
|
}
|
||||||
@ -386,7 +395,7 @@ func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
return *permResp
|
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 {
|
if err == sql.ErrNoRows {
|
||||||
return ginresp.InternAPIError(404, apierr.CLIENT_NOT_FOUND, "Client not found", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return ginresp.InternAPIError(500, apierr.DATABASE_ERROR, "Failed to delete client", err)
|
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()
|
return ginresp.NotImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPIHandler(app *logic.Application) APIHandler {
|
func (h APIHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
|
||||||
return APIHandler{
|
return ginresp.NotImplemented()
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/models"
|
|
||||||
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,21 +1,232 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||||
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/db"
|
||||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"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 {
|
type MessageHandler struct {
|
||||||
app *logic.Application
|
app *logic.Application
|
||||||
}
|
database *db.Database
|
||||||
|
|
||||||
func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
|
|
||||||
return ginresp.NotImplemented()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessageHandler(app *logic.Application) MessageHandler {
|
func NewMessageHandler(app *logic.Application) MessageHandler {
|
||||||
return MessageHandler{
|
return MessageHandler{
|
||||||
app: app,
|
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.GET("/messages/:mid", ginresp.Wrap(r.apiHandler.GetMessage))
|
||||||
apiv2.DELETE("/messages/:mid", ginresp.Wrap(r.apiHandler.DeleteMessage))
|
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 ================
|
// ================ Send API ================
|
||||||
@ -126,7 +126,7 @@ func (r *Router) Init(e *gin.Engine) {
|
|||||||
{
|
{
|
||||||
sendAPI.POST("/", ginresp.Wrap(r.messageHandler.SendMessage))
|
sendAPI.POST("/", ginresp.Wrap(r.messageHandler.SendMessage))
|
||||||
sendAPI.POST("/send", 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 {
|
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 {
|
func SendAPIError(status int, errorid apierr.APIError, highlight int, msg string) HTTPResponse {
|
||||||
return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}}
|
return &errHTTPResponse{statusCode: status, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NotImplemented() HTTPResponse {
|
func NotImplemented() HTTPResponse {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/models"
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"time"
|
"time"
|
||||||
@ -42,8 +42,8 @@ func (db *Database) CreateUser(ctx TxContext, readKey string, sendKey string, ad
|
|||||||
TimestampLastRead: nil,
|
TimestampLastRead: nil,
|
||||||
TimestampLastSent: nil,
|
TimestampLastSent: nil,
|
||||||
MessagesSent: 0,
|
MessagesSent: 0,
|
||||||
QuotaToday: 0,
|
QuotaUsed: 0,
|
||||||
QuotaDay: nil,
|
QuotaUsedDay: nil,
|
||||||
IsPro: protoken != nil,
|
IsPro: protoken != nil,
|
||||||
ProToken: protoken,
|
ProToken: protoken,
|
||||||
}, nil
|
}, nil
|
||||||
@ -254,3 +254,261 @@ func (db *Database) DeleteClient(ctx TxContext, clientid int64) error {
|
|||||||
|
|
||||||
return nil
|
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',
|
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||||
|
|
||||||
quota_today INTEGER NOT NULL DEFAULT '0',
|
quota_used INTEGER NOT NULL DEFAULT '0',
|
||||||
quota_day TEXT NULL DEFAULT NULL,
|
quota_used_day TEXT NULL DEFAULT NULL,
|
||||||
|
|
||||||
is_pro INTEGER CHECK(is_pro IN (0, 1)) NOT NULL DEFAULT 0,
|
is_pro INTEGER CHECK(is_pro IN (0, 1)) NOT NULL DEFAULT 0,
|
||||||
pro_token TEXT NULL DEFAULT NULL
|
pro_token TEXT NULL DEFAULT NULL
|
||||||
@ -66,8 +66,11 @@ CREATE TABLE subscriptions
|
|||||||
subscriber_user_id INTEGER NOT NULL,
|
subscriber_user_id INTEGER NOT NULL,
|
||||||
channel_owner_user_id INTEGER NOT NULL,
|
channel_owner_user_id INTEGER NOT NULL,
|
||||||
channel_name TEXT 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);
|
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,
|
scn_message_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
sender_user_id INTEGER NOT NULL,
|
sender_user_id INTEGER NOT NULL,
|
||||||
|
owner_user_id INTEGER NOT NULL,
|
||||||
channel_name TEXT NOT NULL,
|
channel_name TEXT NOT NULL,
|
||||||
|
|
||||||
channel_id INTEGER NOT NULL,
|
channel_id INTEGER NOT NULL,
|
||||||
|
|
||||||
timestamp_real 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,
|
priority INTEGER CHECK(priority IN (0, 1, 2)) NOT NULL,
|
||||||
usr_message_id TEXT NULL
|
usr_message_id TEXT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX "idx_messages_channel" ON messages (sender_user_id, channel_name);
|
CREATE INDEX "idx_messages_channel" ON messages (owner_user_id, channel_name);
|
||||||
CREATE INDEX "idx_messages_idempotency" ON messages (sender_user_id, usr_message_id);
|
CREATE INDEX "idx_messages_idempotency" ON messages (owner_user_id, usr_message_id);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE deliveries
|
CREATE TABLE deliveries
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func bool2DB(b bool) int {
|
func bool2DB(b bool) int {
|
||||||
if b {
|
if b {
|
||||||
@ -13,3 +16,10 @@ func bool2DB(b bool) int {
|
|||||||
func time2DB(t time.Time) int64 {
|
func time2DB(t time.Time) int64 {
|
||||||
return t.UnixMilli()
|
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
|
package firebase
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
fb "firebase.google.com/go"
|
fb "firebase.google.com/go"
|
||||||
"firebase.google.com/go/messaging"
|
"firebase.google.com/go/messaging"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed scnserviceaccountkey.json
|
//go:embed scnserviceaccountkey.json
|
||||||
@ -43,26 +46,26 @@ type Notification struct {
|
|||||||
Priority int
|
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{
|
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{
|
Notification: &messaging.Notification{
|
||||||
Title: notification.Title,
|
Title: msg.Title,
|
||||||
Body: notification.Body,
|
Body: langext.Coalesce(msg.Content, ""),
|
||||||
},
|
},
|
||||||
Android: nil,
|
Android: nil,
|
||||||
APNS: nil,
|
APNS: nil,
|
||||||
Webpush: nil,
|
Webpush: nil,
|
||||||
FCMOptions: nil,
|
FCMOptions: nil,
|
||||||
Token: notification.Token,
|
Token: *client.FCMToken,
|
||||||
Topic: "",
|
Topic: "",
|
||||||
Condition: "",
|
Condition: "",
|
||||||
}
|
}
|
||||||
if notification.Platform == "ios" {
|
if client.Type == models.ClientTypeIOS {
|
||||||
n.APNS = nil
|
n.APNS = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if notification.Platform == "android" {
|
if client.Type == models.ClientTypeAndroid {
|
||||||
n.Android = nil
|
n.Android = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ require (
|
|||||||
github.com/mattn/go-sqlite3 v1.14.16
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
github.com/rs/zerolog v1.28.0
|
github.com/rs/zerolog v1.28.0
|
||||||
github.com/swaggo/swag v1.8.7
|
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
|
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=
|
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 h1:jsfbvII7aa0SH9qY0fnXBdtNnQe1YY3DgXDThEwLICc=
|
||||||
gogs.mikescher.com/BlackForestBytes/goext v0.0.17/go.mod h1:TMBOjo3FRFh/GiTT0z3nwLmgcFJB87oSF2VMs4XUCTQ=
|
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-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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||||
"blackforestbytes.com/simplecloudnotifier/db"
|
"blackforestbytes.com/simplecloudnotifier/db"
|
||||||
"blackforestbytes.com/simplecloudnotifier/firebase"
|
"blackforestbytes.com/simplecloudnotifier/firebase"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/models"
|
||||||
"context"
|
"context"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
@ -174,3 +175,31 @@ func (app *Application) getPermissions(ctx *AppContext, hdr string) (PermissionS
|
|||||||
|
|
||||||
return NewEmptyPermissions(), nil
|
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"
|
"database/sql"
|
||||||
"github.com/blockloop/scan"
|
"github.com/blockloop/scan"
|
||||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/timeext"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -17,8 +18,8 @@ type User struct {
|
|||||||
TimestampLastRead *time.Time
|
TimestampLastRead *time.Time
|
||||||
TimestampLastSent *time.Time
|
TimestampLastSent *time.Time
|
||||||
MessagesSent int
|
MessagesSent int
|
||||||
QuotaToday int
|
QuotaUsed int
|
||||||
QuotaDay *string
|
QuotaUsedDay *string
|
||||||
IsPro bool
|
IsPro bool
|
||||||
ProToken *string
|
ProToken *string
|
||||||
}
|
}
|
||||||
@ -34,12 +35,41 @@ func (u User) JSON() UserJSON {
|
|||||||
TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano),
|
TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano),
|
||||||
TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano),
|
TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano),
|
||||||
MessagesSent: u.MessagesSent,
|
MessagesSent: u.MessagesSent,
|
||||||
QuotaToday: u.QuotaToday,
|
QuotaUsed: u.QuotaUsed,
|
||||||
QuotaDay: u.QuotaDay,
|
QuotaUsedDay: u.QuotaUsedDay,
|
||||||
IsPro: u.IsPro,
|
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 {
|
type UserJSON struct {
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
@ -50,8 +80,8 @@ type UserJSON struct {
|
|||||||
TimestampLastRead *string `json:"timestamp_last_read"`
|
TimestampLastRead *string `json:"timestamp_last_read"`
|
||||||
TimestampLastSent *string `json:"timestamp_last_sent"`
|
TimestampLastSent *string `json:"timestamp_last_sent"`
|
||||||
MessagesSent int `json:"messages_sent"`
|
MessagesSent int `json:"messages_sent"`
|
||||||
QuotaToday int `json:"quota_today"`
|
QuotaUsed int `json:"quota_used"`
|
||||||
QuotaDay *string `json:"quota_day"`
|
QuotaUsedDay *string `json:"quota_used_day"`
|
||||||
IsPro bool `json:"is_pro"`
|
IsPro bool `json:"is_pro"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,8 +95,8 @@ type UserDB struct {
|
|||||||
TimestampLastRead *int64 `db:"timestamp_lastread"`
|
TimestampLastRead *int64 `db:"timestamp_lastread"`
|
||||||
TimestampLastSent *int64 `db:"timestamp_lastsent"`
|
TimestampLastSent *int64 `db:"timestamp_lastsent"`
|
||||||
MessagesSent int `db:"messages_sent"`
|
MessagesSent int `db:"messages_sent"`
|
||||||
QuotaToday int `db:"quota_today"`
|
QuotaUsed int `db:"quota_used"`
|
||||||
QuotaDay *string `db:"quota_day"`
|
QuotaUsedDay *string `db:"quota_used_day"`
|
||||||
IsPro bool `db:"is_pro"`
|
IsPro bool `db:"is_pro"`
|
||||||
ProToken *string `db:"pro_token"`
|
ProToken *string `db:"pro_token"`
|
||||||
}
|
}
|
||||||
@ -82,8 +112,8 @@ func (u UserDB) Model() User {
|
|||||||
TimestampLastRead: timeOptFromMilli(u.TimestampLastRead),
|
TimestampLastRead: timeOptFromMilli(u.TimestampLastRead),
|
||||||
TimestampLastSent: timeOptFromMilli(u.TimestampLastSent),
|
TimestampLastSent: timeOptFromMilli(u.TimestampLastSent),
|
||||||
MessagesSent: u.MessagesSent,
|
MessagesSent: u.MessagesSent,
|
||||||
QuotaToday: u.QuotaToday,
|
QuotaUsed: u.QuotaUsed,
|
||||||
QuotaDay: u.QuotaDay,
|
QuotaUsedDay: u.QuotaUsedDay,
|
||||||
IsPro: u.IsPro,
|
IsPro: u.IsPro,
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,6 +9,88 @@
|
|||||||
"host": "scn.blackforestbytes.com",
|
"host": "scn.blackforestbytes.com",
|
||||||
"basePath": "/",
|
"basePath": "/",
|
||||||
"paths": {
|
"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/": {
|
"/api-v2/user/": {
|
||||||
"post": {
|
"post": {
|
||||||
"summary": "Create a new user",
|
"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": {
|
"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": {
|
"handler.Update.response": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -132,6 +132,23 @@ definitions:
|
|||||||
success:
|
success:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
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:
|
handler.Update.response:
|
||||||
properties:
|
properties:
|
||||||
is_pro:
|
is_pro:
|
||||||
@ -272,6 +289,57 @@ info:
|
|||||||
title: SimpleCloudNotifier API
|
title: SimpleCloudNotifier API
|
||||||
version: "2.0"
|
version: "2.0"
|
||||||
paths:
|
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/:
|
/api-v2/user/:
|
||||||
post:
|
post:
|
||||||
operationId: api-user-create
|
operationId: api-user-create
|
||||||
@ -720,4 +788,55 @@ paths:
|
|||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.apiError'
|
$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"
|
swagger: "2.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user