POST:/users
works
This commit is contained in:
parent
34a27d9ca4
commit
5991631bfa
2
server/.idea/dataSources.xml
generated
2
server/.idea/dataSources.xml
generated
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
<data-source source="LOCAL" name="DB" uuid="9efcb84c-0b66-4a1f-9c98-f11a17482c42">
|
<data-source source="LOCAL" name="DataSource" uuid="4345c0f7-e4f6-49f3-8694-3827ae63cc61">
|
||||||
<driver-ref>sqlite.xerial</driver-ref>
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
<synchronize>true</synchronize>
|
<synchronize>true</synchronize>
|
||||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
6
server/.idea/sqldialects.xml
generated
6
server/.idea/sqldialects.xml
generated
@ -1,7 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="SqlDialectMappings">
|
<component name="SqlDialectMappings">
|
||||||
<file url="file://$PROJECT_DIR$/db/schema_2.0.sql" dialect="SQLite" />
|
<file url="file://$PROJECT_DIR$/db/schema_3.0.ddl" dialect="SQLite" />
|
||||||
<file url="PROJECT" dialect="SQLite" />
|
<file url="PROJECT" dialect="SQLite" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="SqlResolveMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/db/database.go" scope="{"node":{ "@negative":"1", "group":{ "@kind":"root", "node":{ "name":{ "@qname":"b3228d61-4c36-41ce-803f-63bd80e198b3" }, "group":{ "@kind":"schema", "node":{ "name":{ "@qname":"schema_3.0.ddl" } } } } } }}" />
|
||||||
|
<file url="PROJECT" scope="{"node":{ "@negative":"1", "group":{ "@kind":"root", "node":{ "name":{ "@qname":"b3228d61-4c36-41ce-803f-63bd80e198b3" }, "group":{ "@kind":"schema", "node":{ "name":{ "@qname":"schema_3.0.ddl" } } } } } }}" />
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
@ -5,11 +5,15 @@ type APIError int
|
|||||||
const (
|
const (
|
||||||
NO_ERROR APIError = 0000
|
NO_ERROR APIError = 0000
|
||||||
|
|
||||||
MISSING_UID APIError = 1101
|
MISSING_UID APIError = 1101
|
||||||
MISSING_TOK APIError = 1102
|
MISSING_TOK APIError = 1102
|
||||||
MISSING_TITLE APIError = 1103
|
MISSING_TITLE APIError = 1103
|
||||||
INVALID_PRIO APIError = 1104
|
INVALID_PRIO APIError = 1104
|
||||||
REQ_METHOD APIError = 1105
|
REQ_METHOD APIError = 1105
|
||||||
|
INVALID_CLIENTTYPE APIError = 1106
|
||||||
|
MISSING_QUERY_PARAM APIError = 1151
|
||||||
|
MISSING_BODY_PARAM APIError = 1152
|
||||||
|
MISSING_URI_PARAM APIError = 1153
|
||||||
|
|
||||||
NO_TITLE APIError = 1201
|
NO_TITLE APIError = 1201
|
||||||
TITLE_TOO_LONG APIError = 1202
|
TITLE_TOO_LONG APIError = 1202
|
||||||
@ -24,6 +28,12 @@ const (
|
|||||||
|
|
||||||
QUOTA_REACHED APIError = 2101
|
QUOTA_REACHED APIError = 2101
|
||||||
|
|
||||||
|
FAILED_VERIFY_PRO_TOKEN APIError = 3001
|
||||||
|
INVALID_PRO_TOKEN APIError = 3002
|
||||||
|
|
||||||
|
COMMIT_FAILED = 9001
|
||||||
|
DATABASE_ERROR = 9002
|
||||||
|
|
||||||
FIREBASE_COM_FAILED APIError = 9901
|
FIREBASE_COM_FAILED APIError = 9901
|
||||||
FIREBASE_COM_ERRORED APIError = 9902
|
FIREBASE_COM_ERRORED APIError = 9902
|
||||||
INTERNAL_EXCEPTION APIError = 9903
|
INTERNAL_EXCEPTION APIError = 9903
|
||||||
|
167
server/api/handler/api.go
Normal file
167
server/api/handler/api.go
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/api/models"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIHandler struct {
|
||||||
|
app *logic.Application
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser swaggerdoc
|
||||||
|
//
|
||||||
|
// @Summary Create a new user
|
||||||
|
// @ID api-user-create
|
||||||
|
//
|
||||||
|
// @Param post_body body handler.CreateUser.body false " "
|
||||||
|
//
|
||||||
|
// @Success 200 {object} models.UserJSON
|
||||||
|
// @Failure 400 {object} ginresp.apiError
|
||||||
|
// @Failure 500 {object} ginresp.apiError
|
||||||
|
//
|
||||||
|
// @Router /api-v2/user/ [POST]
|
||||||
|
func (h APIHandler) CreateUser(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
type body struct {
|
||||||
|
FCMToken string `form:"fcm_token"`
|
||||||
|
ProToken *string `form:"pro_token"`
|
||||||
|
Username *string `form:"username"`
|
||||||
|
AgentModel string `form:"agent_model"`
|
||||||
|
AgentVersion string `form:"agent_version"`
|
||||||
|
ClientType string `form:"client_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := h.app.StartRequest(g)
|
||||||
|
defer ctx.Cancel()
|
||||||
|
|
||||||
|
var b body
|
||||||
|
if err := g.ShouldBindJSON(&b); err != nil {
|
||||||
|
return ginresp.InternAPIError(apierr.MISSING_BODY_PARAM, "Failed to read body", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientType models.ClientType
|
||||||
|
if b.ClientType == string(models.ClientTypeAndroid) {
|
||||||
|
clientType = models.ClientTypeAndroid
|
||||||
|
} else if b.ClientType == string(models.ClientTypeIOS) {
|
||||||
|
clientType = models.ClientTypeIOS
|
||||||
|
} else {
|
||||||
|
return ginresp.InternAPIError(apierr.INVALID_CLIENTTYPE, "Invalid ClientType", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.ProToken != nil {
|
||||||
|
ptok, err := h.app.VerifyProToken(*b.ProToken)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.InternAPIError(apierr.FAILED_VERIFY_PRO_TOKEN, "Failed to query purchase status", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ptok {
|
||||||
|
return ginresp.InternAPIError(apierr.INVALID_PRO_TOKEN, "Purchase token could not be verified", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readKey := h.app.GenerateRandomAuthKey()
|
||||||
|
sendKey := h.app.GenerateRandomAuthKey()
|
||||||
|
adminKey := h.app.GenerateRandomAuthKey()
|
||||||
|
|
||||||
|
err := h.app.Database.ClearFCMTokens(ctx, b.FCMToken)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if b.ProToken != nil {
|
||||||
|
err := h.app.Database.ClearProTokens(ctx, b.FCMToken)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to clear existing fcm tokens", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userobj, err := h.app.Database.CreateUser(ctx, readKey, sendKey, adminKey, b.ProToken, b.Username)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.InternAPIError(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)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.InternAPIError(apierr.DATABASE_ERROR, "Failed to create user in db", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.FinishSuccess(ginresp.JSON(http.StatusOK, userobj.JSON()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) GetUser(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) UpdateUser(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) ListClients(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) GetClient(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) AddClient(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) DeleteClient(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) ListChannels(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) GetChannel(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) GetChannelMessages(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) ListUserSubscriptions(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) ListChannelSubscriptions(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) GetSubscription(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) CancelSubscription(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) CreateSubscription(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) ListMessages(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) GetMessage(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h APIHandler) DeleteMessage(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIHandler(app *logic.Application) APIHandler {
|
||||||
|
return APIHandler{
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,7 @@ type pingResponseInfo struct {
|
|||||||
// Ping swaggerdoc
|
// Ping swaggerdoc
|
||||||
//
|
//
|
||||||
// @Success 200 {object} pingResponse
|
// @Success 200 {object} pingResponse
|
||||||
// @Failure 500 {object} ginresp.errBody
|
// @Failure 500 {object} ginresp.apiError
|
||||||
// @Router /ping [get]
|
// @Router /ping [get]
|
||||||
// @Router /ping [post]
|
// @Router /ping [post]
|
||||||
// @Router /ping [put]
|
// @Router /ping [put]
|
||||||
@ -60,7 +60,7 @@ func (h CommonHandler) Ping(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// DatabaseTest swaggerdoc
|
// DatabaseTest swaggerdoc
|
||||||
//
|
//
|
||||||
// @Success 200 {object} handler.DatabaseTest.response
|
// @Success 200 {object} handler.DatabaseTest.response
|
||||||
// @Failure 500 {object} ginresp.errBody
|
// @Failure 500 {object} ginresp.apiError
|
||||||
// @Router /db-test [get]
|
// @Router /db-test [get]
|
||||||
func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
|
func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type response struct {
|
type response struct {
|
||||||
@ -88,7 +88,7 @@ func (h CommonHandler) DatabaseTest(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// Health swaggerdoc
|
// Health swaggerdoc
|
||||||
//
|
//
|
||||||
// @Success 200 {object} handler.Health.response
|
// @Success 200 {object} handler.Health.response
|
||||||
// @Failure 500 {object} ginresp.errBody
|
// @Failure 500 {object} ginresp.apiError
|
||||||
// @Router /health [get]
|
// @Router /health [get]
|
||||||
func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse {
|
func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse {
|
||||||
type response struct {
|
type response struct {
|
||||||
@ -96,3 +96,14 @@ func (h CommonHandler) Health(*gin.Context) ginresp.HTTPResponse {
|
|||||||
}
|
}
|
||||||
return ginresp.JSON(http.StatusOK, response{Status: "ok"})
|
return ginresp.JSON(http.StatusOK, response{Status: "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h CommonHandler) NoRoute(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.JSON(http.StatusNotFound, gin.H{
|
||||||
|
"FullPath": g.FullPath(),
|
||||||
|
"Method": g.Request.Method,
|
||||||
|
"URL": g.Request.URL.String(),
|
||||||
|
"RequestURI": g.Request.RequestURI,
|
||||||
|
"Proto": g.Request.Proto,
|
||||||
|
"Header": g.Request.Header,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -1,17 +1,10 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/models"
|
"blackforestbytes.com/simplecloudnotifier/api/models"
|
||||||
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type CompatHandler struct {
|
type CompatHandler struct {
|
||||||
@ -32,8 +25,8 @@ func NewCompatHandler(app *logic.Application) CompatHandler {
|
|||||||
// @Param pro query string true "if the user is a paid account" Enums(true, false)
|
// @Param pro query string true "if the user is a paid account" Enums(true, false)
|
||||||
// @Param pro_token query string true "the (android) IAP token"
|
// @Param pro_token query string true "the (android) IAP token"
|
||||||
// @Success 200 {object} handler.Register.response
|
// @Success 200 {object} handler.Register.response
|
||||||
// @Failure 500 {object} ginresp.internAPIError
|
// @Failure 500 {object} ginresp.apiError
|
||||||
// @Router /register.php [get]
|
// @Router /api/register.php [get]
|
||||||
func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
|
func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type query struct {
|
type query struct {
|
||||||
FCMToken *string `form:"fcm_token"`
|
FCMToken *string `form:"fcm_token"`
|
||||||
@ -50,81 +43,9 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
IsPro int `json:"is_pro"`
|
IsPro int `json:"is_pro"`
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
//TODO
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var q query
|
return ginresp.NotImplemented()
|
||||||
if err := g.ShouldBindQuery(&q); err != nil {
|
|
||||||
return ginresp.InternAPIError(0, "Failed to read arguments")
|
|
||||||
}
|
|
||||||
|
|
||||||
if q.FCMToken == nil {
|
|
||||||
return ginresp.InternAPIError(0, "Missing parameter [[fcm_token]]")
|
|
||||||
}
|
|
||||||
if q.Pro == nil {
|
|
||||||
return ginresp.InternAPIError(0, "Missing parameter [[pro]]")
|
|
||||||
}
|
|
||||||
if q.ProToken == nil {
|
|
||||||
return ginresp.InternAPIError(0, "Missing parameter [[pro_token]]")
|
|
||||||
}
|
|
||||||
|
|
||||||
isProInt := 0
|
|
||||||
isProBool := false
|
|
||||||
if *q.Pro == "true" {
|
|
||||||
isProInt = 1
|
|
||||||
isProBool = true
|
|
||||||
} else {
|
|
||||||
q.ProToken = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if isProBool {
|
|
||||||
ptok, err := h.app.VerifyProToken(*q.ProToken)
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.InternAPIError(0, fmt.Sprintf("Failed to query purchaste status: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ptok {
|
|
||||||
return ginresp.InternAPIError(0, "Purchase token could not be verified")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
userKey := h.app.GenerateRandomAuthKey()
|
|
||||||
|
|
||||||
return h.app.RunTransaction(ctx, nil, func(tx *sql.Tx) (ginresp.HTTPResponse, bool) {
|
|
||||||
|
|
||||||
res, err := tx.ExecContext(ctx, "INSERT INTO users (user_key, fcm_token, is_pro, pro_token, timestamp_accessed) VALUES (?, ?, ?, ?, NOW())", userKey, *q.FCMToken, isProInt, q.ProToken)
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.InternAPIError(0, fmt.Sprintf("Failed to create user: %v", err)), false
|
|
||||||
}
|
|
||||||
|
|
||||||
userId, err := res.LastInsertId()
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.InternAPIError(0, fmt.Sprintf("Failed to get user_id: %v", err)), false
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.ExecContext(ctx, "UPDATE users SET fcm_token=NULL WHERE user_id <> ? AND fcm_token=?", userId, q.FCMToken)
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.InternAPIError(0, fmt.Sprintf("Failed to update fcm: %v", err)), false
|
|
||||||
}
|
|
||||||
|
|
||||||
if isProInt == 1 {
|
|
||||||
_, err := tx.ExecContext(ctx, "UPDATE users SET is_pro=0, pro_token=NULL WHERE user_id <> ? AND pro_token = ?", userId, q.ProToken)
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.InternAPIError(0, fmt.Sprintf("Failed to update ispro: %v", err)), false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ginresp.JSON(http.StatusOK, response{
|
|
||||||
Success: true,
|
|
||||||
Message: "New user registered",
|
|
||||||
UserID: strconv.FormatInt(userId, 10),
|
|
||||||
UserKey: userKey,
|
|
||||||
QuotaUsed: 0,
|
|
||||||
QuotaMax: h.app.QuotaMax(isProBool),
|
|
||||||
IsPro: isProInt,
|
|
||||||
}), true
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info swaggerdoc
|
// Info swaggerdoc
|
||||||
@ -134,8 +55,8 @@ func (h CompatHandler) Register(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// @Param user_id query string true "the user_id"
|
// @Param user_id query string true "the user_id"
|
||||||
// @Param user_key query string true "the user_key"
|
// @Param user_key query string true "the user_key"
|
||||||
// @Success 200 {object} handler.Info.response
|
// @Success 200 {object} handler.Info.response
|
||||||
// @Failure 500 {object} ginresp.internAPIError
|
// @Failure 500 {object} ginresp.apiError
|
||||||
// @Router /info.php [get]
|
// @Router /api/info.php [get]
|
||||||
func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
|
func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type query struct {
|
type query struct {
|
||||||
UserID string `form:"user_id"`
|
UserID string `form:"user_id"`
|
||||||
@ -155,7 +76,7 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
//TODO
|
//TODO
|
||||||
|
|
||||||
return ginresp.InternAPIError(0, "NotImplemented")
|
return ginresp.NotImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ack swaggerdoc
|
// Ack swaggerdoc
|
||||||
@ -166,8 +87,8 @@ func (h CompatHandler) Info(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// @Param user_key query string true "the user_key"
|
// @Param user_key query string true "the user_key"
|
||||||
// @Param scn_msg_id query string true "the message id"
|
// @Param scn_msg_id query string true "the message id"
|
||||||
// @Success 200 {object} handler.Ack.response
|
// @Success 200 {object} handler.Ack.response
|
||||||
// @Failure 500 {object} ginresp.internAPIError
|
// @Failure 500 {object} ginresp.apiError
|
||||||
// @Router /ack.php [get]
|
// @Router /api/ack.php [get]
|
||||||
func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
|
func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type query struct {
|
type query struct {
|
||||||
UserID string `form:"user_id"`
|
UserID string `form:"user_id"`
|
||||||
@ -183,7 +104,7 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
//TODO
|
//TODO
|
||||||
|
|
||||||
return ginresp.InternAPIError(0, "NotImplemented")
|
return ginresp.NotImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Requery swaggerdoc
|
// Requery swaggerdoc
|
||||||
@ -193,8 +114,8 @@ func (h CompatHandler) Ack(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// @Param user_id query string true "the user_id"
|
// @Param user_id query string true "the user_id"
|
||||||
// @Param user_key query string true "the user_key"
|
// @Param user_key query string true "the user_key"
|
||||||
// @Success 200 {object} handler.Requery.response
|
// @Success 200 {object} handler.Requery.response
|
||||||
// @Failure 500 {object} ginresp.internAPIError
|
// @Failure 500 {object} ginresp.apiError
|
||||||
// @Router /requery.php [get]
|
// @Router /api/requery.php [get]
|
||||||
func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
|
func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type query struct {
|
type query struct {
|
||||||
UserID string `form:"user_id"`
|
UserID string `form:"user_id"`
|
||||||
@ -209,7 +130,7 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
//TODO
|
//TODO
|
||||||
|
|
||||||
return ginresp.InternAPIError(0, "NotImplemented")
|
return ginresp.NotImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update swaggerdoc
|
// Update swaggerdoc
|
||||||
@ -220,8 +141,8 @@ func (h CompatHandler) Requery(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// @Param user_key query string true "the user_key"
|
// @Param user_key query string true "the user_key"
|
||||||
// @Param fcm_token query string true "the (android) fcm token"
|
// @Param fcm_token query string true "the (android) fcm token"
|
||||||
// @Success 200 {object} handler.Update.response
|
// @Success 200 {object} handler.Update.response
|
||||||
// @Failure 500 {object} ginresp.internAPIError
|
// @Failure 500 {object} ginresp.apiError
|
||||||
// @Router /update.php [get]
|
// @Router /api/update.php [get]
|
||||||
func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
|
func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type query struct {
|
type query struct {
|
||||||
UserID string `form:"user_id"`
|
UserID string `form:"user_id"`
|
||||||
@ -240,7 +161,7 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
//TODO
|
//TODO
|
||||||
|
|
||||||
return ginresp.InternAPIError(0, "NotImplemented")
|
return ginresp.NotImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand swaggerdoc
|
// Expand swaggerdoc
|
||||||
@ -248,8 +169,8 @@ func (h CompatHandler) Update(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// @Summary Get a whole (potentially truncated) message
|
// @Summary Get a whole (potentially truncated) message
|
||||||
// @ID compat-expand
|
// @ID compat-expand
|
||||||
// @Success 200 {object} handler.Expand.response
|
// @Success 200 {object} handler.Expand.response
|
||||||
// @Failure 500 {object} ginresp.internAPIError
|
// @Failure 500 {object} ginresp.apiError
|
||||||
// @Router /expand.php [get]
|
// @Router /api/expand.php [get]
|
||||||
func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
|
func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type query struct {
|
type query struct {
|
||||||
UserID string `form:"user_id"`
|
UserID string `form:"user_id"`
|
||||||
@ -264,7 +185,7 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
//TODO
|
//TODO
|
||||||
|
|
||||||
return ginresp.InternAPIError(0, "NotImplemented")
|
return ginresp.NotImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade swaggerdoc
|
// Upgrade swaggerdoc
|
||||||
@ -276,8 +197,8 @@ func (h CompatHandler) Expand(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
// @Param pro query string true "if the user is a paid account" Enums(true, false)
|
// @Param pro query string true "if the user is a paid account" Enums(true, false)
|
||||||
// @Param pro_token query string true "the (android) IAP token"
|
// @Param pro_token query string true "the (android) IAP token"
|
||||||
// @Success 200 {object} handler.Upgrade.response
|
// @Success 200 {object} handler.Upgrade.response
|
||||||
// @Failure 500 {object} ginresp.internAPIError
|
// @Failure 500 {object} ginresp.apiError
|
||||||
// @Router /upgrade.php [get]
|
// @Router /api/upgrade.php [get]
|
||||||
func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
|
func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
|
||||||
type query struct {
|
type query struct {
|
||||||
UserID string `form:"user_id"`
|
UserID string `form:"user_id"`
|
||||||
@ -293,47 +214,5 @@ func (h CompatHandler) Upgrade(g *gin.Context) ginresp.HTTPResponse {
|
|||||||
|
|
||||||
//TODO
|
//TODO
|
||||||
|
|
||||||
return ginresp.InternAPIError(0, "NotImplemented")
|
return ginresp.NotImplemented()
|
||||||
}
|
|
||||||
|
|
||||||
// Send swaggerdoc
|
|
||||||
//
|
|
||||||
// @Summary Send a message
|
|
||||||
// @Description (all arguments can either be supplied in the query or in the json body)
|
|
||||||
// @ID compat-send
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param _ query handler.Send.query false " "
|
|
||||||
// @Param post_body body handler.Send.body false " "
|
|
||||||
// @Success 200 {object} handler.Send.response
|
|
||||||
// @Failure 500 {object} ginresp.sendAPIError
|
|
||||||
// @Router /send.php [post]
|
|
||||||
func (h CompatHandler) Send(g *gin.Context) ginresp.HTTPResponse {
|
|
||||||
type query struct {
|
|
||||||
UserID string `form:"user_id" required:"true"`
|
|
||||||
UserKey string `form:"user_key" required:"true"`
|
|
||||||
Title string `form:"title" required:"true"`
|
|
||||||
Content *string `form:"content"`
|
|
||||||
Priority *string `form:"priority"`
|
|
||||||
MessageID *string `form:"msg_id"`
|
|
||||||
Timestamp *string `form:"timestamp"`
|
|
||||||
}
|
|
||||||
type body struct {
|
|
||||||
UserID string `json:"user_id" required:"true"`
|
|
||||||
UserKey string `json:"user_key" required:"true"`
|
|
||||||
Title string `json:"title" required:"true"`
|
|
||||||
Content *string `json:"content"`
|
|
||||||
Priority *string `json:"priority"`
|
|
||||||
MessageID *string `json:"msg_id"`
|
|
||||||
Timestamp *string `json:"timestamp"`
|
|
||||||
}
|
|
||||||
type response struct {
|
|
||||||
Success string `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
//TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO
|
|
||||||
|
|
||||||
return ginresp.SendAPIError(apierr.INTERNAL_EXCEPTION, -1, "NotImplemented")
|
|
||||||
}
|
}
|
||||||
|
21
server/api/handler/message.go
Normal file
21
server/api/handler/message.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MessageHandler struct {
|
||||||
|
app *logic.Application
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h MessageHandler) SendMessage(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return ginresp.NotImplemented()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMessageHandler(app *logic.Application) MessageHandler {
|
||||||
|
return MessageHandler{
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
}
|
98
server/api/handler/website.go
Normal file
98
server/api/handler/website.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/website"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebsiteHandler struct {
|
||||||
|
app *logic.Application
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebsiteHandler(app *logic.Application) WebsiteHandler {
|
||||||
|
return WebsiteHandler{
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebsiteHandler) Index(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return h.serveAsset(g, "index.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebsiteHandler) APIDocs(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return h.serveAsset(g, "api.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebsiteHandler) APIDocsMore(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return h.serveAsset(g, "api_more.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebsiteHandler) MessageSent(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return h.serveAsset(g, "message_sent.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebsiteHandler) FaviconIco(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return h.serveAsset(g, "favicon.ico")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebsiteHandler) FaviconPNG(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
return h.serveAsset(g, "favicon.png")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebsiteHandler) Javascript(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
type uri struct {
|
||||||
|
Filename string `uri:"fn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var u uri
|
||||||
|
if err := g.ShouldBindUri(&u); err != nil {
|
||||||
|
return ginresp.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.serveAsset(g, "js/"+u.Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebsiteHandler) CSS(g *gin.Context) ginresp.HTTPResponse {
|
||||||
|
type uri struct {
|
||||||
|
Filename string `uri:"fn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var u uri
|
||||||
|
if err := g.ShouldBindUri(&u); err != nil {
|
||||||
|
return ginresp.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.serveAsset(g, "css/"+u.Filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h WebsiteHandler) serveAsset(g *gin.Context, fn string) ginresp.HTTPResponse {
|
||||||
|
data, err := website.Assets.ReadFile(fn)
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.Status(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
mime := "text/plain"
|
||||||
|
|
||||||
|
lowerFN := strings.ToLower(fn)
|
||||||
|
if strings.HasSuffix(lowerFN, ".html") || strings.HasSuffix(lowerFN, ".htm") {
|
||||||
|
mime = "text/html"
|
||||||
|
} else if strings.HasSuffix(lowerFN, ".css") {
|
||||||
|
mime = "text/css"
|
||||||
|
} else if strings.HasSuffix(lowerFN, ".js") {
|
||||||
|
mime = "text/javascript"
|
||||||
|
} else if strings.HasSuffix(lowerFN, ".json") {
|
||||||
|
mime = "application/json"
|
||||||
|
} else if strings.HasSuffix(lowerFN, ".jpeg") || strings.HasSuffix(lowerFN, ".jpg") {
|
||||||
|
mime = "image/jpeg"
|
||||||
|
} else if strings.HasSuffix(lowerFN, ".png") {
|
||||||
|
mime = "image/png"
|
||||||
|
} else if strings.HasSuffix(lowerFN, ".svg") {
|
||||||
|
mime = "image/svg+xml"
|
||||||
|
}
|
||||||
|
|
||||||
|
return ginresp.Data(http.StatusOK, mime, data)
|
||||||
|
}
|
23
server/api/models/client.go
Normal file
23
server/api/models/client.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type ClientType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ClientTypeAndroid ClientType = "ANDROID"
|
||||||
|
ClientTypeIOS ClientType = "IOS"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
ClientID int64
|
||||||
|
UserID int64
|
||||||
|
Type ClientType
|
||||||
|
FCMToken *string
|
||||||
|
TimestampCreated time.Time
|
||||||
|
AgentModel string
|
||||||
|
AgentVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientJSON struct {
|
||||||
|
}
|
51
server/api/models/user.go
Normal file
51
server/api/models/user.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
UserID int64
|
||||||
|
Username *string
|
||||||
|
ReadKey string
|
||||||
|
SendKey string
|
||||||
|
AdminKey string
|
||||||
|
TimestampCreated time.Time
|
||||||
|
TimestampLastRead *time.Time
|
||||||
|
TimestampLastSent *time.Time
|
||||||
|
MessagesSent int
|
||||||
|
QuotaToday int
|
||||||
|
QuotaDay *string
|
||||||
|
IsPro bool
|
||||||
|
ProToken *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u User) JSON() UserJSON {
|
||||||
|
return UserJSON{
|
||||||
|
UserID: u.UserID,
|
||||||
|
Username: u.Username,
|
||||||
|
ReadKey: u.ReadKey,
|
||||||
|
SendKey: u.SendKey,
|
||||||
|
AdminKey: u.AdminKey,
|
||||||
|
TimestampCreated: u.TimestampCreated.Format(time.RFC3339Nano),
|
||||||
|
TimestampLastRead: timeOptFmt(u.TimestampLastRead, time.RFC3339Nano),
|
||||||
|
TimestampLastSent: timeOptFmt(u.TimestampLastSent, time.RFC3339Nano),
|
||||||
|
MessagesSent: u.MessagesSent,
|
||||||
|
QuotaToday: u.QuotaToday,
|
||||||
|
QuotaDay: u.QuotaDay,
|
||||||
|
IsPro: u.IsPro,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserJSON struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
Username *string `json:"username"`
|
||||||
|
ReadKey string `json:"read_key"`
|
||||||
|
SendKey string `json:"send_key"`
|
||||||
|
AdminKey string `json:"admin_key"`
|
||||||
|
TimestampCreated string `json:"timestamp_created"`
|
||||||
|
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"`
|
||||||
|
IsPro bool `json:"is_pro"`
|
||||||
|
}
|
14
server/api/models/utils.go
Normal file
14
server/api/models/utils.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func timeOptFmt(t *time.Time, fmt string) *string {
|
||||||
|
if t == nil {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return langext.Ptr(t.Format(fmt))
|
||||||
|
}
|
||||||
|
}
|
@ -12,16 +12,22 @@ import (
|
|||||||
type Router struct {
|
type Router struct {
|
||||||
app *logic.Application
|
app *logic.Application
|
||||||
|
|
||||||
commonHandler handler.CommonHandler
|
commonHandler handler.CommonHandler
|
||||||
compatHandler handler.CompatHandler
|
compatHandler handler.CompatHandler
|
||||||
|
websiteHandler handler.WebsiteHandler
|
||||||
|
apiHandler handler.APIHandler
|
||||||
|
messageHandler handler.MessageHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRouter(app *logic.Application) *Router {
|
func NewRouter(app *logic.Application) *Router {
|
||||||
return &Router{
|
return &Router{
|
||||||
app: app,
|
app: app,
|
||||||
|
|
||||||
commonHandler: handler.NewCommonHandler(app),
|
commonHandler: handler.NewCommonHandler(app),
|
||||||
compatHandler: handler.NewCompatHandler(app),
|
compatHandler: handler.NewCompatHandler(app),
|
||||||
|
websiteHandler: handler.NewWebsiteHandler(app),
|
||||||
|
apiHandler: handler.NewAPIHandler(app),
|
||||||
|
messageHandler: handler.NewMessageHandler(app),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,23 +36,101 @@ func NewRouter(app *logic.Application) *Router {
|
|||||||
// @version 2.0
|
// @version 2.0
|
||||||
// @description API for SCN
|
// @description API for SCN
|
||||||
// @host scn.blackforestbytes.com
|
// @host scn.blackforestbytes.com
|
||||||
// @BasePath /api/
|
// @BasePath /
|
||||||
func (r *Router) Init(e *gin.Engine) {
|
func (r *Router) Init(e *gin.Engine) {
|
||||||
|
|
||||||
e.Any("/ping", ginresp.Wrap(r.commonHandler.Ping))
|
// ================ General ================
|
||||||
e.POST("/db-test", ginresp.Wrap(r.commonHandler.DatabaseTest))
|
|
||||||
e.GET("/health", ginresp.Wrap(r.commonHandler.Health))
|
|
||||||
|
|
||||||
e.GET("documentation/swagger", ginext.RedirectTemporary("/documentation/swagger/"))
|
e.Any("/api/common/ping", ginresp.Wrap(r.commonHandler.Ping))
|
||||||
e.GET("documentation/swagger/", ginresp.Wrap(swagger.Handle))
|
e.POST("/api/common/db-test", ginresp.Wrap(r.commonHandler.DatabaseTest))
|
||||||
e.GET("documentation/swagger/:fn", ginresp.Wrap(swagger.Handle))
|
e.GET("/api/common/health", ginresp.Wrap(r.commonHandler.Health))
|
||||||
|
|
||||||
|
// ================ Swagger ================
|
||||||
|
|
||||||
|
e.GET("/documentation/swagger", ginext.RedirectTemporary("/documentation/swagger/"))
|
||||||
|
e.GET("/documentation/swagger/", ginresp.Wrap(swagger.Handle))
|
||||||
|
e.GET("/documentation/swagger/:fn", ginresp.Wrap(swagger.Handle))
|
||||||
|
|
||||||
|
// ================ Website ================
|
||||||
|
|
||||||
|
e.GET("/", ginresp.Wrap(r.websiteHandler.Index))
|
||||||
|
e.GET("/index.php", ginresp.Wrap(r.websiteHandler.Index))
|
||||||
|
e.GET("/index.html", ginresp.Wrap(r.websiteHandler.Index))
|
||||||
|
e.GET("/index", ginresp.Wrap(r.websiteHandler.Index))
|
||||||
|
|
||||||
|
e.GET("/api", ginresp.Wrap(r.websiteHandler.APIDocs))
|
||||||
|
e.GET("/api.php", ginresp.Wrap(r.websiteHandler.APIDocs))
|
||||||
|
e.GET("/api.html", ginresp.Wrap(r.websiteHandler.APIDocs))
|
||||||
|
|
||||||
|
e.GET("/api_more", ginresp.Wrap(r.websiteHandler.APIDocsMore))
|
||||||
|
e.GET("/api_more.php", ginresp.Wrap(r.websiteHandler.APIDocsMore))
|
||||||
|
e.GET("/api_more.html", ginresp.Wrap(r.websiteHandler.APIDocsMore))
|
||||||
|
|
||||||
|
e.GET("/message_sent", ginresp.Wrap(r.websiteHandler.MessageSent))
|
||||||
|
e.GET("/message_sent.php", ginresp.Wrap(r.websiteHandler.MessageSent))
|
||||||
|
e.GET("/message_sent.html", ginresp.Wrap(r.websiteHandler.MessageSent))
|
||||||
|
|
||||||
|
e.GET("/favicon.ico", ginresp.Wrap(r.websiteHandler.FaviconIco))
|
||||||
|
e.GET("/favicon.png", ginresp.Wrap(r.websiteHandler.FaviconPNG))
|
||||||
|
|
||||||
|
e.GET("/js/:fn", ginresp.Wrap(r.websiteHandler.Javascript))
|
||||||
|
e.GET("/css/:fn", ginresp.Wrap(r.websiteHandler.CSS))
|
||||||
|
|
||||||
|
// ================ Compat (v1) ================
|
||||||
|
|
||||||
|
compat := e.Group("/api/")
|
||||||
|
{
|
||||||
|
compat.GET("/register.php", ginresp.Wrap(r.compatHandler.Register))
|
||||||
|
compat.GET("/info.php", ginresp.Wrap(r.compatHandler.Info))
|
||||||
|
compat.GET("/ack.php", ginresp.Wrap(r.compatHandler.Ack))
|
||||||
|
compat.GET("/requery.php", ginresp.Wrap(r.compatHandler.Requery))
|
||||||
|
compat.GET("/update.php", ginresp.Wrap(r.compatHandler.Update))
|
||||||
|
compat.GET("/expand.php", ginresp.Wrap(r.compatHandler.Expand))
|
||||||
|
compat.GET("/upgrade.php", ginresp.Wrap(r.compatHandler.Upgrade))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================ Manage API ================
|
||||||
|
|
||||||
|
apiv2 := e.Group("/api-v2/")
|
||||||
|
{
|
||||||
|
|
||||||
|
apiv2.POST("/user/", ginresp.Wrap(r.apiHandler.CreateUser))
|
||||||
|
apiv2.GET("/user/:uid", ginresp.Wrap(r.apiHandler.GetUser))
|
||||||
|
apiv2.PATCH("/user/:uid", ginresp.Wrap(r.apiHandler.UpdateUser))
|
||||||
|
|
||||||
|
apiv2.GET("/user/:uid/clients", ginresp.Wrap(r.apiHandler.ListClients))
|
||||||
|
apiv2.GET("/user/:uid/clients/:cid", ginresp.Wrap(r.apiHandler.GetClient))
|
||||||
|
apiv2.POST("/user/:uid/clients", ginresp.Wrap(r.apiHandler.AddClient))
|
||||||
|
apiv2.DELETE("/user/:uid/clients", ginresp.Wrap(r.apiHandler.DeleteClient))
|
||||||
|
|
||||||
|
apiv2.GET("/user/:uid/channels", ginresp.Wrap(r.apiHandler.ListChannels))
|
||||||
|
apiv2.GET("/user/:uid/channels/:cid", ginresp.Wrap(r.apiHandler.GetChannel))
|
||||||
|
apiv2.GET("/user/:uid/channels/:cid/messages", ginresp.Wrap(r.apiHandler.GetChannelMessages))
|
||||||
|
apiv2.GET("/user/:uid/channels/:cid/subscriptions", ginresp.Wrap(r.apiHandler.ListChannelSubscriptions))
|
||||||
|
|
||||||
|
apiv2.GET("/user/:uid/subscriptions", ginresp.Wrap(r.apiHandler.ListUserSubscriptions))
|
||||||
|
apiv2.GET("/user/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.GetSubscription))
|
||||||
|
apiv2.DELETE("/user/:uid/subscriptions/:sid", ginresp.Wrap(r.apiHandler.CancelSubscription))
|
||||||
|
apiv2.POST("/user/:uid/subscriptions", ginresp.Wrap(r.apiHandler.CreateSubscription))
|
||||||
|
|
||||||
|
apiv2.GET("/messages", ginresp.Wrap(r.apiHandler.ListMessages))
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================ Send API ================
|
||||||
|
|
||||||
|
sendAPI := e.Group("")
|
||||||
|
{
|
||||||
|
sendAPI.POST("/", ginresp.Wrap(r.messageHandler.SendMessage))
|
||||||
|
sendAPI.POST("/send", ginresp.Wrap(r.messageHandler.SendMessage))
|
||||||
|
sendAPI.POST("/send.php")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.app.Config.ReturnRawErrors {
|
||||||
|
e.NoRoute(ginresp.Wrap(r.commonHandler.NoRoute))
|
||||||
|
}
|
||||||
|
|
||||||
e.POST("/send.php", ginresp.Wrap(r.compatHandler.Send))
|
|
||||||
e.GET("/register.php", ginresp.Wrap(r.compatHandler.Register))
|
|
||||||
e.GET("/info.php", ginresp.Wrap(r.compatHandler.Info))
|
|
||||||
e.GET("/ack.php", ginresp.Wrap(r.compatHandler.Ack))
|
|
||||||
e.GET("/requery.php", ginresp.Wrap(r.compatHandler.Requery))
|
|
||||||
e.GET("/update.php", ginresp.Wrap(r.compatHandler.Update))
|
|
||||||
e.GET("/expand.php", ginresp.Wrap(r.compatHandler.Expand))
|
|
||||||
e.GET("/upgrade.php", ginresp.Wrap(r.compatHandler.Upgrade))
|
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"blackforestbytes.com/simplecloudnotifier/common/ginext"
|
"blackforestbytes.com/simplecloudnotifier/common/ginext"
|
||||||
"blackforestbytes.com/simplecloudnotifier/db"
|
"blackforestbytes.com/simplecloudnotifier/db"
|
||||||
"blackforestbytes.com/simplecloudnotifier/logic"
|
"blackforestbytes.com/simplecloudnotifier/logic"
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
@ -19,13 +18,17 @@ func main() {
|
|||||||
|
|
||||||
log.Info().Msg(fmt.Sprintf("Starting with config-namespace <%s>", conf.Namespace))
|
log.Info().Msg(fmt.Sprintf("Starting with config-namespace <%s>", conf.Namespace))
|
||||||
|
|
||||||
sqlite, err := db.NewDatabase(context.Background(), conf)
|
sqlite, err := db.NewDatabase(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app := logic.NewApp(sqlite)
|
app := logic.NewApp(sqlite)
|
||||||
|
|
||||||
|
if err := app.Migrate(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
ginengine := ginext.NewEngine(conf)
|
ginengine := ginext.NewEngine(conf)
|
||||||
|
|
||||||
router := api.NewRouter(app)
|
router := api.NewRouter(app)
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
package ginresp
|
package ginresp
|
||||||
|
|
||||||
type sendAPIError struct {
|
type apiError struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Error int `json:"error"`
|
Error int `json:"error"`
|
||||||
ErrorHighlight int `json:"errhighlight"`
|
ErrorHighlight int `json:"errhighlight"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
RawError error `json:"errorObject,omitempty"`
|
||||||
|
|
||||||
type internAPIError struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
ErrorID int `json:"errid,omitempty"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
package ginresp
|
|
||||||
|
|
||||||
type errBody struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
@ -1,13 +1,14 @@
|
|||||||
package ginresp
|
package ginresp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
scn "blackforestbytes.com/simplecloudnotifier"
|
||||||
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HTTPResponse interface {
|
type HTTPResponse interface {
|
||||||
Write(context *gin.Context)
|
Write(g *gin.Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsonHTTPResponse struct {
|
type jsonHTTPResponse struct {
|
||||||
@ -73,13 +74,21 @@ func Text(sc int, data string) HTTPResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func InternalError(e error) HTTPResponse {
|
func InternalError(e error) HTTPResponse {
|
||||||
return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: errBody{Success: false, Message: e.Error()}}
|
return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(apierr.INTERNAL_EXCEPTION), Message: e.Error()}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func InternAPIError(errid int, msg string) HTTPResponse {
|
func InternAPIError(errorid apierr.APIError, msg string, e error) HTTPResponse {
|
||||||
return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: internAPIError{Success: false, ErrorID: errid, Message: msg}}
|
if scn.Conf.ReturnRawErrors {
|
||||||
|
return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), Message: msg, RawError: e}}
|
||||||
|
} else {
|
||||||
|
return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), Message: msg}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendAPIError(errorid apierr.APIError, highlight int, msg string) HTTPResponse {
|
func SendAPIError(errorid apierr.APIError, highlight int, msg string) HTTPResponse {
|
||||||
return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: sendAPIError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}}
|
return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: int(errorid), ErrorHighlight: highlight, Message: msg}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotImplemented() HTTPResponse {
|
||||||
|
return &errHTTPResponse{statusCode: http.StatusInternalServerError, data: apiError{Success: false, Error: -1, ErrorHighlight: 0, Message: "Not Implemented"}}
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,15 @@ type WHandlerFunc func(*gin.Context) HTTPResponse
|
|||||||
|
|
||||||
func Wrap(fn WHandlerFunc) gin.HandlerFunc {
|
func Wrap(fn WHandlerFunc) gin.HandlerFunc {
|
||||||
|
|
||||||
return func(context *gin.Context) {
|
return func(g *gin.Context) {
|
||||||
|
|
||||||
wrap := fn(context)
|
wrap := fn(g)
|
||||||
|
|
||||||
if context.Writer.Written() {
|
if g.Writer.Written() {
|
||||||
panic("Writing in WrapperFunc is not supported")
|
panic("Writing in WrapperFunc is not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
wrap.Write(context)
|
wrap.Write(g)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,48 +3,59 @@ package server
|
|||||||
import (
|
import (
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Namespace string
|
Namespace string
|
||||||
GinDebug bool
|
GinDebug bool
|
||||||
ServerIP string
|
ServerIP string
|
||||||
ServerPort string
|
ServerPort string
|
||||||
DBFile string
|
DBFile string
|
||||||
|
RequestTimeout time.Duration
|
||||||
|
ReturnRawErrors bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var Conf Config
|
var Conf Config
|
||||||
|
|
||||||
var configLoc = Config{
|
var configLoc = Config{
|
||||||
Namespace: "local",
|
Namespace: "local",
|
||||||
GinDebug: true,
|
GinDebug: true,
|
||||||
ServerIP: "0.0.0.0",
|
ServerIP: "0.0.0.0",
|
||||||
ServerPort: "8080",
|
ServerPort: "8080",
|
||||||
DBFile: ".run-data/db.sqlite3",
|
DBFile: ".run-data/db.sqlite3",
|
||||||
|
RequestTimeout: 16 * time.Second,
|
||||||
|
ReturnRawErrors: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
var configDev = Config{
|
var configDev = Config{
|
||||||
Namespace: "develop",
|
Namespace: "develop",
|
||||||
GinDebug: true,
|
GinDebug: true,
|
||||||
ServerIP: "0.0.0.0",
|
ServerIP: "0.0.0.0",
|
||||||
ServerPort: "80",
|
ServerPort: "80",
|
||||||
DBFile: "/data/scn.sqlite3",
|
DBFile: "/data/scn.sqlite3",
|
||||||
|
RequestTimeout: 16 * time.Second,
|
||||||
|
ReturnRawErrors: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
var configStag = Config{
|
var configStag = Config{
|
||||||
Namespace: "staging",
|
Namespace: "staging",
|
||||||
GinDebug: true,
|
GinDebug: true,
|
||||||
ServerIP: "0.0.0.0",
|
ServerIP: "0.0.0.0",
|
||||||
ServerPort: "80",
|
ServerPort: "80",
|
||||||
DBFile: "/data/scn.sqlite3",
|
DBFile: "/data/scn.sqlite3",
|
||||||
|
RequestTimeout: 16 * time.Second,
|
||||||
|
ReturnRawErrors: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
var configProd = Config{
|
var configProd = Config{
|
||||||
Namespace: "production",
|
Namespace: "production",
|
||||||
GinDebug: false,
|
GinDebug: false,
|
||||||
ServerIP: "0.0.0.0",
|
ServerIP: "0.0.0.0",
|
||||||
ServerPort: "80",
|
ServerPort: "80",
|
||||||
DBFile: "/data/scn.sqlite3",
|
DBFile: "/data/scn.sqlite3",
|
||||||
|
RequestTimeout: 16 * time.Second,
|
||||||
|
ReturnRawErrors: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
var allConfig = []Config{
|
var allConfig = []Config{
|
||||||
|
15
server/db/context.go
Normal file
15
server/db/context.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TxContext interface {
|
||||||
|
Deadline() (deadline time.Time, ok bool)
|
||||||
|
Done() <-chan struct{}
|
||||||
|
Err() error
|
||||||
|
Value(key any) any
|
||||||
|
|
||||||
|
GetOrCreateTransaction(db *Database) (*sql.Tx, error)
|
||||||
|
}
|
@ -8,43 +8,60 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed schema_1.0.sql
|
//go:embed schema_1.0.ddl
|
||||||
var schema_1_0 string
|
var schema_1_0 string
|
||||||
|
|
||||||
//go:embed schema_2.0.sql
|
//go:embed schema_2.0.ddl
|
||||||
var schema_2_0 string
|
var schema_2_0 string
|
||||||
|
|
||||||
func NewDatabase(ctx context.Context, conf scn.Config) (*sql.DB, error) {
|
//go:embed schema_3.0.ddl
|
||||||
|
var schema_3_0 string
|
||||||
|
|
||||||
|
type Database struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDatabase(conf scn.Config) (*Database, error) {
|
||||||
db, err := sql.Open("sqlite3", conf.DBFile)
|
db, err := sql.Open("sqlite3", conf.DBFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
schema, err := getSchemaFromDB(ctx, db)
|
return &Database{db}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) Migrate(ctx context.Context) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
schema, err := db.ReadSchema(ctx)
|
||||||
if schema == 0 {
|
if schema == 0 {
|
||||||
|
|
||||||
_, err = db.ExecContext(ctx, schema_1_0)
|
_, err = db.db.ExecContext(ctx, schema_3_0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return db, nil
|
return nil
|
||||||
|
|
||||||
} else if schema == 1 {
|
} else if schema == 1 {
|
||||||
return nil, errors.New("cannot autom. upgrade schema 1")
|
return errors.New("cannot autom. upgrade schema 1")
|
||||||
} else if schema == 2 {
|
} else if schema == 2 {
|
||||||
return db, nil
|
return errors.New("cannot autom. upgrade schema 2") //TODO
|
||||||
|
} else if schema == 3 {
|
||||||
|
return nil // current
|
||||||
} else {
|
} else {
|
||||||
return nil, errors.New(fmt.Sprintf("Unknown DB schema: %d", schema))
|
return errors.New(fmt.Sprintf("Unknown DB schema: %d", schema))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSchemaFromDB(ctx context.Context, db *sql.DB) (int, error) {
|
func (db *Database) ReadSchema(ctx context.Context) (int, error) {
|
||||||
|
|
||||||
r1, err := db.QueryContext(ctx, "SELECT name FROM sqlite_master WHERE type='table' AND name='meta'")
|
r1, err := db.db.QueryContext(ctx, "SELECT name FROM sqlite_master WHERE type='table' AND name='meta'")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@ -53,7 +70,7 @@ func getSchemaFromDB(ctx context.Context, db *sql.DB) (int, error) {
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
r2, err := db.QueryContext(ctx, "SELECT value_int FROM meta WHERE key='schema'")
|
r2, err := db.db.QueryContext(ctx, "SELECT value_int FROM meta WHERE meta_key='schema'")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@ -69,3 +86,11 @@ func getSchemaFromDB(ctx context.Context, db *sql.DB) (int, error) {
|
|||||||
|
|
||||||
return schema, nil
|
return schema, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *Database) Ping() error {
|
||||||
|
return db.db.Ping()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) BeginTx(ctx context.Context) (*sql.Tx, error) {
|
||||||
|
return db.db.BeginTx(ctx, nil)
|
||||||
|
}
|
||||||
|
112
server/db/methods.go
Normal file
112
server/db/methods.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/api/models"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (db *Database) CreateUser(ctx TxContext, readKey string, sendKey string, adminKey string, protoken *string, username *string) (models.User, error) {
|
||||||
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
|
if err != nil {
|
||||||
|
return models.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
res, err := tx.ExecContext(ctx, "INSERT INTO users (username, read_key, send_key, admin_key, is_pro, pro_token, timestamp_created) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
username,
|
||||||
|
readKey,
|
||||||
|
sendKey,
|
||||||
|
adminKey,
|
||||||
|
bool2DB(protoken != nil),
|
||||||
|
protoken,
|
||||||
|
time2DB(now))
|
||||||
|
if err != nil {
|
||||||
|
return models.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
liid, err := res.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return models.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.User{
|
||||||
|
UserID: liid,
|
||||||
|
Username: username,
|
||||||
|
ReadKey: readKey,
|
||||||
|
SendKey: sendKey,
|
||||||
|
AdminKey: adminKey,
|
||||||
|
TimestampCreated: now,
|
||||||
|
TimestampLastRead: nil,
|
||||||
|
TimestampLastSent: nil,
|
||||||
|
MessagesSent: 0,
|
||||||
|
QuotaToday: 0,
|
||||||
|
QuotaDay: nil,
|
||||||
|
IsPro: protoken != nil,
|
||||||
|
ProToken: protoken,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) CreateClient(ctx TxContext, userid int64, ctype models.ClientType, fcmToken string, agentModel string, agentVersion string) (models.Client, error) {
|
||||||
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
|
if err != nil {
|
||||||
|
return models.Client{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
res, err := tx.ExecContext(ctx, "INSERT INTO clients (user_id, type, fcm_token, timestamp_created, agent_model, agent_version) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
userid,
|
||||||
|
string(ctype),
|
||||||
|
fcmToken,
|
||||||
|
time2DB(now),
|
||||||
|
agentModel,
|
||||||
|
agentVersion)
|
||||||
|
if err != nil {
|
||||||
|
return models.Client{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
liid, err := res.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
return models.Client{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.Client{
|
||||||
|
ClientID: liid,
|
||||||
|
UserID: userid,
|
||||||
|
Type: ctype,
|
||||||
|
FCMToken: langext.Ptr(fcmToken),
|
||||||
|
TimestampCreated: now,
|
||||||
|
AgentModel: agentModel,
|
||||||
|
AgentVersion: agentVersion,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) ClearFCMTokens(ctx TxContext, fcmtoken string) error {
|
||||||
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, "DELETE FROM clients WHERE fcm_token = ?", fcmtoken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *Database) ClearProTokens(ctx TxContext, protoken string) error {
|
||||||
|
tx, err := ctx.GetOrCreateTransaction(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, "UPDATE users SET is_pro=0, pro_token=NULL WHERE pro_token = ?", protoken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -8,9 +8,9 @@ CREATE TABLE users
|
|||||||
send_key TEXT NOT NULL,
|
send_key TEXT NOT NULL,
|
||||||
admin_key TEXT NOT NULL,
|
admin_key TEXT NOT NULL,
|
||||||
|
|
||||||
timestamp_created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
timestamp_created INTEGER NOT NULL,
|
||||||
timestamp_lastread TEXT NULL DEFAULT NULL,
|
timestamp_lastread INTEGER NULL DEFAULT NULL,
|
||||||
timestamp_lastsent TEXT NULL DEFAULT NULL,
|
timestamp_lastsent INTEGER NULL DEFAULT NULL,
|
||||||
|
|
||||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||||
|
|
||||||
@ -18,24 +18,23 @@ CREATE TABLE users
|
|||||||
quota_day TEXT NULL DEFAULT NULL,
|
quota_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
|
||||||
|
|
||||||
PRIMARY KEY (user_id)
|
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX "idx_users_protoken" ON users (pro_token);
|
CREATE UNIQUE INDEX "idx_users_protoken" ON users (pro_token) WHERE pro_token IS NOT NULL;
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE clients
|
CREATE TABLE clients
|
||||||
(
|
(
|
||||||
client_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
client_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|
||||||
user_id INTEGER NOT NULL,
|
user_id INTEGER NOT NULL,
|
||||||
|
type TEXT CHECK(type IN ('ANDROID', 'IOS')) NOT NULL,
|
||||||
|
fcm_token TEXT NULL,
|
||||||
|
|
||||||
type TEXT NOT NULL,
|
timestamp_created INTEGER NOT NULL,
|
||||||
|
|
||||||
fcm_token TEXT NULL,
|
agent_model TEXT NOT NULL,
|
||||||
|
agent_version TEXT NOT NULL
|
||||||
PRIMARY KEY (client_id)
|
|
||||||
);
|
);
|
||||||
CREATE INDEX "idx_clients_userid" ON clients (user_id);
|
CREATE INDEX "idx_clients_userid" ON clients (user_id);
|
||||||
CREATE UNIQUE INDEX "idx_clients_fcmtoken" ON clients (fcm_token);
|
CREATE UNIQUE INDEX "idx_clients_fcmtoken" ON clients (fcm_token);
|
||||||
@ -54,11 +53,9 @@ CREATE TABLE channels
|
|||||||
|
|
||||||
messages_sent INTEGER NOT NULL DEFAULT '0',
|
messages_sent INTEGER NOT NULL DEFAULT '0',
|
||||||
|
|
||||||
timestamp_created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
timestamp_created INTEGER NOT NULL,
|
||||||
timestamp_lastread TEXT NULL DEFAULT NULL,
|
timestamp_lastread INTEGER NULL DEFAULT NULL,
|
||||||
timestamp_lastsent TEXT NULL DEFAULT NULL,
|
timestamp_lastsent INTEGER NULL DEFAULT NULL
|
||||||
|
|
||||||
PRIMARY KEY (channel_id)
|
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, name);
|
CREATE UNIQUE INDEX "idx_channels_identity" ON channels (owner_user_id, name);
|
||||||
|
|
||||||
@ -68,9 +65,7 @@ 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
|
||||||
|
|
||||||
PRIMARY KEY (subscription_id)
|
|
||||||
);
|
);
|
||||||
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);
|
||||||
|
|
||||||
@ -83,15 +78,13 @@ CREATE TABLE messages
|
|||||||
|
|
||||||
channel_id INTEGER NOT NULL,
|
channel_id INTEGER NOT NULL,
|
||||||
|
|
||||||
timestamp_real TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
timestamp_real INTEGER NOT NULL,
|
||||||
timestamp_client TEXT NULL,
|
timestamp_client INTEGER NULL,
|
||||||
|
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
content TEXT NULL,
|
content TEXT NULL,
|
||||||
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
|
||||||
|
|
||||||
PRIMARY KEY (scn_message_id)
|
|
||||||
);
|
);
|
||||||
CREATE INDEX "idx_messages_channel" ON messages (sender_user_id, channel_name);
|
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_idempotency" ON messages (sender_user_id, usr_message_id);
|
||||||
@ -105,16 +98,15 @@ CREATE TABLE deliveries
|
|||||||
receiver_user_id INTEGER NOT NULL,
|
receiver_user_id INTEGER NOT NULL,
|
||||||
receiver_client_id INTEGER NOT NULL,
|
receiver_client_id INTEGER NOT NULL,
|
||||||
|
|
||||||
timestamp_created TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
timestamp_created INTEGER NOT NULL,
|
||||||
timestamp_finalized TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
timestamp_finalized INTEGER NOT NULL,
|
||||||
|
|
||||||
|
|
||||||
status TEXT CHECK(status IN ('RETRY','SUCCESS','FAILED')) NOT NULL,
|
status TEXT CHECK(status IN ('RETRY','SUCCESS','FAILED')) NOT NULL,
|
||||||
retry_count INTEGER NOT NULL DEFAULT 0,
|
retry_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
next_delivery INTEGER NULL DEFAULT NULL,
|
||||||
|
|
||||||
fcm_message_id TEXT NULL,
|
fcm_message_id TEXT NULL
|
||||||
|
|
||||||
PRIMARY KEY (delivery_id)
|
|
||||||
);
|
);
|
||||||
CREATE INDEX "idx_deliveries_receiver" ON deliveries (scn_message_id, receiver_client_id);
|
CREATE INDEX "idx_deliveries_receiver" ON deliveries (scn_message_id, receiver_client_id);
|
||||||
|
|
7
server/db/schema_sqlite.ddl
Normal file
7
server/db/schema_sqlite.ddl
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE sqlite_master (
|
||||||
|
type text,
|
||||||
|
name text,
|
||||||
|
tbl_name text,
|
||||||
|
rootpage integer,
|
||||||
|
sql text
|
||||||
|
);
|
15
server/db/utils.go
Normal file
15
server/db/utils.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func bool2DB(b bool) int {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func time2DB(t time.Time) int64 {
|
||||||
|
return t.UnixMilli()
|
||||||
|
}
|
@ -2,10 +2,8 @@ package logic
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
scn "blackforestbytes.com/simplecloudnotifier"
|
scn "blackforestbytes.com/simplecloudnotifier"
|
||||||
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
"blackforestbytes.com/simplecloudnotifier/db"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
@ -20,10 +18,10 @@ import (
|
|||||||
type Application struct {
|
type Application struct {
|
||||||
Config scn.Config
|
Config scn.Config
|
||||||
Gin *gin.Engine
|
Gin *gin.Engine
|
||||||
Database *sql.DB
|
Database *db.Database
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(db *sql.DB) *Application {
|
func NewApp(db *db.Database) *Application {
|
||||||
return &Application{Database: db}
|
return &Application{Database: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,30 +72,6 @@ func (app *Application) GenerateRandomAuthKey() string {
|
|||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *Application) RunTransaction(ctx context.Context, opt *sql.TxOptions, fn func(tx *sql.Tx) (ginresp.HTTPResponse, bool)) ginresp.HTTPResponse {
|
|
||||||
|
|
||||||
tx, err := app.Database.BeginTx(ctx, opt)
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.InternAPIError(0, fmt.Sprintf("Failed to create transaction: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
res, commit := fn(tx)
|
|
||||||
|
|
||||||
if commit {
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.InternAPIError(0, fmt.Sprintf("Failed to commit transaction: %v", err))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = tx.Rollback()
|
|
||||||
if err != nil {
|
|
||||||
return ginresp.InternAPIError(0, fmt.Sprintf("Failed to rollback transaction: %v", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *Application) QuotaMax(ispro bool) int {
|
func (app *Application) QuotaMax(ispro bool) int {
|
||||||
if ispro {
|
if ispro {
|
||||||
return 1000
|
return 1000
|
||||||
@ -109,3 +83,16 @@ func (app *Application) QuotaMax(ispro bool) int {
|
|||||||
func (app *Application) VerifyProToken(token string) (bool, error) {
|
func (app *Application) VerifyProToken(token string) (bool, error) {
|
||||||
return false, nil //TODO implement pro verification
|
return false, nil //TODO implement pro verification
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *Application) Migrate() error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 24*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return app.Database.Migrate(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *Application) StartRequest(g *gin.Context) *AppContext {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), app.Config.RequestTimeout)
|
||||||
|
|
||||||
|
return &AppContext{inner: ctx, cancelFunc: cancel}
|
||||||
|
}
|
||||||
|
75
server/logic/context.go
Normal file
75
server/logic/context.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package logic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/api/apierr"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/common/ginresp"
|
||||||
|
"blackforestbytes.com/simplecloudnotifier/db"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AppContext struct {
|
||||||
|
inner context.Context
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
cancelled bool
|
||||||
|
transaction *sql.Tx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppContext) Deadline() (deadline time.Time, ok bool) {
|
||||||
|
return ac.inner.Deadline()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppContext) Done() <-chan struct{} {
|
||||||
|
return ac.inner.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppContext) Err() error {
|
||||||
|
return ac.inner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppContext) Value(key any) any {
|
||||||
|
return ac.inner.Value(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppContext) Cancel() {
|
||||||
|
ac.cancelled = true
|
||||||
|
if ac.transaction != nil {
|
||||||
|
err := ac.transaction.Rollback()
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to rollback transaction: " + err.Error())
|
||||||
|
}
|
||||||
|
ac.transaction = nil
|
||||||
|
}
|
||||||
|
ac.cancelFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppContext) FinishSuccess(res ginresp.HTTPResponse) ginresp.HTTPResponse {
|
||||||
|
if ac.cancelled {
|
||||||
|
panic("Cannot finish a cancelled request")
|
||||||
|
}
|
||||||
|
if ac.transaction != nil {
|
||||||
|
err := ac.transaction.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return ginresp.InternAPIError(apierr.COMMIT_FAILED, "Failed to comit changes to DB", err)
|
||||||
|
}
|
||||||
|
ac.transaction = nil
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AppContext) GetOrCreateTransaction(db *db.Database) (*sql.Tx, error) {
|
||||||
|
if ac.cancelled {
|
||||||
|
return nil, errors.New("context cancelled")
|
||||||
|
}
|
||||||
|
if ac.transaction != nil {
|
||||||
|
return ac.transaction, nil
|
||||||
|
}
|
||||||
|
tx, err := db.BeginTx(ac)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ac.transaction = tx
|
||||||
|
return tx, nil
|
||||||
|
}
|
@ -7,9 +7,45 @@
|
|||||||
"version": "2.0"
|
"version": "2.0"
|
||||||
},
|
},
|
||||||
"host": "scn.blackforestbytes.com",
|
"host": "scn.blackforestbytes.com",
|
||||||
"basePath": "/api/",
|
"basePath": "/",
|
||||||
"paths": {
|
"paths": {
|
||||||
"/ack.php": {
|
"/api-v2/user/": {
|
||||||
|
"post": {
|
||||||
|
"summary": "Create a new user",
|
||||||
|
"operationId": "api-user-create",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": " ",
|
||||||
|
"name": "post_body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.CreateUser.body"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/models.UserJSON"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/ack.php": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Acknowledge that a message was received",
|
"summary": "Acknowledge that a message was received",
|
||||||
"operationId": "compat-ack",
|
"operationId": "compat-ack",
|
||||||
@ -46,31 +82,13 @@
|
|||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ginresp.internAPIError"
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/db-test": {
|
"/api/expand.php": {
|
||||||
"get": {
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handler.DatabaseTest.response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ginresp.errBody"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/expand.php": {
|
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Get a whole (potentially truncated) message",
|
"summary": "Get a whole (potentially truncated) message",
|
||||||
"operationId": "compat-expand",
|
"operationId": "compat-expand",
|
||||||
@ -84,31 +102,13 @@
|
|||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ginresp.internAPIError"
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/health": {
|
"/api/info.php": {
|
||||||
"get": {
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handler.Health.response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ginresp.errBody"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/info.php": {
|
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Get information about the current user",
|
"summary": "Get information about the current user",
|
||||||
"operationId": "compat-info",
|
"operationId": "compat-info",
|
||||||
@ -138,95 +138,13 @@
|
|||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ginresp.internAPIError"
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/ping": {
|
"/api/register.php": {
|
||||||
"get": {
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handler.pingResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ginresp.errBody"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handler.pingResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ginresp.errBody"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handler.pingResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ginresp.errBody"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"delete": {
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handler.pingResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ginresp.errBody"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"patch": {
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handler.pingResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ginresp.errBody"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/register.php": {
|
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Register a new account",
|
"summary": "Register a new account",
|
||||||
"operationId": "compat-register",
|
"operationId": "compat-register",
|
||||||
@ -267,13 +185,13 @@
|
|||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ginresp.internAPIError"
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/requery.php": {
|
"/api/requery.php": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Return all not-acknowledged messages",
|
"summary": "Return all not-acknowledged messages",
|
||||||
"operationId": "compat-requery",
|
"operationId": "compat-requery",
|
||||||
@ -303,85 +221,13 @@
|
|||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ginresp.internAPIError"
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/send.php": {
|
"/api/update.php": {
|
||||||
"post": {
|
|
||||||
"description": "(all arguments can either be supplied in the query or in the json body)",
|
|
||||||
"consumes": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"summary": "Send a message",
|
|
||||||
"operationId": "compat-send",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"name": "content",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"name": "messageID",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"name": "priority",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"name": "timestamp",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"name": "title",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"name": "userID",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"name": "userKey",
|
|
||||||
"in": "query"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": " ",
|
|
||||||
"name": "post_body",
|
|
||||||
"in": "body",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handler.Send.body"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "OK",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/handler.Send.response"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal Server Error",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/definitions/ginresp.sendAPIError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/update.php": {
|
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Set the fcm-token (android)",
|
"summary": "Set the fcm-token (android)",
|
||||||
"operationId": "compat-update",
|
"operationId": "compat-update",
|
||||||
@ -418,13 +264,13 @@
|
|||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ginresp.internAPIError"
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/upgrade.php": {
|
"/api/upgrade.php": {
|
||||||
"get": {
|
"get": {
|
||||||
"summary": "Upgrade a free account to a paid account",
|
"summary": "Upgrade a free account to a paid account",
|
||||||
"operationId": "compat-upgrade",
|
"operationId": "compat-upgrade",
|
||||||
@ -472,7 +318,125 @@
|
|||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error",
|
"description": "Internal Server Error",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/ginresp.internAPIError"
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/db-test": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.DatabaseTest.response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/health": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.Health.response"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/ping": {
|
||||||
|
"get": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.pingResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.pingResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.pingResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.pingResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/handler.pingResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal Server Error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/ginresp.apiError"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -480,32 +444,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"definitions": {
|
"definitions": {
|
||||||
"ginresp.errBody": {
|
"ginresp.apiError": {
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ginresp.internAPIError": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"errid": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ginresp.sendAPIError": {
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"errhighlight": {
|
"errhighlight": {
|
||||||
@ -514,6 +453,7 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
"errorObject": {},
|
||||||
"message": {
|
"message": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -539,6 +479,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handler.CreateUser.body": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"agentModel": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"agentVersion": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"clientType": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fcmtoken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"proToken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"handler.DatabaseTest.response": {
|
"handler.DatabaseTest.response": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -656,43 +619,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handler.Send.body": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"content": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"msg_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"priority": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"timestamp": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"user_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"user_key": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handler.Send.response": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"success": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"handler.Update.response": {
|
"handler.Update.response": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -818,6 +744,47 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"models.UserJSON": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"admin_key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"is_pro": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"messages_sent": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"quota_day": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"quota_today": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"read_key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"send_key": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp_created": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp_last_read": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp_last_sent": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,27 +1,12 @@
|
|||||||
basePath: /api/
|
basePath: /
|
||||||
definitions:
|
definitions:
|
||||||
ginresp.errBody:
|
ginresp.apiError:
|
||||||
properties:
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
ginresp.internAPIError:
|
|
||||||
properties:
|
|
||||||
errid:
|
|
||||||
type: integer
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
type: boolean
|
|
||||||
type: object
|
|
||||||
ginresp.sendAPIError:
|
|
||||||
properties:
|
properties:
|
||||||
errhighlight:
|
errhighlight:
|
||||||
type: integer
|
type: integer
|
||||||
error:
|
error:
|
||||||
type: integer
|
type: integer
|
||||||
|
errorObject: {}
|
||||||
message:
|
message:
|
||||||
type: string
|
type: string
|
||||||
success:
|
success:
|
||||||
@ -38,6 +23,21 @@ definitions:
|
|||||||
success:
|
success:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
handler.CreateUser.body:
|
||||||
|
properties:
|
||||||
|
agentModel:
|
||||||
|
type: string
|
||||||
|
agentVersion:
|
||||||
|
type: string
|
||||||
|
clientType:
|
||||||
|
type: string
|
||||||
|
fcmtoken:
|
||||||
|
type: string
|
||||||
|
proToken:
|
||||||
|
type: string
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
handler.DatabaseTest.response:
|
handler.DatabaseTest.response:
|
||||||
properties:
|
properties:
|
||||||
libVersion:
|
libVersion:
|
||||||
@ -114,30 +114,6 @@ definitions:
|
|||||||
success:
|
success:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
handler.Send.body:
|
|
||||||
properties:
|
|
||||||
content:
|
|
||||||
type: string
|
|
||||||
msg_id:
|
|
||||||
type: string
|
|
||||||
priority:
|
|
||||||
type: string
|
|
||||||
timestamp:
|
|
||||||
type: string
|
|
||||||
title:
|
|
||||||
type: string
|
|
||||||
user_id:
|
|
||||||
type: string
|
|
||||||
user_key:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
handler.Send.response:
|
|
||||||
properties:
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
success:
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
handler.Update.response:
|
handler.Update.response:
|
||||||
properties:
|
properties:
|
||||||
is_pro:
|
is_pro:
|
||||||
@ -220,6 +196,33 @@ definitions:
|
|||||||
usr_msg_id:
|
usr_msg_id:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
models.UserJSON:
|
||||||
|
properties:
|
||||||
|
admin_key:
|
||||||
|
type: string
|
||||||
|
is_pro:
|
||||||
|
type: boolean
|
||||||
|
messages_sent:
|
||||||
|
type: integer
|
||||||
|
quota_day:
|
||||||
|
type: string
|
||||||
|
quota_today:
|
||||||
|
type: integer
|
||||||
|
read_key:
|
||||||
|
type: string
|
||||||
|
send_key:
|
||||||
|
type: string
|
||||||
|
timestamp_created:
|
||||||
|
type: string
|
||||||
|
timestamp_last_read:
|
||||||
|
type: string
|
||||||
|
timestamp_last_sent:
|
||||||
|
type: string
|
||||||
|
user_id:
|
||||||
|
type: integer
|
||||||
|
username:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
host: scn.blackforestbytes.com
|
host: scn.blackforestbytes.com
|
||||||
info:
|
info:
|
||||||
contact: {}
|
contact: {}
|
||||||
@ -227,7 +230,30 @@ info:
|
|||||||
title: SimpleCloudNotifier API
|
title: SimpleCloudNotifier API
|
||||||
version: "2.0"
|
version: "2.0"
|
||||||
paths:
|
paths:
|
||||||
/ack.php:
|
/api-v2/user/:
|
||||||
|
post:
|
||||||
|
operationId: api-user-create
|
||||||
|
parameters:
|
||||||
|
- description: ' '
|
||||||
|
in: body
|
||||||
|
name: post_body
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.CreateUser.body'
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/models.UserJSON'
|
||||||
|
"400":
|
||||||
|
description: Bad Request
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
|
summary: Create a new user
|
||||||
|
/api/ack.php:
|
||||||
get:
|
get:
|
||||||
operationId: compat-ack
|
operationId: compat-ack
|
||||||
parameters:
|
parameters:
|
||||||
@ -254,20 +280,9 @@ paths:
|
|||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.internAPIError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: Acknowledge that a message was received
|
summary: Acknowledge that a message was received
|
||||||
/db-test:
|
/api/expand.php:
|
||||||
get:
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handler.DatabaseTest.response'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ginresp.errBody'
|
|
||||||
/expand.php:
|
|
||||||
get:
|
get:
|
||||||
operationId: compat-expand
|
operationId: compat-expand
|
||||||
responses:
|
responses:
|
||||||
@ -278,20 +293,9 @@ paths:
|
|||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.internAPIError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: Get a whole (potentially truncated) message
|
summary: Get a whole (potentially truncated) message
|
||||||
/health:
|
/api/info.php:
|
||||||
get:
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handler.Health.response'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ginresp.errBody'
|
|
||||||
/info.php:
|
|
||||||
get:
|
get:
|
||||||
operationId: compat-info
|
operationId: compat-info
|
||||||
parameters:
|
parameters:
|
||||||
@ -313,60 +317,9 @@ paths:
|
|||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.internAPIError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: Get information about the current user
|
summary: Get information about the current user
|
||||||
/ping:
|
/api/register.php:
|
||||||
delete:
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handler.pingResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ginresp.errBody'
|
|
||||||
get:
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handler.pingResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ginresp.errBody'
|
|
||||||
patch:
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handler.pingResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ginresp.errBody'
|
|
||||||
post:
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handler.pingResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ginresp.errBody'
|
|
||||||
put:
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handler.pingResponse'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ginresp.errBody'
|
|
||||||
/register.php:
|
|
||||||
get:
|
get:
|
||||||
operationId: compat-register
|
operationId: compat-register
|
||||||
parameters:
|
parameters:
|
||||||
@ -396,9 +349,9 @@ paths:
|
|||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.internAPIError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: Register a new account
|
summary: Register a new account
|
||||||
/requery.php:
|
/api/requery.php:
|
||||||
get:
|
get:
|
||||||
operationId: compat-requery
|
operationId: compat-requery
|
||||||
parameters:
|
parameters:
|
||||||
@ -420,55 +373,9 @@ paths:
|
|||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.internAPIError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: Return all not-acknowledged messages
|
summary: Return all not-acknowledged messages
|
||||||
/send.php:
|
/api/update.php:
|
||||||
post:
|
|
||||||
consumes:
|
|
||||||
- application/json
|
|
||||||
description: (all arguments can either be supplied in the query or in the json
|
|
||||||
body)
|
|
||||||
operationId: compat-send
|
|
||||||
parameters:
|
|
||||||
- in: query
|
|
||||||
name: content
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: messageID
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: priority
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: timestamp
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: title
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: userID
|
|
||||||
type: string
|
|
||||||
- in: query
|
|
||||||
name: userKey
|
|
||||||
type: string
|
|
||||||
- description: ' '
|
|
||||||
in: body
|
|
||||||
name: post_body
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handler.Send.body'
|
|
||||||
produces:
|
|
||||||
- application/json
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/handler.Send.response'
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/ginresp.sendAPIError'
|
|
||||||
summary: Send a message
|
|
||||||
/update.php:
|
|
||||||
get:
|
get:
|
||||||
operationId: compat-update
|
operationId: compat-update
|
||||||
parameters:
|
parameters:
|
||||||
@ -495,9 +402,9 @@ paths:
|
|||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.internAPIError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: Set the fcm-token (android)
|
summary: Set the fcm-token (android)
|
||||||
/upgrade.php:
|
/api/upgrade.php:
|
||||||
get:
|
get:
|
||||||
operationId: compat-upgrade
|
operationId: compat-upgrade
|
||||||
parameters:
|
parameters:
|
||||||
@ -532,6 +439,79 @@ paths:
|
|||||||
"500":
|
"500":
|
||||||
description: Internal Server Error
|
description: Internal Server Error
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ginresp.internAPIError'
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
summary: Upgrade a free account to a paid account
|
summary: Upgrade a free account to a paid account
|
||||||
|
/db-test:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.DatabaseTest.response'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
|
/health:
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.Health.response'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
|
/ping:
|
||||||
|
delete:
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.pingResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
|
get:
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.pingResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
|
patch:
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.pingResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.pingResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
|
put:
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/handler.pingResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal Server Error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/ginresp.apiError'
|
||||||
swagger: "2.0"
|
swagger: "2.0"
|
||||||
|
47
server/website/api.html
Normal file
47
server/website/api.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
|
||||||
|
<title>Simple Cloud Notifications - API</title>
|
||||||
|
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||||
|
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="copyinfo">
|
||||||
|
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||||
|
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="mainpnl">
|
||||||
|
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||||
|
<a tabindex="-1" href="/" class="button bordered" id="tr_link">Send</a>
|
||||||
|
|
||||||
|
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||||
|
|
||||||
|
<p>Get your user-id and user-key from the app and send notifications to your phone by performing a POST request against <code>https://simplecloudnotifier.blackforestbytes.com/</code></p>
|
||||||
|
<pre>curl \
|
||||||
|
--data "user_id={userid}" \
|
||||||
|
--data "user_key={userkey}" \
|
||||||
|
--data "title={message_title}" \
|
||||||
|
--data "content={message_body}" \
|
||||||
|
--data "priority={0|1|2}" \
|
||||||
|
--data "msg_id={unique_message_id}" \
|
||||||
|
https://scn.blackforestbytes.com/</pre>
|
||||||
|
<p>The <code>content</code>, <code>priority</code> and <code>msg_id</code> parameters are optional, you can also send message with only a title and the default priority</p>
|
||||||
|
<pre>curl \
|
||||||
|
--data "user_id={userid}" \
|
||||||
|
--data "user_key={userkey}" \
|
||||||
|
--data "title={message_title}" \
|
||||||
|
https://scn.blackforestbytes.com/</pre>
|
||||||
|
|
||||||
|
<a href="/api_more" class="button bordered tertiary" style="float: right; min-width: 100px; text-align: center">More</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
299
server/website/api_more.html
Normal file
299
server/website/api_more.html
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
|
||||||
|
<title>Simple Cloud Notifications - API</title>
|
||||||
|
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||||
|
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="copyinfo">
|
||||||
|
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||||
|
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="mainpnl">
|
||||||
|
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||||
|
<a tabindex="-1" href="/" class="button bordered" id="tr_link">Send</a>
|
||||||
|
|
||||||
|
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||||
|
|
||||||
|
<h2>Introduction</h2>
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
With this API you can send push notifications to your phone.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To recieve them you will need to install the <a href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier">SimpleCloudNotifier</a> app from the play store.
|
||||||
|
When you open the app you can click on the account tab to see you unique <code>user_id</code> and <code>user_key</code>.
|
||||||
|
These two values are used to identify and authenticate your device so that send messages can be routed to your phone.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can at any time generate a new <code>user_key</code> in the app and invalidate the old one.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
There is also a <a href="/">web interface</a> for this API to manually send notifications to your phone or to test your setup.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Quota</h2>
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
By default you can send up to 100 messages per day per device.
|
||||||
|
If you need more you can upgrade your account in the app to get 1000 messages per day, this has the additional benefit of removing ads and supporting the development of the app (and making sure I can pay the server costs).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>API Requests</h2>
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
To send a new notification you send a <code>POST</code> request to the URL <code>https://scn.blackforestbytes.com/</code>.
|
||||||
|
All Parameters can either directly be submitted as URL parameters or they can be put into the POST body.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You <i>need</i> to supply a valid <code>user_id</code> - <code>user_key</code> pair and a <code>title</code> for your message, all other parameter are optional.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>API Response</h2>
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
If the operation was successful the API will respond with an HTTP statuscode 200 and an JSON payload indicating the send message and your remaining quota
|
||||||
|
</p>
|
||||||
|
<pre class="red-code">{
|
||||||
|
"success":true,
|
||||||
|
"message":"Message sent",
|
||||||
|
"response":
|
||||||
|
{
|
||||||
|
"multicast_id":8000000000000000006,
|
||||||
|
"success":1,
|
||||||
|
"failure":0,
|
||||||
|
"canonical_ids":0,
|
||||||
|
"results": [{"message_id":"0:10000000000000000000000000000000d"}]
|
||||||
|
},
|
||||||
|
"quota":17,
|
||||||
|
"quota_max":100
|
||||||
|
}</pre>
|
||||||
|
<p>
|
||||||
|
If the operation is <b>not</b> successful the API will respond with an 4xx HTTP statuscode.
|
||||||
|
</p>
|
||||||
|
<table class="scode_table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Statuscode</th>
|
||||||
|
<th>Explanation</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td data-label="Statuscode">200 (OK)</td>
|
||||||
|
<td data-label="Explanation">Message sent</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td data-label="Statuscode">400 (Bad Request)</td>
|
||||||
|
<td data-label="Explanation">The request is invalid (missing parameters or wrong values)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td data-label="Statuscode">401 (Unauthorized)</td>
|
||||||
|
<td data-label="Explanation">The user_id was not found or the user_key is wrong</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td data-label="Statuscode">403 (Forbidden)</td>
|
||||||
|
<td data-label="Explanation">The user has exceeded its daily quota - wait 24 hours or upgrade your account</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td data-label="Statuscode">412 (Precondition Failed)</td>
|
||||||
|
<td data-label="Explanation">There is no device connected with this account - open the app and press the refresh button in the account tab</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td data-label="Statuscode">500 (Internal Server Error)</td>
|
||||||
|
<td data-label="Explanation">There was an internal error while sending your data - try again later</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>
|
||||||
|
There is also always a JSON payload with additional information.
|
||||||
|
The <code>success</code> field is always there and in the error state you the <code>message</code> field to get a descritpion of the problem.
|
||||||
|
</p>
|
||||||
|
<pre class="red-code">{
|
||||||
|
"success":false,
|
||||||
|
"error":2101,
|
||||||
|
"errhighlight":-1,
|
||||||
|
"message":"Daily quota reached (100)"
|
||||||
|
}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Message Content</h2>
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
Every message must have a title set.
|
||||||
|
But you also (optionally) add more content, while the title has a max length of 120 characters, the conntent can be up to 10.000 characters.
|
||||||
|
You can see the whole message with title and content in the app or when clicking on the notification.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If needed the content can be supplied in the <code>content</code> parameter.
|
||||||
|
</p>
|
||||||
|
<pre>curl \
|
||||||
|
--data "user_id={userid}" \
|
||||||
|
--data "user_key={userkey}" \
|
||||||
|
--data "title={message_title}" \
|
||||||
|
--data "content={message_content}" \
|
||||||
|
https://scn.blackforestbytes.com/</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Message Priority</h2>
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
Currently you can send a message with three different priorities: 0 (low), 1 (normal) and 2 (high).
|
||||||
|
In the app you can then configure a different behaviour for different priorities, e.g. only playing a sound if the notification is high priority.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Priorites are either 0, 1 or 2 and are supplied in the <code>priority</code> parameter.
|
||||||
|
If no priority is supplied the message will get the default priority of 1.
|
||||||
|
</p>
|
||||||
|
<pre>curl \
|
||||||
|
--data "user_id={userid}" \
|
||||||
|
--data "user_key={userkey}" \
|
||||||
|
--data "title={message_title}" \
|
||||||
|
--data "priority={0|1|2}" \
|
||||||
|
https://scn.blackforestbytes.com/s</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Message Uniqueness</h2>
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
Sometimes your script can run in an environment with an unstable connection and you want to implement an automatic re-try mechanism to send a message again if the last try failed due to bad connectivity.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To ensure that a message is only send once you can generate a unique id for your message (I would recommend a simple <code>uuidgen</code>).
|
||||||
|
If you send a message with an UUID that was already used in the near past the API still returns OK, but no new message is sent.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The message_id is optional - but if you want to use it you need to supply it via the <code>msg_id</code> parameter.
|
||||||
|
</p>
|
||||||
|
<pre>curl \
|
||||||
|
--data "user_id={userid}" \
|
||||||
|
--data "user_key={userkey}" \
|
||||||
|
--data "title={message_title}" \
|
||||||
|
--data "msg_id={message_id}" \
|
||||||
|
https://scn.blackforestbytes.com/</pre>
|
||||||
|
<p>
|
||||||
|
Be aware that the server only saves send messages for a short amount of time. Because of that you can only use this to prevent duplicates in a short time-frame, older messages with the same ID are probably already deleted and the message will be send again.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Custom Time</h2>
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
You can modify the displayed timestamp of a message by sending the <code>timestamp</code> parameter. The format must be a valid UNIX timestamp (elapsed seconds since 1970-01-01 GMT)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The custom timestamp must be within 48 hours of the current time. This parameter is only intended to supply a more precise value in case the message sending was delayed.
|
||||||
|
</p>
|
||||||
|
<pre>curl \
|
||||||
|
--data "user_id={userid}" \
|
||||||
|
--data "user_key={userkey}" \
|
||||||
|
--data "title={message_title}" \
|
||||||
|
--data "timestamp={unix_timestamp}" \
|
||||||
|
https://scn.blackforestbytes.com/</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Bash script example</h2>
|
||||||
|
<div class="section">
|
||||||
|
<p>
|
||||||
|
Depending on your use case it can be useful to create a bash script that handles things like resending messages if you have connection problems or waiting if there is no quota left.<br/>
|
||||||
|
Here is an example how such a scrippt could look like, you can put it into <code>/usr/local/sbin</code> and call it with <code>scn_send "title" "content"</code>
|
||||||
|
</p>
|
||||||
|
<pre style="color:#000000;" class="yellow-code"><span style="color:#3f7f59; font-weight:bold;">#!/usr/bin/env bash</span>
|
||||||
|
|
||||||
|
<span style="color:#3f7f59; ">#</span>
|
||||||
|
<span style="color:#3f7f59; "># Call with `scn_send title`</span>
|
||||||
|
<span style="color:#3f7f59; "># or `scn_send title content`</span>
|
||||||
|
<span style="color:#3f7f59; "># or `scn_send title content priority`</span>
|
||||||
|
<span style="color:#3f7f59; ">#</span>
|
||||||
|
<span style="color:#3f7f59; ">#</span>
|
||||||
|
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -lt 1 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"no title supplied via parameter"</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||||
|
|
||||||
|
<span style="color:#3f7f59; ">################################################################################</span>
|
||||||
|
<span style="color:#3f7f59; "># INSERT YOUR DATA HERE #</span>
|
||||||
|
<span style="color:#3f7f59; ">################################################################################</span>
|
||||||
|
user_id=999
|
||||||
|
user_key=<span style="color:#2a00ff; ">"????????????????????????????????????????????????????????????????"</span>
|
||||||
|
<span style="color:#3f7f59; ">################################################################################</span>
|
||||||
|
|
||||||
|
title=$1
|
||||||
|
content=<span style="color:#2a00ff; ">""</span>
|
||||||
|
sendtime=$(date +%s)
|
||||||
|
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -gt 1 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||||
|
content=$2
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||||
|
|
||||||
|
priority=1
|
||||||
|
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"$#"</span> -gt 2 ]; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||||
|
priority=$3
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||||
|
|
||||||
|
usr_msg_id=$(uuidgen)
|
||||||
|
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">while</span> true ; <span style="color:#7f0055; font-weight:bold; ">do</span>
|
||||||
|
|
||||||
|
curlresp=$(curl -s -o <span style="color:#3f3fbf; ">/dev/null</span> -w <span style="color:#2a00ff; ">"%{http_code}"</span> <span style="color:#2a00ff; ">\</span>
|
||||||
|
-d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">user_id</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$user_id</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">user_key</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$user_key</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">title</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$title</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">timestamp</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$sendtime</span><span style="color:#2a00ff; ">"</span> <span style="color:#2a00ff; ">\</span>
|
||||||
|
-d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">content</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$content</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">priority</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$priority</span><span style="color:#2a00ff; ">"</span> -d <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">msg_id</span><span style="color:#2a00ff; ">=</span><span style="color:#2a00ff; ">$usr_msg_id</span><span style="color:#2a00ff; ">"</span> <span style="color:#2a00ff; ">\</span>
|
||||||
|
https:<span style="color:#3f3fbf; ">/</span><span style="color:#3f3fbf; ">/scn.blackforestbytes.com/</span>)
|
||||||
|
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 200 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Successfully send"</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">exit</span> 0
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||||
|
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 400 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Bad request - something went wrong"</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||||
|
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 401 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Unauthorized - wrong </span><span style="color:#3f3fbf; ">userid/userkey</span><span style="color:#2a00ff; ">"</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||||
|
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 403 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Quota exceeded - wait one hour before re-try"</span>
|
||||||
|
sleep 3600
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||||
|
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 412 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Precondition Failed - No device linked"</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">exit</span> 1
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||||
|
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">if</span> [ <span style="color:#2a00ff; ">"</span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">"</span> == 500 ] ; <span style="color:#7f0055; font-weight:bold; ">then</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Internal server error - waiting for better times"</span>
|
||||||
|
sleep 60
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">fi</span>
|
||||||
|
|
||||||
|
<span style="color:#3f7f59; "># if none of the above matched we probably hav no network ...</span>
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">echo</span> <span style="color:#2a00ff; ">"Send failed (response code </span><span style="color:#2a00ff; ">$curlresp</span><span style="color:#2a00ff; ">) ... try again in 5s"</span>
|
||||||
|
sleep 5
|
||||||
|
<span style="color:#7f0055; font-weight:bold; ">done</span>
|
||||||
|
</pre>
|
||||||
|
<p>
|
||||||
|
Be aware that the server only saves send messages for a short amount of time. Because of that you can only use this to prevent duplicates in a short time-frame, older messages with the same ID are probably already deleted and the message will be send again.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
1
server/website/css/mini-dark.min.css
vendored
Normal file
1
server/website/css/mini-dark.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
server/website/css/mini-default.min.css
vendored
Normal file
1
server/website/css/mini-default.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
server/website/css/mini-nord.min.css
vendored
Normal file
1
server/website/css/mini-nord.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
250
server/website/css/style.css
Normal file
250
server/website/css/style.css
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
|
||||||
|
html
|
||||||
|
{
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body
|
||||||
|
{
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink-shadow {
|
||||||
|
0% { box-shadow: 0 0 32px #DDD; }
|
||||||
|
50% { box-shadow: none; }
|
||||||
|
100% { box-shadow: 0 0 32px #DDD; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainpnl
|
||||||
|
{
|
||||||
|
box-shadow: 0 0 32px #DDD;
|
||||||
|
//animation:blink-shadow ease-in-out 4s infinite;
|
||||||
|
width: 87%;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 900px;
|
||||||
|
position: relative;
|
||||||
|
min-height: 570px;
|
||||||
|
|
||||||
|
background: var(--form-back-color);
|
||||||
|
color: var(--form-fore-color);
|
||||||
|
border: .0625rem solid var(--form-border-color);
|
||||||
|
border-radius: var(--universal-border-radius);
|
||||||
|
margin: 32px .5rem;
|
||||||
|
padding: calc(2 * var(--universal-padding)) var(--universal-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-code
|
||||||
|
{
|
||||||
|
border-left: .25rem solid #E53935;
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow-code
|
||||||
|
{
|
||||||
|
border-left: .25rem solid #FFCB05;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainpnl input,
|
||||||
|
#mainpnl textarea
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive-label {
|
||||||
|
align-items:center;
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.responsive-label .col-md-3 {
|
||||||
|
text-align:right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainpnl h1
|
||||||
|
{
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #FFF;
|
||||||
|
text-shadow: #000 0 0 2px, #888 0 0 8px;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#mainpnl h1 {
|
||||||
|
font-size: calc(0.85rem * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio) * var(--heading-ratio));
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainpnl button
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copyinfo
|
||||||
|
{
|
||||||
|
margin: 4px;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
//z-index: -999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copyinfo a,
|
||||||
|
#copyinfo a:visited,
|
||||||
|
#copyinfo a:active
|
||||||
|
{
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
color: #AAA;
|
||||||
|
text-decoration: none;
|
||||||
|
display: block;
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#copyinfo a:hover
|
||||||
|
{
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
color: #0288D1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tr_link
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: -1px -1px 0 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tl_link
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: -1px 0 0 -1px;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
padding: 4px 4px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icn-google-play {
|
||||||
|
display: inline-block;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHg9IjBweCIgeT0iMHB4IgogICAgIHdpZHRoPSI1MCIgaGVpZ2h0PSI1MCIKICAgICB2aWV3Qm94PSIwIDAgNDggNDgiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDsiPjxnIGlkPSJzdXJmYWNlMSI+PHBhdGggc3R5bGU9IiBmaWxsOiM0REI2QUM7IiBkPSJNIDcuNzAzMTI1IDQuMDQyOTY5IEMgNy4yOTI5NjkgNC4xNDg0MzggNyA0LjUwNzgxMyA3IDUuMTIxMDk0IEMgNyA2LjkyMTg3NSA3IDIzLjkxNDA2MyA3IDIzLjkxNDA2MyBDIDcgMjMuOTE0MDYzIDcgNDIuMjgxMjUgNyA0My4wODk4NDQgQyA3IDQzLjUzNTE1NiA3LjE5NTMxMyA0My44MzU5MzggNy41IDQzLjk0NTMxMyBMIDI3LjY3OTY4OCAyMy44ODI4MTMgWiAiPjwvcGF0aD48cGF0aCBzdHlsZT0iIGZpbGw6I0RDRTc3NTsiIGQ9Ik0gMzMuMjM4MjgxIDE4LjM1OTM3NSBMIDI0LjkyOTY4OCAxMy41NjI1IEMgMjQuOTI5Njg4IDEzLjU2MjUgOS42ODM1OTQgNC43NjE3MTkgOC43ODkwNjMgNC4yNDIxODggQyA4LjQwMjM0NCA0LjAxOTUzMSA4LjAxOTUzMSAzLjk2MDkzOCA3LjcwMzEyNSA0LjA0Mjk2OSBMIDI3LjY4MzU5NCAyMy44ODI4MTMgWiAiPjwvcGF0aD48cGF0aCBzdHlsZT0iIGZpbGw6I0QzMkYyRjsiIGQ9Ik0gOC40MTc5NjkgNDMuODAwNzgxIEMgOC45NDkyMTkgNDMuNDkyMTg4IDIzLjY5OTIxOSAzNC45NzY1NjMgMzMuMjgxMjUgMjkuNDQ1MzEzIEwgMjcuNjc5Njg4IDIzLjg4MjgxMyBMIDcuNSA0My45NDUzMTMgQyA3Ljc0NjA5NCA0NC4wMzkwNjMgOC4wNjY0MDYgNDQuMDAzOTA2IDguNDE3OTY5IDQzLjgwMDc4MSBaICI+PC9wYXRoPjxwYXRoIHN0eWxlPSIgZmlsbDojRkJDMDJEOyIgZD0iTSA0MS4zOTg0MzggMjMuMDcwMzEzIEMgNDAuNjAxNTYzIDIyLjY0MDYyNSAzMy4yOTY4NzUgMTguMzk0NTMxIDMzLjI5Njg3NSAxOC4zOTQ1MzEgTCAzMy4yMzgyODEgMTguMzU5Mzc1IEwgMjcuNjc5Njg4IDIzLjg4MjgxMyBMIDMzLjI4MTI1IDI5LjQ0NTMxMyBDIDM3LjcxNDg0NCAyNi44ODY3MTkgNDEuMDQyOTY5IDI0Ljk2NDg0NCA0MS4zMzk4NDQgMjQuNzkyOTY5IEMgNDIuMjg1MTU2IDI0LjI0NjA5NCA0Mi4xOTUzMTMgMjMuNSA0MS4zOTg0MzggMjMuMDcwMzEzIFogIj48L3BhdGg+PC9nPjwvc3ZnPg==') 50% 50% no-repeat;
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btnSend
|
||||||
|
{
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btnSend .spinnerbox .spinner
|
||||||
|
{
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btnSend .spinnerbox
|
||||||
|
{
|
||||||
|
margin: -8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='number'] {
|
||||||
|
-moz-appearance:textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-invalid,
|
||||||
|
.input-invalid:hover,
|
||||||
|
.input-invalid:active
|
||||||
|
{
|
||||||
|
border-color: var(--input-invalid-color) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.success {
|
||||||
|
--card-back-color: rgb(48, 135, 50);
|
||||||
|
--card-border-color: rgba(0, 0, 0, 0.3);;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullcenterflex
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullcenterflex .card
|
||||||
|
{
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.card,
|
||||||
|
a.card:active,
|
||||||
|
a.card:visited,
|
||||||
|
a.card:hover
|
||||||
|
{
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.card:hover
|
||||||
|
{
|
||||||
|
box-shadow: 0 0 16px #AAA;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.scode_table {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.scode_table td:nth-child(2) {
|
||||||
|
flex-grow: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.scode_table th:nth-child(2) {
|
||||||
|
flex-grow: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mainpnl h2 {
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkcaption:hover,
|
||||||
|
.linkcaption:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre, pre span
|
||||||
|
{
|
||||||
|
font-family: Menlo, Consolas, monospace;
|
||||||
|
background: #F9F9F9;;
|
||||||
|
}
|
15
server/website/css/toastify.min.css
vendored
Normal file
15
server/website/css/toastify.min.css
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Minified by jsDelivr using clean-css v4.2.0.
|
||||||
|
* Original file: /npm/toastify.js@1.3.0/src/toastify.css
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
/*!
|
||||||
|
* Toastify js 1.2.2
|
||||||
|
* https://github.com/apvarun/toastify-js
|
||||||
|
* @license MIT licensed
|
||||||
|
*
|
||||||
|
* Copyright (C) 2018 Varun A P
|
||||||
|
*/
|
||||||
|
.toastify{padding:12px 20px;color:#fff;display:inline-block;box-shadow:0 3px 6px -1px rgba(0,0,0,.12),0 10px 36px -4px rgba(77,96,232,.3);background:-webkit-linear-gradient(315deg,#73a5ff,#5477f5);background:linear-gradient(135deg,#73a5ff,#5477f5);position:fixed;opacity:0;transition:all .4s cubic-bezier(.215,.61,.355,1);border-radius:2px;cursor:pointer;text-decoration:none;max-width:calc(50% - 20px)}.toastify.on{opacity:1}.toast-close{opacity:.4;padding:0 5px}.right{right:15px}.left{left:15px}.top{top:-150px}.bottom{bottom:-150px}.rounded{border-radius:25px}.avatar{width:1.5em;height:1.5em;margin:0 5px;border-radius:2px}@media only screen and (max-width:360px){.left,.right{margin-left:auto;margin-right:auto;left:0;right:0;max-width:fit-content}}
|
||||||
|
/*# sourceMappingURL=/sm/734ed69e2fe87a4469526acc0a10708fa8e0211c7d4359f9e034ceb89bb5d540.map */
|
BIN
server/website/favicon.ico
Normal file
BIN
server/website/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
BIN
server/website/favicon.png
Normal file
BIN
server/website/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
69
server/website/index.html
Normal file
69
server/website/index.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Simple Cloud Notifications</title>
|
||||||
|
<link rel="stylesheet" href="/css/toastify.min.css"/>
|
||||||
|
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/ -->
|
||||||
|
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||||
|
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="copyinfo">
|
||||||
|
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||||
|
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="mainpnl">
|
||||||
|
|
||||||
|
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||||
|
<a tabindex="-1" href="/api" class="button bordered" id="tr_link">API</a>
|
||||||
|
|
||||||
|
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||||
|
|
||||||
|
<div class="row responsive-label">
|
||||||
|
<div class="col-sm-12 col-md-3"><label for="uid" class="doc">UserID</label></div>
|
||||||
|
<div class="col-sm-12 col-md"><input placeholder="UserID" id="uid" class="doc" <?php echo (isset($_GET['preset_user_id']) ? (' value="'.$_GET['preset_user_id'].'" '):(''));?> type="number"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row responsive-label">
|
||||||
|
<div class="col-sm-12 col-md-3"><label for="ukey" class="doc">Authentification Key</label></div>
|
||||||
|
<div class="col-sm-12 col-md"><input placeholder="Key" id="ukey" class="doc" <?php echo (isset($_GET['preset_user_key']) ? (' value="'.$_GET['preset_user_key'].'" '):(''));?> type="text" maxlength="64"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row responsive-label">
|
||||||
|
<div class="col-sm-12 col-md-3"><label for="prio" class="doc">Priority</label></div>
|
||||||
|
<div class="col-sm-12 col-md">
|
||||||
|
<select id="prio" class="doc" type="text" style="width:100%;">
|
||||||
|
<option value="0" <?php echo (( isset($_GET['preset_priority'])&&$_GET['preset_priority']==='0') ? 'selected':'');?>>Low</option>
|
||||||
|
<option value="1" <?php echo ((!isset($_GET['preset_priority'])||$_GET['preset_priority']==='1') ? 'selected':'');?>>Normal</option>
|
||||||
|
<option value="2" <?php echo (( isset($_GET['preset_priority'])&&$_GET['preset_priority']==='2') ? 'selected':'');?>>High</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row responsive-label">
|
||||||
|
<div class="col-sm-12 col-md-3"><label for="msg" class="doc">Message Title</label></div>
|
||||||
|
<div class="col-sm-12 col-md"><input placeholder="Message" id="msg" class="doc" <?php echo (isset($_GET['preset_title']) ? (' value="'.$_GET['preset_title'].'" '):(''));?> type="text" maxlength="80"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row responsive-label">
|
||||||
|
<div class="col-sm-12 col-md-3"><label for="txt" class="doc">Message Content</label></div>
|
||||||
|
<div class="col-sm-12 col-md"><textarea id="txt" class="doc" <?php echo (isset($_GET['preset_content']) ? (' value="'.$_GET['preset_content'].'" '):(''));?> rows="8" maxlength="2048"></textarea></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12 col-md-3"></div>
|
||||||
|
<div class="col-sm-12 col-md"><button type="submit" class="primary bordered" id="btnSend">Send</button></div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script src="/js/logic.js" type="text/javascript" ></script>
|
||||||
|
<script src="/js/toastify.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
90
server/website/js/logic.js
Normal file
90
server/website/js/logic.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
function send()
|
||||||
|
{
|
||||||
|
let me = document.getElementById("btnSend");
|
||||||
|
if (me.classList.contains("btn-disabled")) return;
|
||||||
|
|
||||||
|
me.innerHTML = "<div class=\"spinnerbox\"><div class=\"spinner primary\"></div></div>";
|
||||||
|
|
||||||
|
me.classList.add("btn-disabled");
|
||||||
|
|
||||||
|
let uid = document.getElementById("uid");
|
||||||
|
let key = document.getElementById("ukey");
|
||||||
|
let msg = document.getElementById("msg");
|
||||||
|
let txt = document.getElementById("txt");
|
||||||
|
let pio = document.getElementById("prio");
|
||||||
|
|
||||||
|
uid.classList.remove('input-invalid');
|
||||||
|
key.classList.remove('input-invalid');
|
||||||
|
msg.classList.remove('input-invalid');
|
||||||
|
txt.classList.remove('input-invalid');
|
||||||
|
pio.classList.remove('input-invalid');
|
||||||
|
|
||||||
|
let data = new FormData();
|
||||||
|
data.append('user_id', uid.value);
|
||||||
|
data.append('user_key', key.value);
|
||||||
|
data.append('title', msg.value);
|
||||||
|
data.append('content', txt.value);
|
||||||
|
data.append('priority', pio.value);
|
||||||
|
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/', true);
|
||||||
|
xhr.onreadystatechange = function ()
|
||||||
|
{
|
||||||
|
if (xhr.readyState !== 4) return;
|
||||||
|
|
||||||
|
console.log('Status: ' + xhr.status);
|
||||||
|
if (xhr.status === 200 || xhr.status === 401 || xhr.status === 403 || xhr.status === 412)
|
||||||
|
{
|
||||||
|
let resp = JSON.parse(xhr.responseText);
|
||||||
|
if (!resp.success || xhr.status !== 200)
|
||||||
|
{
|
||||||
|
if (resp.errhighlight === 101) uid.classList.add('input-invalid');
|
||||||
|
if (resp.errhighlight === 102) key.classList.add('input-invalid');
|
||||||
|
if (resp.errhighlight === 103) msg.classList.add('input-invalid');
|
||||||
|
if (resp.errhighlight === 104) txt.classList.add('input-invalid');
|
||||||
|
if (resp.errhighlight === 105) pio.classList.add('input-invalid');
|
||||||
|
|
||||||
|
Toastify({
|
||||||
|
text: resp.message,
|
||||||
|
gravity: "top",
|
||||||
|
positionLeft: false,
|
||||||
|
backgroundColor: "#D32F2F",
|
||||||
|
}).showToast();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
window.location.href =
|
||||||
|
'/message_sent' +
|
||||||
|
'?ok=' + 1 +
|
||||||
|
'&message_count=' + resp.messagecount +
|
||||||
|
'"a=' + resp.quota +
|
||||||
|
'"a_remain=' + (resp.quota_max-resp.quota) +
|
||||||
|
'"a_max=' + resp.quota_max +
|
||||||
|
'&preset_user_id=' + uid.value +
|
||||||
|
'&preset_user_key=' + key.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Toastify({
|
||||||
|
text: 'Request failed: Statuscode=' + xhr.status,
|
||||||
|
gravity: "top",
|
||||||
|
positionLeft: false,
|
||||||
|
backgroundColor: "#D32F2F",
|
||||||
|
}).showToast();
|
||||||
|
}
|
||||||
|
|
||||||
|
me.classList.remove("btn-disabled");
|
||||||
|
me.innerHTML = "Send";
|
||||||
|
};
|
||||||
|
xhr.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("load",function ()
|
||||||
|
{
|
||||||
|
let btnSend = document.getElementById("btnSend");
|
||||||
|
|
||||||
|
if (btnSend !== undefined) btnSend.onclick = function () { send(); return false; };
|
||||||
|
|
||||||
|
},false);
|
8
server/website/js/toastify.js
Normal file
8
server/website/js/toastify.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Minified by jsDelivr using UglifyJS v3.4.3.
|
||||||
|
* Original file: /npm/toastify-js@1.3.0/src/toastify.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
!function(t,o){"object"==typeof module&&module.exports?(require("./toastify.css"),module.exports=o()):t.Toastify=o()}(this,function(t){var i=function(t){return new i.lib.init(t)};function r(t,o){return!(!t||"string"!=typeof o)&&!!(t.className&&-1<t.className.trim().split(/\s+/gi).indexOf(o))}return i.lib=i.prototype={toastify:"1.2.2",constructor:i,init:function(t){return t||(t={}),this.options={},this.options.text=t.text||"Hi there!",this.options.duration=t.duration||3e3,this.options.selector=t.selector,this.options.callback=t.callback||function(){},this.options.destination=t.destination,this.options.newWindow=t.newWindow||!1,this.options.close=t.close||!1,this.options.gravity="bottom"==t.gravity?"bottom":"top",this.options.positionLeft=t.positionLeft||!1,this.options.backgroundColor=t.backgroundColor,this.options.avatar=t.avatar||"",this.options.className=t.className||"",this},buildToast:function(){if(!this.options)throw"Toastify is not initialized";var t=document.createElement("div");if(t.className="toastify on "+this.options.className,!0===this.options.positionLeft?t.className+=" left":t.className+=" right",t.className+=" "+this.options.gravity,this.options.backgroundColor&&(t.style.background=this.options.backgroundColor),t.innerHTML=this.options.text,""!==this.options.avatar){var o=document.createElement("img");o.src=this.options.avatar,o.className="avatar",!0===this.options.positionLeft?t.appendChild(o):t.insertAdjacentElement("beforeend",o)}if(!0===this.options.close){var i=document.createElement("span");i.innerHTML="✖",i.className="toast-close",i.addEventListener("click",function(t){t.stopPropagation(),this.removeElement(t.target.parentElement),window.clearTimeout(t.target.parentElement.timeOutValue)}.bind(this));var n=0<window.innerWidth?window.innerWidth:screen.width;!0===this.options.positionLeft&&360<n?t.insertAdjacentElement("afterbegin",i):t.appendChild(i)}return void 0!==this.options.destination&&t.addEventListener("click",function(t){t.stopPropagation(),!0===this.options.newWindow?window.open(this.options.destination,"_blank"):window.location=this.options.destination}.bind(this)),t},showToast:function(){var t,o=this.buildToast();if(!(t=void 0===this.options.selector?document.body:document.getElementById(this.options.selector)))throw"Root element is not defined";return t.insertBefore(o,t.firstChild),i.reposition(),o.timeOutValue=window.setTimeout(function(){this.removeElement(o)}.bind(this),this.options.duration),this},removeElement:function(t){t.className=t.className.replace(" on",""),window.setTimeout(function(){t.parentNode.removeChild(t),this.options.callback.call(t),i.reposition()}.bind(this),400)}},i.reposition=function(){for(var t,o={top:15,bottom:15},i={top:15,bottom:15},n={top:15,bottom:15},e=document.getElementsByClassName("toastify"),s=0;s<e.length;s++){t=!0===r(e[s],"top")?"top":"bottom";var a=e[s].offsetHeight;(0<window.innerWidth?window.innerWidth:screen.width)<=360?(e[s].style[t]=n[t]+"px",n[t]+=a+15):!0===r(e[s],"left")?(e[s].style[t]=o[t]+"px",o[t]+=a+15):(e[s].style[t]=i[t]+"px",i[t]+=a+15)}return this},i.lib.init.prototype=i.lib,i});
|
||||||
|
//# sourceMappingURL=/sm/3f68e387be4f7a323a891120e4e01e3bee54a927113a386cf5e598b3cd442fcc.map
|
56
server/website/message_sent.html
Normal file
56
server/website/message_sent.html
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Simple Cloud Notifications</title>
|
||||||
|
<link rel="stylesheet" href="/css/mini-default.min.css"> <!-- https://minicss.org/docs -->
|
||||||
|
<!--<link rel="stylesheet" href="/css/mini-nord.min.css">-->
|
||||||
|
<!--<link rel="stylesheet" href="/css/mini-dark.min.css">-->
|
||||||
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.png"/>
|
||||||
|
<link rel="icon" type="image/png" href="/favicon.ico"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="copyinfo">
|
||||||
|
<a tabindex="-1" href="https://www.blackforestbytes.com">© blackforestbytes</a>
|
||||||
|
<a tabindex="-1" href="https://www.mikescher.com">made by Mike Schwörer</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="mainpnl">
|
||||||
|
|
||||||
|
<div class="fullcenterflex">
|
||||||
|
|
||||||
|
<?php if (isset($_GET['ok']) && $_GET['ok'] === "1" ): ?>
|
||||||
|
|
||||||
|
<a class="card success" href="/index.php?preset_user_id=<?php echo isset($_GET['preset_user_id'])?$_GET['preset_user_id']:'ERR';?>&preset_user_key=<?php echo isset($_GET['preset_user_key'])?$_GET['preset_user_key']:'ERR';?>">
|
||||||
|
<div class="section">
|
||||||
|
<h3 class="doc">Message sent</h3>
|
||||||
|
<p class="doc">Message succesfully sent<br>
|
||||||
|
<?php echo isset($_GET['quota_remain'])?$_GET['quota_remain']:'ERR';?>/<?php echo isset($_GET['quota_max'])?$_GET['quota_max']:'ERR';?> remaining</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
|
||||||
|
<a class="card error" href="/index.php">
|
||||||
|
<div class="section">
|
||||||
|
<h3 class="doc">Failure</h3>
|
||||||
|
<p class="doc">Unknown error</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a tabindex="-1" href="https://play.google.com/store/apps/details?id=com.blackforestbytes.simplecloudnotifier" class="button bordered" id="tl_link"><span class="icn-google-play"></span></a>
|
||||||
|
<a tabindex="-1" href="/index.php" class="button bordered" id="tr_link">Send</a>
|
||||||
|
|
||||||
|
<a tabindex="-1" href="/" class="linkcaption"><h1>Simple Cloud Notifier</h1></a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
8
server/website/website.go
Normal file
8
server/website/website.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package website
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed *
|
||||||
|
//go:embed css/*
|
||||||
|
//go:embed js/*
|
||||||
|
var Assets embed.FS
|
Loading…
Reference in New Issue
Block a user