16 Commits
Author SHA1 Message Date
phoenixandphoenix 0d252bc261 tsk-51: Send message endpoint (#52)
Closes #51

Reviewed-on: phoenix/textsender-api#52
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-10 21:35:02 +00:00
phoenixandphoenix 20ffc45f89 tsk-49: Add catapult to docker (#50)
Closes #49

Reviewed-on: phoenix/textsender-api#50
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-02 22:07:00 +00:00
phoenixandphoenix fddf0f7f87 tsk-44: Record when message was sent (#48)
Closes #44

Reviewed-on: phoenix/textsender-api#48
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-01 21:29:31 +00:00
phoenixandphoenix 34262cda68 tsk-43: Added message limit code (#47)
Closes #43

Reviewed-on: phoenix/textsender-api#47
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-29 20:48:14 +00:00
phoenixandphoenix a8dbd693ba tsk-40: Get Contact bug (#46)
Closes #40

Reviewed-on: phoenix/textsender-api#46
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-29 20:22:31 +00:00
phoenixandphoenix 1646428016 tsk-41: Go version bump (#42)
Closes #41

Reviewed-on: phoenix/textsender-api#42
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-24 21:21:24 +00:00
phoenixandphoenix f69d56f527 tsk-35: Add API documentation (#39)
Closes #35

Reviewed-on: phoenix/textsender-api#39
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-13 21:45:17 +00:00
phoenixandphoenix 0cd71c5506 tsk-34: Fetch next ready scheduled message (#38)
Closes #34

Reviewed-on: phoenix/textsender-api#38
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-13 01:59:39 +00:00
phoenixandphoenix 3218c69f1e tsk-33: Tweak get schedule message event endpoint (#37)
Closes #33

Reviewed-on: phoenix/textsender-api#37
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-13 00:48:04 +00:00
phoenixandphoenix 7f588cc80c tsk-13: Retrieve scheduled message endpoint (#36)
Closes #13

Reviewed-on: phoenix/textsender-api#36
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-12 19:25:54 +00:00
phoenixandphoenix a37f1627fc tsk-19: Update status of scheduled message endpoint (#32)
Closes #19

Reviewed-on: phoenix/textsender-api#32
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-12 17:54:32 +00:00
phoenixandphoenix d191d6a9c1 tsk-27: Added endpoint to delete scheduled message event (#31)
Closes #27

Reviewed-on: phoenix/textsender-api#31
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-11 18:35:04 +00:00
phoenixandphoenix 2716bb50d3 tsk-29: Place restrictions on adding scheduled message event (#30)
Closes #29

Reviewed-on: phoenix/textsender-api#30
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-11 16:43:51 +00:00
phoenixandphoenix 880461dffb tsk-26: Retrieve scheduled message event endpoint (#28)
Closes #26

Reviewed-on: phoenix/textsender-api#28
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-10 20:22:40 +00:00
phoenixandphoenix 77c950d299 tsk-24: Fixed tests (#25)
Closes #24

Reviewed-on: phoenix/textsender-api#25
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-10 17:42:41 +00:00
phoenixandphoenix 44e00684cc tsk-20: Add event to scheduled message (#23)
Closes #20

Reviewed-on: phoenix/textsender-api#23
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-10 16:25:34 +00:00
44 changed files with 5109 additions and 202 deletions
+4
View File
@@ -5,3 +5,7 @@ DB_PASSWORD=password
DB_HOST=main_db DB_HOST=main_db
DB_PORT=5432 DB_PORT=5432
DB_SSLMODE=disable DB_SSLMODE=disable
TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U
TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T
TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1"
TWILIO_PHONE_NUMBER=+10123456789
+11
View File
@@ -0,0 +1,11 @@
JWT_SECRET=NULqYIzgt28bTiyziCd7IOO7b6LnWDW!
DB_NAME=textsender_db
DB_USER=textsender
DB_PASSWORD=password
DB_HOST=localhost
DB_PORT=5432
DB_SSLMODE=disable
TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U
TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T
TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1"
TWILIO_PHONE_NUMBER=+10123456789
-7
View File
@@ -1,7 +0,0 @@
JWT_SECRET=NULqYIzgt28bTiyziCd7IOO7b6LnWDW!
DB_NAME=textsender_db
DB_USER=textsender
DB_PASSWORD=password
DB_HOST=localhost
DB_PORT=5432
DB_SSLMODE=disable
+10 -9
View File
@@ -14,9 +14,9 @@ jobs:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v6
with: with:
go-version: '1.25.3' # You can specify a specific version or 'stable' go-version: '1.25.4' # You can specify a specific version or 'stable'
- name: Build - name: Build
run: | run: |
@@ -24,7 +24,7 @@ jobs:
mkdir -p ~/.ssh mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender_models_deploy_key echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender_models_deploy_key
chmod 600 ~/.ssh/textsender_models_deploy_key chmod 600 ~/.ssh/textsender_models_deploy_key
ssh-keyscan ${{ vars.MY_HOST }} >> ~/.ssh/known_hosts ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s) eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender_models_deploy_key ssh-add -v ~/.ssh/textsender_models_deploy_key
@@ -96,22 +96,23 @@ jobs:
DB_SSLMODE: disable DB_SSLMODE: disable
SECRET_KEY: ${{ secrets.SECRET_KEY }} SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: | run: |
echo "Parent directory" echo "JWT_SECRET=$SECRET_KEY" > .env
echo `pwd` echo "DB_NAME=$DB_NAME" >> .env
echo "JWT_SECRET=$SECRET_KEY" >> .env
echo "DB_NAME=$DB_NAME" > .env
echo "DB_USER=$DB_USER" >> .env echo "DB_USER=$DB_USER" >> .env
echo "DB_PASSWORD=$DB_PASSWORD" >> .env echo "DB_PASSWORD=$DB_PASSWORD" >> .env
echo "DB_HOST=$DB_HOST" >> .env echo "DB_HOST=$DB_HOST" >> .env
echo "DB_PORT=$DB_PORT" >> .env echo "DB_PORT=$DB_PORT" >> .env
echo "DB_SSLMODE=$DB_SSLMODE" >> .env echo "DB_SSLMODE=$DB_SSLMODE" >> .env
echo "TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U" >> .env
echo "TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T" >> .env
echo "TWILIO_AUTH_TOKEN=f4a1f2b0b79ea3735078c2d8ee9684e1" >> .env
echo "TWILIO_PHONE_NUMBER=10123456789" >> .env
echo "Initializing config" echo "Initializing config"
mkdir -p ~/.ssh mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender_models_deploy_key echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender_models_deploy_key
chmod 600 ~/.ssh/textsender_models_deploy_key chmod 600 ~/.ssh/textsender_models_deploy_key
ssh-keyscan ${{ vars.MY_HOST }} >> ~/.ssh/known_hosts ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s) eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender_models_deploy_key ssh-add -v ~/.ssh/textsender_models_deploy_key
+2 -1
View File
@@ -1,5 +1,5 @@
# Multi-stage Dockerfile for Go application # Multi-stage Dockerfile for Go application
FROM golang:1.25.3 AS builder FROM golang:1.25.4 AS builder
WORKDIR /app WORKDIR /app
@@ -29,6 +29,7 @@ COPY ./internal ./internal
COPY ./Makefile . COPY ./Makefile .
COPY ./.env . COPY ./.env .
COPY ./migrations ./migrations COPY ./migrations ./migrations
COPY ./docs ./docs
# Build the application # Build the application
RUN CGO_ENABLED=0 GOOS=linux make build RUN CGO_ENABLED=0 GOOS=linux make build
+12 -1
View File
@@ -4,7 +4,18 @@ A software system to process Text messaging queueing
## Getting started ## Getting started
### Building ### Building
```BASH ```SHELL
make build make build
``` ```
Generate API documentation
```
go install github.com/swaggo/swag/cmd/swag@latest
swag init --generalInfo main.go --dir ./cmd/api,./internal/handler --output docs/ --parseDependency --parseInternal
```
The API documentation can be viewed from `http://localhost:8080/swagger/index.html`.
+50 -17
View File
@@ -12,7 +12,10 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/swaggo/http-swagger/v2"
_ "git.kundeng.us/phoenix/textsender-api/docs"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/config" "git.kundeng.us/phoenix/textsender-api/internal/config"
database "git.kundeng.us/phoenix/textsender-api/internal/db" database "git.kundeng.us/phoenix/textsender-api/internal/db"
"git.kundeng.us/phoenix/textsender-api/internal/handler" "git.kundeng.us/phoenix/textsender-api/internal/handler"
@@ -22,19 +25,32 @@ import (
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
) )
// @title textsender-api
// @version 1.0
// @description Core API to send text messages
// @host localhost:8080
// @BasePath /api/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description JWT Bearer Token
func main() { func main() {
cfg := config.Load() cfg, tCfg, err := config.Load()
if cfg == nil { if err != nil {
fmt.Println("Error initializing config") log.Fatal("Error:", err)
os.Exit(-1) } else if cfg == nil {
log.Fatal("Error initializing config")
} else if cfg.JWTSecret == "" { } else if cfg.JWTSecret == "" {
fmt.Println("Error: JWTSecret not initialized") log.Fatal("Error: JWTSecret not initialized")
os.Exit(-1)
} }
apiApp := app.App{TwilioConfig: tCfg}
db, err := database.NewDatabase(cfg.GetDBConnString()) db, err := database.NewDatabase(cfg.GetDBConnString())
if err != nil { if err != nil {
log.Fatalf("Failed to connect to database: %v", err) log.Fatal("Failed to connect to database:", err)
} }
defer db.Close() defer db.Close()
@@ -43,19 +59,18 @@ func main() {
if cfg.ResetDB { if cfg.ResetDB {
log.Println("Resetting database") log.Println("Resetting database")
if err := db.ResetDatabase(ctx); err != nil { if err := db.ResetDatabase(ctx); err != nil {
log.Fatalf("Failed to reset database: %v", err) log.Fatal("Failed to reset database:", err)
} }
log.Println("Database reset completed. Exiting.") log.Println("Database reset completed. Exiting.")
return return
} else { } else {
if exists, err := database.TableExists(ctx, db.Pool, "contacts "); err == nil && !exists { if exists, err := database.TableExists(ctx, db.Pool, "contacts"); err == nil && !exists {
fmt.Println("Resetting database") log.Println("Resetting database")
err = db.ResetDatabase(ctx) if err = db.ResetDatabase(ctx); err != nil {
if err != nil { log.Fatal("Error:", err)
fmt.Printf("Error:%v", err)
} }
} else { } else {
fmt.Printf("Error:%v", err) log.Fatal("Error:", err)
} }
} }
@@ -64,10 +79,16 @@ func main() {
contactStore := store.NewContactStore(db.Pool) contactStore := store.NewContactStore(db.Pool)
messageStore := store.NewMessageStore(db.Pool) messageStore := store.NewMessageStore(db.Pool)
schStore := store.NewScheduledMessageStore(db.Pool) schStore := store.NewScheduledMessageStore(db.Pool)
schMsgEventStore := store.NewScheduledMessageEventStore(db.Pool)
merStore := store.NewMessageEventResponseStore(db.Pool)
contactHandler := handler.NewContactHandler(contactStore) contactHandler := handler.NewContactHandler(&apiApp, contactStore)
messageHandler := handler.NewMessageHandler(messageStore) messageHandler := handler.NewMessageHandler(&apiApp, messageStore)
scheduledMessageHandler := handler.NewScheduledMessageHandler(schStore) scheduledMessageHandler := handler.NewScheduledMessageHandler(&apiApp, schStore)
scheduledMessageEventHandler := handler.NewScheduledMessageEventHandler(&apiApp, schMsgEventStore, schStore)
scheduledMessageStatusHandler := handler.NewScheduledMessageStatusHandler(&apiApp, schMsgEventStore, schStore)
eventHandler := handler.NewEventResponseHandler(&apiApp, merStore)
instantMessageHandler := handler.NewSendInstantMessageHandler(&apiApp, merStore, contactStore, messageStore)
router := chi.NewRouter() router := chi.NewRouter()
@@ -81,6 +102,18 @@ func main() {
router.Handle(endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage))) router.Handle(endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage)))
router.Handle(endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage))) router.Handle(endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage)))
router.Handle(endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage))) router.Handle(endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage)))
router.Handle(endpoint.AddEventToScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.AddScheduledMessageEvent)))
router.Method("GET", endpoint.GetScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.GetScheduledMessageEvent)))
router.Method("DELETE", endpoint.DeleteScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.DeleteScheduledMessageEvent)))
router.Method("PATCH", endpoint.UpdateScheduledMessageStatusEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageStatusHandler.UpdateStatus)))
router.Method("GET", endpoint.GetScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.GetScheduledMessage)))
router.Method("GET", endpoint.FetchNextScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.FetchNextMessage)))
router.Method("POST", endpoint.RecordEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.RecordResponse)))
router.Method("POST", endpoint.SendInstantMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(instantMessageHandler.Send)))
router.Method("GET", "/swagger/*", httpSwagger.Handler(
httpSwagger.URL(fmt.Sprintf("http://localhost:%s/swagger/doc.json", config.PORT)),
))
// Start server // Start server
server := &http.Server{ server := &http.Server{
+62 -18
View File
@@ -4,50 +4,89 @@ import (
"context" "context"
"flag" "flag"
"fmt" "fmt"
"log"
"net/http"
"os" "os"
"path" "path"
"testing" "testing"
"github.com/gorilla/mux" "github.com/go-chi/chi/v5"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/config" "git.kundeng.us/phoenix/textsender-api/internal/config"
"git.kundeng.us/phoenix/textsender-api/internal/db" "git.kundeng.us/phoenix/textsender-api/internal/db"
"git.kundeng.us/phoenix/textsender-api/internal/handler" "git.kundeng.us/phoenix/textsender-api/internal/handler"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
mdlware "git.kundeng.us/phoenix/textsender-api/internal/middleware"
"git.kundeng.us/phoenix/textsender-api/internal/services"
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
) )
var testRouter *mux.Router var testRouter *chi.Mux
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
cfg := load() cfg, apiApp := load()
db, err := db.NewDatabase(cfg.GetDBConnString()) database, err := db.NewDatabase(cfg.GetDBConnString())
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
panic("Failed to initialize database") panic("Failed to initialize database")
} }
defer db.Close() defer database.Close()
ctx := context.Background() ctx := context.Background()
err = db.ResetDatabase(ctx)
if err != nil { if cfg.ResetDB {
fmt.Println(err.Error()) if err = database.ResetDatabase(ctx); err != nil {
panic("Failed to initialize database") fmt.Println(err.Error())
panic("Failed to initialize database")
}
} else {
if exists, err := db.TableExists(ctx, database.Pool, "contacts "); err == nil && !exists {
fmt.Println("Resetting database")
err = database.ResetDatabase(ctx)
if err != nil {
fmt.Printf("Error:%v", err)
}
} else {
fmt.Printf("Error:%v", err)
}
} }
contactStore := store.NewContactStore(db.Pool) jwtService := services.NewJWTService(cfg.JWTSecret)
contactHandler := handler.NewContactHandler(contactStore)
testRouter = mux.NewRouter() contactStore := store.NewContactStore(database.Pool)
testRouter.HandleFunc(endpoint.ADD_CONTACT_ENDPOINT, contactHandler.AddContact).Methods("POST") messageStore := store.NewMessageStore(db.Pool)
schStore := store.NewScheduledMessageStore(db.Pool)
schMsgEventStore := store.NewScheduledMessageEventStore(db.Pool)
merStore := store.NewMessageEventResponseStore(db.Pool)
contactHandler := handler.NewContactHandler(apiApp, contactStore)
messageHandler := handler.NewMessageHandler(apiApp, messageStore)
scheduledMessageHandler := handler.NewScheduledMessageHandler(apiApp, schStore)
scheduledMessageEventHandler := handler.NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schStore)
scheduledMessageStatusHandler := handler.NewScheduledMessageStatusHandler(apiApp, schMsgEventStore, schStore)
eventHandler := handler.NewEventResponseHandler(apiApp, merStore)
testRouter = chi.NewRouter()
testRouter.Handle(endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact)))
testRouter.Handle(endpoint.GET_CONTACT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.GetContact)))
testRouter.Handle(endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage)))
testRouter.Handle(endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage)))
testRouter.Handle(endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage)))
testRouter.Method("GET", endpoint.GetScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.GetScheduledMessageEvent)))
testRouter.Method("DELETE", endpoint.DeleteScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.DeleteScheduledMessageEvent)))
testRouter.Method("PATCH", endpoint.UpdateScheduledMessageStatusEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageStatusHandler.UpdateStatus)))
testRouter.Method("GET", endpoint.GetScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.GetScheduledMessage)))
testRouter.Method("GET", endpoint.FetchNextScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.FetchNextMessage)))
testRouter.Method("POST", endpoint.RecordEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.RecordResponse)))
code := m.Run() code := m.Run()
os.Exit(code) os.Exit(code)
} }
func load() *config.Config { func load() (*config.Config, *app.App) {
resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit") resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit")
port := flag.String("port", config.PORT, "Server port") port := flag.String("port", config.PORT, "Server port")
flag.Parse() flag.Parse()
@@ -66,10 +105,15 @@ func load() *config.Config {
unpackedConnString := config.UnpackDBConnString() unpackedConnString := config.UnpackDBConnString()
dbConnString := unpackedConnString.Parse() dbConnString := unpackedConnString.Parse()
return &config.Config{ if tCfg, err := config.TwilioConfig(); err != nil {
DBConnString: dbConnString, log.Fatal("Error: ", err)
ServerPort: *port, return nil, nil
ResetDB: *resetDb, } else {
return &config.Config{
DBConnString: dbConnString,
ServerPort: *port,
ResetDB: *resetDb,
}, &app.App{TwilioConfig: tCfg}
} }
} }
+17 -2
View File
@@ -5,7 +5,7 @@ services:
build: # Tells docker-compose to build the Dockerfile in the current directory build: # Tells docker-compose to build the Dockerfile in the current directory
context: . context: .
ssh: ["default"] # Uses host's SSH agent ssh: ["default"] # Uses host's SSH agent
container_name: textsender # Optional: Give the container a specific name container_name: textsender_api # Optional: Give the container a specific name
ports: ports:
# Map host port 8000 to container port 3000 (adjust as needed) # Map host port 8000 to container port 3000 (adjust as needed)
- "8080:8080" - "8080:8080"
@@ -19,7 +19,7 @@ services:
restart: unless-stopped # Optional: Restart policy restart: unless-stopped # Optional: Restart policy
auth_api: auth_api:
build: # Tells docker-compose to build the Dockerfile in the current directory build:
context: ../textsender-auth context: ../textsender-auth
ssh: ["default"] # Uses host's SSH agent ssh: ["default"] # Uses host's SSH agent
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -36,6 +36,21 @@ services:
- textsender-network - textsender-network
restart: unless-stopped # Optional: Restart policy restart: unless-stopped # Optional: Restart policy
catapult:
build:
context: ../catapult
ssh: ["default"]
dockerfile: Dockerfile
container_name: catapult
env_file:
- ../catapult/.env
depends_on:
- auth_api
- api
networks:
- textsender-network
restart: unless-stopped
# PostgreSQL Database Service # PostgreSQL Database Service
main_db: main_db:
image: postgres:18.0-alpine # Use an official Postgres image (Alpine variant is smaller) image: postgres:18.0-alpine # Use an official Postgres image (Alpine variant is smaller)
+1017
View File
File diff suppressed because it is too large Load Diff
+993
View File
@@ -0,0 +1,993 @@
{
"swagger": "2.0",
"info": {
"description": "Core API to send text messages",
"title": "textsender-api",
"contact": {},
"version": "1.0"
},
"host": "localhost:8080",
"basePath": "/api/v1",
"paths": {
"/contact": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get a contact record to have a recipient to send a text to (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Get contact",
"parameters": [
{
"type": "string",
"description": "Contact Id",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "User Id",
"name": "user_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.GetContactResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.GetContactResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.GetContactResponse"
}
}
}
}
},
"/contact/new": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Add a contact record to have a recipient to send a text to (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"contacts"
],
"summary": "Add contact",
"parameters": [
{
"description": "Data to add contact",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RequestAddContact"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.AddContactResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.AddContactResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.AddContactResponse"
}
}
}
}
},
"/instant/message": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Send a message without the constraints of scheduled messages (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"messages"
],
"summary": "Send instant message",
"parameters": [
{
"description": "Send an instnt message",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.SendInstantMessageRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.SendInstantMessageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.SendInstantMessageResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.SendInstantMessageResponse"
}
}
}
}
},
"/message": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get a message record to have a recipient to send a text to (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"messages"
],
"summary": "Get message",
"parameters": [
{
"type": "string",
"description": "Message Id",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "User Id",
"name": "user_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.GetMessageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.GetMessageResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.GetMessageResponse"
}
}
}
}
},
"/message/new": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Add a message record to send a text to (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"messages"
],
"summary": "Add message",
"parameters": [
{
"description": "Data to add message",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RequestAddMessage"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.AddMessageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.AddMessageResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.AddMessageResponse"
}
}
}
}
},
"/schedule/message": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Get a scheduled message (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"scheduled messages"
],
"summary": "Get scheduled message",
"parameters": [
{
"type": "string",
"description": "Contact Id",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "User Id",
"name": "user_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.GetScheduledMessageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.GetScheduledMessageResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.GetScheduledMessageResponse"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Adds a scheduled message (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"scheduled messages"
],
"summary": "Add scheduled message",
"parameters": [
{
"description": "Data to add scheduled message",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RequestAddScheduledMessage"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.AddScheduledMessageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.AddScheduledMessageResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.AddScheduledMessageResponse"
}
}
}
}
},
"/schedule/message/event": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Gets a scheduled message event (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"scheduled message events"
],
"summary": "Get scheduled message event",
"parameters": [
{
"type": "string",
"description": "Contact Id",
"name": "id",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Scheduled Message Id",
"name": "scheduled_message_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.GetScheduledMessageEventResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.GetScheduledMessageEventResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.GetScheduledMessageEventResponse"
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Add a scheduled message event (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"scheduled message events"
],
"summary": "Add scheduled message event",
"parameters": [
{
"description": "Data to add scheduled message event",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RequestAddScheduledMessageEvent"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.AddScheduledMessageEventResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.AddScheduledMessageEventResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.AddScheduledMessageEventResponse"
}
}
}
}
},
"/schedule/message/event/{id}": {
"delete": {
"security": [
{
"BearerAuth": []
}
],
"description": "Deletes a scheduled message event (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"scheduled message events"
],
"summary": "Delete scheduled message event",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "scheduled message event Id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.DeleteScheduledMessageEventResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.DeleteScheduledMessageEventResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.DeleteScheduledMessageEventResponse"
}
}
}
}
},
"/schedule/message/fetch": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Fetches a scheduled message that is available to be processed (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"scheduled messages"
],
"summary": "Fetch scheduled message",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.FetchNextMessageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.FetchNextMessageResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.FetchNextMessageResponse"
}
}
}
}
},
"/schedule/message/status/update": {
"patch": {
"security": [
{
"BearerAuth": []
}
],
"description": "Update the status of a scheduled message (requires JWT)",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"status"
],
"summary": "Update status",
"parameters": [
{
"description": "Used to update scheduled message status",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.RequestScheduledMessageStatus"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/handler.ScheduledMessageStatusResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/handler.ScheduledMessageStatusResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/handler.ScheduledMessageStatusResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/handler.ScheduledMessageStatusResponse"
}
}
}
}
}
},
"definitions": {
"contact.Contact": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.AddContactResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/contact.Contact"
}
},
"message": {
"type": "string"
}
}
},
"handler.AddMessageResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/message.Message"
}
},
"message": {
"type": "string"
}
}
},
"handler.AddScheduledMessageEventResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/scheduling.ScheduledMessageEvent"
}
},
"message": {
"type": "string"
}
}
},
"handler.AddScheduledMessageResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/scheduling.ScheduledMessage"
}
},
"message": {
"type": "string"
}
}
},
"handler.DeleteScheduledMessageEventResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/scheduling.ScheduledMessageEvent"
}
},
"message": {
"type": "string"
}
}
},
"handler.FetchNextMessageResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/scheduling.ScheduledMessage"
}
},
"message": {
"type": "string"
}
}
},
"handler.GetContactResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/contact.Contact"
}
},
"message": {
"type": "string"
}
}
},
"handler.GetMessageResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/message.Message"
}
},
"message": {
"type": "string"
}
}
},
"handler.GetScheduledMessageEventResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/scheduling.ScheduledMessageEvent"
}
},
"message": {
"type": "string"
}
}
},
"handler.GetScheduledMessageResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/scheduling.ScheduledMessage"
}
},
"message": {
"type": "string"
}
}
},
"handler.RequestAddContact": {
"type": "object",
"properties": {
"phone_number": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.RequestAddMessage": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.RequestAddScheduledMessage": {
"type": "object",
"properties": {
"scheduled": {
"type": "string"
},
"status": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.RequestAddScheduledMessageEvent": {
"type": "object",
"properties": {
"message_id": {
"type": "string"
},
"recipient_id": {
"type": "string"
},
"scheduled_message_id": {
"type": "string"
}
}
},
"handler.RequestScheduledMessageStatus": {
"type": "object",
"properties": {
"scheduled_message_id": {
"type": "string"
},
"status": {
"type": "string"
}
}
},
"handler.ScheduledMessageChange": {
"type": "object",
"properties": {
"old_status": {
"type": "string"
},
"scheduled_message": {
"$ref": "#/definitions/scheduling.ScheduledMessage"
}
}
},
"handler.ScheduledMessageStatusResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/handler.ScheduledMessageChange"
}
},
"message": {
"type": "string"
}
}
},
"handler.SendInstantMessageRequest": {
"type": "object",
"properties": {
"contact_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"message_id": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"handler.SendInstantMessageResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/definitions/message.MessageEventResponse"
}
},
"message": {
"type": "string"
}
}
},
"message.Message": {
"type": "object",
"properties": {
"content": {
"type": "string"
},
"id": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"message.MessageEventResponse": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"response": {
"type": "array",
"items": {
"type": "integer"
}
},
"scheduled_message_event_id": {
"type": "string"
},
"sent": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"scheduling.ScheduledMessage": {
"type": "object",
"properties": {
"created": {
"type": "string"
},
"id": {
"type": "string"
},
"scheduled": {
"type": "string"
},
"status": {
"type": "string"
},
"user_id": {
"type": "string"
}
}
},
"scheduling.ScheduledMessageEvent": {
"type": "object",
"properties": {
"created": {
"type": "string"
},
"id": {
"type": "string"
},
"message_id": {
"type": "string"
},
"recipient_id": {
"type": "string"
},
"scheduled_message_id": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"BearerAuth": {
"description": "JWT Bearer Token",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
}
}
+640
View File
@@ -0,0 +1,640 @@
basePath: /api/v1
definitions:
contact.Contact:
properties:
id:
type: string
phone_number:
type: string
user_id:
type: string
type: object
handler.AddContactResponse:
properties:
data:
items:
$ref: '#/definitions/contact.Contact'
type: array
message:
type: string
type: object
handler.AddMessageResponse:
properties:
data:
items:
$ref: '#/definitions/message.Message'
type: array
message:
type: string
type: object
handler.AddScheduledMessageEventResponse:
properties:
data:
items:
$ref: '#/definitions/scheduling.ScheduledMessageEvent'
type: array
message:
type: string
type: object
handler.AddScheduledMessageResponse:
properties:
data:
items:
$ref: '#/definitions/scheduling.ScheduledMessage'
type: array
message:
type: string
type: object
handler.DeleteScheduledMessageEventResponse:
properties:
data:
items:
$ref: '#/definitions/scheduling.ScheduledMessageEvent'
type: array
message:
type: string
type: object
handler.FetchNextMessageResponse:
properties:
data:
items:
$ref: '#/definitions/scheduling.ScheduledMessage'
type: array
message:
type: string
type: object
handler.GetContactResponse:
properties:
data:
items:
$ref: '#/definitions/contact.Contact'
type: array
message:
type: string
type: object
handler.GetMessageResponse:
properties:
data:
items:
$ref: '#/definitions/message.Message'
type: array
message:
type: string
type: object
handler.GetScheduledMessageEventResponse:
properties:
data:
items:
$ref: '#/definitions/scheduling.ScheduledMessageEvent'
type: array
message:
type: string
type: object
handler.GetScheduledMessageResponse:
properties:
data:
items:
$ref: '#/definitions/scheduling.ScheduledMessage'
type: array
message:
type: string
type: object
handler.RequestAddContact:
properties:
phone_number:
type: string
user_id:
type: string
type: object
handler.RequestAddMessage:
properties:
content:
type: string
user_id:
type: string
type: object
handler.RequestAddScheduledMessage:
properties:
scheduled:
type: string
status:
type: string
user_id:
type: string
type: object
handler.RequestAddScheduledMessageEvent:
properties:
message_id:
type: string
recipient_id:
type: string
scheduled_message_id:
type: string
type: object
handler.RequestScheduledMessageStatus:
properties:
scheduled_message_id:
type: string
status:
type: string
type: object
handler.ScheduledMessageChange:
properties:
old_status:
type: string
scheduled_message:
$ref: '#/definitions/scheduling.ScheduledMessage'
type: object
handler.ScheduledMessageStatusResponse:
properties:
data:
items:
$ref: '#/definitions/handler.ScheduledMessageChange'
type: array
message:
type: string
type: object
handler.SendInstantMessageRequest:
properties:
contact_ids:
items:
type: string
type: array
message_id:
type: string
user_id:
type: string
type: object
handler.SendInstantMessageResponse:
properties:
data:
items:
$ref: '#/definitions/message.MessageEventResponse'
type: array
message:
type: string
type: object
message.Message:
properties:
content:
type: string
id:
type: string
user_id:
type: string
type: object
message.MessageEventResponse:
properties:
id:
type: string
response:
items:
type: integer
type: array
scheduled_message_event_id:
type: string
sent:
type: string
user_id:
type: string
type: object
scheduling.ScheduledMessage:
properties:
created:
type: string
id:
type: string
scheduled:
type: string
status:
type: string
user_id:
type: string
type: object
scheduling.ScheduledMessageEvent:
properties:
created:
type: string
id:
type: string
message_id:
type: string
recipient_id:
type: string
scheduled_message_id:
type: string
type: object
host: localhost:8080
info:
contact: {}
description: Core API to send text messages
title: textsender-api
version: "1.0"
paths:
/contact:
get:
consumes:
- application/json
description: Get a contact record to have a recipient to send a text to (requires
JWT)
parameters:
- description: Contact Id
in: path
name: id
required: true
type: string
- description: User Id
in: path
name: user_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.GetContactResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.GetContactResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.GetContactResponse'
security:
- BearerAuth: []
summary: Get contact
tags:
- contacts
/contact/new:
post:
consumes:
- application/json
description: Add a contact record to have a recipient to send a text to (requires
JWT)
parameters:
- description: Data to add contact
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.RequestAddContact'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/handler.AddContactResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.AddContactResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.AddContactResponse'
security:
- BearerAuth: []
summary: Add contact
tags:
- contacts
/instant/message:
post:
consumes:
- application/json
description: Send a message without the constraints of scheduled messages (requires
JWT)
parameters:
- description: Send an instnt message
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.SendInstantMessageRequest'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/handler.SendInstantMessageResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.SendInstantMessageResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.SendInstantMessageResponse'
security:
- BearerAuth: []
summary: Send instant message
tags:
- messages
/message:
get:
consumes:
- application/json
description: Get a message record to have a recipient to send a text to (requires
JWT)
parameters:
- description: Message Id
in: path
name: id
required: true
type: string
- description: User Id
in: path
name: user_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.GetMessageResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.GetMessageResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.GetMessageResponse'
security:
- BearerAuth: []
summary: Get message
tags:
- messages
/message/new:
post:
consumes:
- application/json
description: Add a message record to send a text to (requires JWT)
parameters:
- description: Data to add message
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.RequestAddMessage'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/handler.AddMessageResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.AddMessageResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.AddMessageResponse'
security:
- BearerAuth: []
summary: Add message
tags:
- messages
/schedule/message:
get:
consumes:
- application/json
description: Get a scheduled message (requires JWT)
parameters:
- description: Contact Id
in: path
name: id
required: true
type: string
- description: User Id
in: path
name: user_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.GetScheduledMessageResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.GetScheduledMessageResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.GetScheduledMessageResponse'
security:
- BearerAuth: []
summary: Get scheduled message
tags:
- scheduled messages
post:
consumes:
- application/json
description: Adds a scheduled message (requires JWT)
parameters:
- description: Data to add scheduled message
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.RequestAddScheduledMessage'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/handler.AddScheduledMessageResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.AddScheduledMessageResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.AddScheduledMessageResponse'
security:
- BearerAuth: []
summary: Add scheduled message
tags:
- scheduled messages
/schedule/message/event:
get:
consumes:
- application/json
description: Gets a scheduled message event (requires JWT)
parameters:
- description: Contact Id
in: path
name: id
required: true
type: string
- description: Scheduled Message Id
in: path
name: scheduled_message_id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.GetScheduledMessageEventResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.GetScheduledMessageEventResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.GetScheduledMessageEventResponse'
security:
- BearerAuth: []
summary: Get scheduled message event
tags:
- scheduled message events
post:
consumes:
- application/json
description: Add a scheduled message event (requires JWT)
parameters:
- description: Data to add scheduled message event
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.RequestAddScheduledMessageEvent'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/handler.AddScheduledMessageEventResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.AddScheduledMessageEventResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.AddScheduledMessageEventResponse'
security:
- BearerAuth: []
summary: Add scheduled message event
tags:
- scheduled message events
/schedule/message/event/{id}:
delete:
consumes:
- application/json
description: Deletes a scheduled message event (requires JWT)
parameters:
- description: scheduled message event Id
format: uuid
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/handler.DeleteScheduledMessageEventResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.DeleteScheduledMessageEventResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.DeleteScheduledMessageEventResponse'
security:
- BearerAuth: []
summary: Delete scheduled message event
tags:
- scheduled message events
/schedule/message/fetch:
get:
consumes:
- application/json
description: Fetches a scheduled message that is available to be processed (requires
JWT)
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.FetchNextMessageResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.FetchNextMessageResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.FetchNextMessageResponse'
security:
- BearerAuth: []
summary: Fetch scheduled message
tags:
- scheduled messages
/schedule/message/status/update:
patch:
consumes:
- application/json
description: Update the status of a scheduled message (requires JWT)
parameters:
- description: Used to update scheduled message status
in: body
name: request
required: true
schema:
$ref: '#/definitions/handler.RequestScheduledMessageStatus'
produces:
- application/json
responses:
"201":
description: Created
schema:
$ref: '#/definitions/handler.ScheduledMessageStatusResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/handler.ScheduledMessageStatusResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/handler.ScheduledMessageStatusResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/handler.ScheduledMessageStatusResponse'
security:
- BearerAuth: []
summary: Update status
tags:
- status
securityDefinitions:
BearerAuth:
description: JWT Bearer Token
in: header
name: Authorization
type: apiKey
swagger: "2.0"
+27 -8
View File
@@ -1,28 +1,47 @@
module git.kundeng.us/phoenix/textsender-api module git.kundeng.us/phoenix/textsender-api
go 1.25.3 go 1.25.4
require ( require (
git.kundeng.us/phoenix/textsender-models v0.0.5-3-ca38ce876f-556 git.kundeng.us/phoenix/swoosh v0.0.7
git.kundeng.us/phoenix/textsender-models v0.0.11
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.3
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/jackc/pgx/v5 v5.7.6 github.com/jackc/pgx/v5 v5.7.6
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger/v2 v2.0.2
github.com/swaggo/swag v1.16.6
) )
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.22.3 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/spec v0.22.1 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/kr/text v0.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/swaggo/files/v2 v2.0.2 // indirect
golang.org/x/crypto v0.42.0 // indirect github.com/twilio/twilio-go v1.28.8 // indirect
golang.org/x/sync v0.17.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/text v0.29.0 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.39.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+90 -10
View File
@@ -1,17 +1,52 @@
git.kundeng.us/phoenix/textsender-models v0.0.5-3-ca38ce876f-556 h1:dDgjre2FlukxeKZfJiP3gtIT4wyzpQ8glAYV9LpLtuU= git.kundeng.us/phoenix/swoosh v0.0.7 h1:9M42tgKr6o34YOiwds7offvjCUoFzYN2B5TrFKbefFo=
git.kundeng.us/phoenix/textsender-models v0.0.5-3-ca38ce876f-556/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no= git.kundeng.us/phoenix/swoosh v0.0.7/go.mod h1:9iXlBd7UCMA7/jj8gZ1CxEfBualsClpiJrqGj8Fo2Xw=
git.kundeng.us/phoenix/textsender-models v0.0.11 h1:kd2FdeZJhJJAXBm8MoyadtgNGyzC+puU1oR8B8N+MfE=
git.kundeng.us/phoenix/textsender-models v0.0.11/go.mod h1:9iPDQJg1Tc6WMNoW5+f8YKmnosMwlWHJ++hmxNLDEe0=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/spec v0.22.1 h1:beZMa5AVQzRspNjvhe5aG1/XyBSMeX1eEOs7dMoXh/k=
github.com/go-openapi/spec v0.22.1/go.mod h1:c7aeIQT175dVowfp7FeCvXXnjN/MrpaONStibD2WtDA=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -24,8 +59,15 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q=
github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -35,13 +77,51 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/twilio/twilio-go v1.28.8 h1:wbFz7Wt4S5mCEaes6FcM/ddcJGIhdjwp/9CHb9e+4fk=
github.com/twilio/twilio-go v1.28.8/go.mod h1:FpgNWMoD8CFnmukpKq9RNpUSGXC0BwnbeKZj2YHlIkw=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+9
View File
@@ -0,0 +1,9 @@
package app
import (
"git.kundeng.us/phoenix/textsender-models/tx0/config"
)
type App struct {
TwilioConfig *config.TwiloConfig
}
+39 -10
View File
@@ -3,10 +3,10 @@ package config
import ( import (
"flag" "flag"
"fmt" "fmt"
"log"
"os" "os"
"strconv" "strconv"
"git.kundeng.us/phoenix/textsender-models/tx0/config"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-api/internal/version" "git.kundeng.us/phoenix/textsender-api/internal/version"
@@ -17,6 +17,7 @@ type Config struct {
ServerPort string ServerPort string
ResetDB bool ResetDB bool
JWTSecret string `env:"JWT_SECRET" required:"true"` JWTSecret string `env:"JWT_SECRET" required:"true"`
TwilioConfig *config.TwiloConfig
} }
type ConnectionInfo struct { type ConnectionInfo struct {
@@ -46,7 +47,7 @@ func PrintName() {
fmt.Println(version.String()) fmt.Println(version.String())
} }
func Load() *Config { func Load() (*Config, *config.TwiloConfig, error) {
versionFlag := flag.Bool("version", false, "Print version information") versionFlag := flag.Bool("version", false, "Print version information")
resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit") resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit")
port := flag.String("port", PORT, "Server port") port := flag.String("port", PORT, "Server port")
@@ -57,19 +58,22 @@ func Load() *Config {
os.Exit(-1) os.Exit(-1)
} }
err := godotenv.Load() if err := godotenv.Load(); err != nil {
if err != nil { return nil, nil, fmt.Errorf("Error loading .env file")
log.Fatal("Error loading .env file")
} }
unpackedConnString := UnpackDBConnString() unpackedConnString := UnpackDBConnString()
dbConnString := unpackedConnString.Parse() dbConnString := unpackedConnString.Parse()
return &Config{ if cfg, err := TwilioConfig(); err != nil {
DBConnString: dbConnString, return nil, nil, err
ServerPort: *port, } else {
ResetDB: *resetDb, return &Config{
JWTSecret: os.Getenv("JWT_SECRET"), DBConnString: dbConnString,
ServerPort: *port,
ResetDB: *resetDb,
JWTSecret: os.Getenv("JWT_SECRET"),
}, cfg, nil
} }
} }
@@ -124,6 +128,31 @@ func UnpackDBConnString() (connInfo ConnectionInfo) {
return return
} }
func TwilioConfig() (*config.TwiloConfig, error) {
authSid := os.Getenv("TWILIO_AUTH_SID")
serviceSid := os.Getenv("TWILIO_SERVICE_SID")
authToken := os.Getenv("TWILIO_AUTH_TOKEN")
phoneNumber := os.Getenv("TWILIO_PHONE_NUMBER")
if len(authSid) == 0 {
return nil, fmt.Errorf("Twilio config account sid not found")
} else if len(serviceSid) == 0 {
return nil, fmt.Errorf("Twilio config service sid not found")
} else if len(authToken) == 0 {
return nil, fmt.Errorf("Twilio config auth token not found")
} else if len(phoneNumber) == 0 {
return nil, fmt.Errorf("Twilio config phone number not found")
} else {
var cfg config.TwiloConfig
cfg.AccountSID = authSid
cfg.ServiceSID = serviceSid
cfg.AuthToken = authToken
cfg.Number = phoneNumber
return &cfg, nil
}
}
func (c *Config) GetDBConnString() string { func (c *Config) GetDBConnString() string {
return c.DBConnString return c.DBConnString
} }
+36 -8
View File
@@ -4,10 +4,11 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"github.com/google/uuid" "github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
) )
type RequestAddContact struct { type RequestAddContact struct {
@@ -20,19 +21,27 @@ type AddContactResponse struct {
Data []contact.Contact `json:"data"` Data []contact.Contact `json:"data"`
} }
type GetContactResponse struct {
Message string `json:"message"`
Data []contact.Contact `json:"data"`
}
type ContactHandler struct { type ContactHandler struct {
App *app.App
ContactStore store.ContactStore ContactStore store.ContactStore
} }
func NewContactHandler(str store.ContactStore) *ContactHandler { func NewContactHandler(apiApp *app.App, str store.ContactStore) *ContactHandler {
return &ContactHandler{ContactStore: str} return &ContactHandler{App: apiApp, ContactStore: str}
} }
// AddContact godoc
// @Summary Add contact
// @Description Add a contact record to have a recipient to send a text to (requires JWT)
// @Tags contacts
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body RequestAddContact true "Data to add contact"
// @Success 201 {object} AddContactResponse
// @Failure 400 {object} AddContactResponse
// @Failure 500 {object} AddContactResponse
// @Router /contact/new [post]
func (c *ContactHandler) AddContact(w http.ResponseWriter, r *http.Request) { func (c *ContactHandler) AddContact(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -76,6 +85,24 @@ func (c *ContactHandler) AddContact(w http.ResponseWriter, r *http.Request) {
RespondWithJSON(w, statusCode, &resp) RespondWithJSON(w, statusCode, &resp)
} }
type GetContactResponse struct {
Message string `json:"message"`
Data []contact.Contact `json:"data"`
}
// GetContact godoc
// @Summary Get contact
// @Description Get a contact record to have a recipient to send a text to (requires JWT)
// @Tags contacts
// @Accept json
// @Produce json
// @Param id path string true "Contact Id"
// @Param user_id path string true "User Id"
// @Security BearerAuth
// @Success 200 {object} GetContactResponse
// @Failure 400 {object} GetContactResponse
// @Failure 500 {object} GetContactResponse
// @Router /contact [get]
func (c *ContactHandler) GetContact(w http.ResponseWriter, r *http.Request) { func (c *ContactHandler) GetContact(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
fmt.Println("Method:", r.Method) fmt.Println("Method:", r.Method)
@@ -83,6 +110,7 @@ func (c *ContactHandler) GetContact(w http.ResponseWriter, r *http.Request) {
return return
} }
// One or the other
var id, userId uuid.UUID var id, userId uuid.UUID
queryParams := r.URL.Query() queryParams := r.URL.Query()
+13 -7
View File
@@ -9,12 +9,13 @@ import (
"strings" "strings"
"testing" "testing"
"git.kundeng.us/phoenix/textsender-models/pkg/contact" "git.kundeng.us/phoenix/textsender-models/tx0/contact"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-api/internal/db" "git.kundeng.us/phoenix/textsender-api/internal/db"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-api/internal/store/mock"
) )
type Request struct { type Request struct {
@@ -23,8 +24,11 @@ type Request struct {
} }
func TestCreateContactWithMock(t *testing.T) { func TestCreateContactWithMock(t *testing.T) {
mockstore := NewMockContactStore() mockstore := mock.NewMockContactStore()
handler := NewContactHandler(mockstore) apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewContactHandler(apiApp, mockstore)
testUserId := uuid.New() testUserId := uuid.New()
testBody := Request{PhoneNumber: "+12335403383", UserId: testUserId} testBody := Request{PhoneNumber: "+12335403383", UserId: testUserId}
@@ -38,7 +42,7 @@ func TestCreateContactWithMock(t *testing.T) {
assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, http.StatusCreated, rr.Code)
var response AddContactResponse var response AddContactResponse
err := json.Unmarshal(rr.Body.Bytes(), &response) err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error Creating contact %v", err) assert.NoError(t, err, "Error Creating contact %v", err)
assert.NotEmpty(t, response.Data, "No Contact created") assert.NotEmpty(t, response.Data, "No Contact created")
@@ -47,7 +51,7 @@ func TestCreateContactWithMock(t *testing.T) {
} }
func TestGetContactWithMock(t *testing.T) { func TestGetContactWithMock(t *testing.T) {
mockstore := NewMockContactStore() mockstore := mock.NewMockContactStore()
testUserId := uuid.New() testUserId := uuid.New()
testCon := contact.Contact{PhoneNumber: "+12335403383", UserId: testUserId} testCon := contact.Contact{PhoneNumber: "+12335403383", UserId: testUserId}
@@ -61,13 +65,15 @@ func TestGetContactWithMock(t *testing.T) {
req, _ := http.NewRequest("GET", url, nil) req, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
contactHandler := NewContactHandler(mockstore) apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
contactHandler := NewContactHandler(apiApp, mockstore)
contactHandler.GetContact(rr, req) contactHandler.GetContact(rr, req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
var response GetContactResponse var response GetContactResponse
err := json.Unmarshal(rr.Body.Bytes(), &response) err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error getting contact %v", err) assert.NoError(t, err, "Error getting contact %v", err)
assert.NotEmpty(t, response.Data, "No Contact retrieved") assert.NotEmpty(t, response.Data, "No Contact retrieved")
+1
View File
@@ -4,6 +4,7 @@ import "net/http"
import "git.kundeng.us/phoenix/textsender-api/internal/model" import "git.kundeng.us/phoenix/textsender-api/internal/model"
// TODO: Remove file
func DraftMessageHandler(w http.ResponseWriter, r *http.Request) { func DraftMessageHandler(w http.ResponseWriter, r *http.Request) {
da := model.MessageItem{ da := model.MessageItem{
Id: 1, Id: 1,
+8
View File
@@ -6,3 +6,11 @@ const GET_MESSAGE = "/api/v1/message"
const GET_CONTACT = "/api/v1/contact" const GET_CONTACT = "/api/v1/contact"
const ADD_CONTACT_ENDPOINT = "/api/v1/contact/new" const ADD_CONTACT_ENDPOINT = "/api/v1/contact/new"
const ScheduleMessageEndpoint = "/api/v1/schedule/message" const ScheduleMessageEndpoint = "/api/v1/schedule/message"
const GetScheduledMessageEndpoint = "/api/v1/schedule/message"
const AddEventToScheduledMessageEndpoint = "/api/v1/schedule/message/event"
const GetScheduledMessageEventEndpoint = "/api/v1/schedule/message/event"
const DeleteScheduledMessageEventEndpoint = "/api/v1/schedule/message/event/{id}"
const UpdateScheduledMessageStatusEndpoint = "/api/v1/schedule/message/status/update"
const FetchNextScheduledMessageEndpoint = "/api/v1/schedule/message/fetch"
const RecordEventResponse = "/api/v1/schedule/message/event/response/record"
const SendInstantMessageEndpoint = "/api/v1/instant/message"
+26 -3
View File
@@ -1,8 +1,11 @@
package handler package handler
import "encoding/json" import (
import "log" "encoding/json"
import "net/http" "fmt"
"log"
"net/http"
)
func ExtractFromRequest(r *http.Request, reqItem interface{}) error { func ExtractFromRequest(r *http.Request, reqItem interface{}) error {
err := json.NewDecoder(r.Body).Decode(&reqItem) err := json.NewDecoder(r.Body).Decode(&reqItem)
@@ -13,7 +16,12 @@ func ExtractFromRequest(r *http.Request, reqItem interface{}) error {
} }
} }
func InvalidJson(err error) string {
return fmt.Sprintf("Invalid JSON: %s", err.Error())
}
// Helper function to send JSON responses // Helper function to send JSON responses
// TODO: Change type of data to any
func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) { func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode) w.WriteHeader(statusCode)
@@ -21,3 +29,18 @@ func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
log.Printf("Error encoding JSON: %v", err) log.Printf("Error encoding JSON: %v", err)
} }
} }
// Gets the query parameter from the URL
func ParseQueryParams(r *http.Request, query string) (*string, error) {
queryParams := r.URL.Query()
if _, exists := queryParams[query]; exists {
value := queryParams.Get(query)
if len(value) == 0 {
return nil, fmt.Errorf("Value of query parameter is empty")
} else {
return &value, nil
}
} else {
return nil, fmt.Errorf("Could not find query %s", query)
}
}
+31
View File
@@ -0,0 +1,31 @@
package handler
import (
"fmt"
"os"
"path"
"github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/config"
)
func GetApp() (*app.App, error) {
cwd, _ := os.Getwd()
envPath := path.Join(cwd, ".env")
err := godotenv.Load(envPath)
if err != nil {
envPath = path.Join(cwd, "../..", ".env")
if err := godotenv.Load(envPath); err != nil {
panic("Error loading .env file: " + err.Error())
}
}
if tCfg, err := config.TwilioConfig(); err != nil {
return nil, fmt.Errorf("Error: %v", err)
} else {
return &app.App{TwilioConfig: tCfg}, nil
}
}
+191
View File
@@ -0,0 +1,191 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"git.kundeng.us/phoenix/swoosh/swoop/send"
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
const Contact_Limit = 400
type SendInstantMessageHandler struct {
App *app.App
MERStore store.MessageEventResponseStore
ContactStore store.ContactStore
MessageStore store.MessageStore
}
func NewSendInstantMessageHandler(apiApp *app.App, str store.MessageEventResponseStore, cStore store.ContactStore, mStore store.MessageStore) *SendInstantMessageHandler {
return &SendInstantMessageHandler{App: apiApp, MERStore: str, ContactStore: cStore, MessageStore: mStore}
}
type SendInstantMessageRequest struct {
ContactIds []uuid.UUID `json:"contact_ids"`
MessageId uuid.UUID `json:"message_id"`
UserId uuid.UUID `json:"user_id"`
}
type SendInstantMessageResponse struct {
Message string `json:"message"`
Data []*message.MessageEventResponse `json:"data"`
}
// Sent godoc
// @Summary Send instant message
// @Description Send a message without the constraints of scheduled messages (requires JWT)
// @Tags messages
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body SendInstantMessageRequest true "Send an instnt message"
// @Success 201 {object} SendInstantMessageResponse
// @Failure 400 {object} SendInstantMessageResponse
// @Failure 500 {object} SendInstantMessageResponse
// @Router /instant/message [post]
func (s *SendInstantMessageHandler) Send(w http.ResponseWriter, r *http.Request) {
var req SendInstantMessageRequest
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, InvalidJson(err), http.StatusBadRequest)
}
defer r.Body.Close()
var statusCode int
var resp SendInstantMessageResponse
if req.UserId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "User Id is nil"
} else if req.MessageId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "Message Id is nil"
} else if len(req.ContactIds) == 0 {
statusCode = http.StatusBadRequest
resp.Message = "No contacts provided"
} else {
if len(req.ContactIds) > Contact_Limit {
statusCode = http.StatusBadRequest
resp.Message = "Too many contacts"
} else {
ctx := r.Context()
if msg, contacts, err := s.verify(ctx, &req); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if msg == nil {
statusCode = http.StatusBadRequest
resp.Message = "Message is empty"
} else if len(contacts) == 0 {
statusCode = http.StatusBadRequest
resp.Message = "Contacts not found"
} else {
if s.App != nil {
msgSender := send.MessageSender{Config: s.App.TwilioConfig}
for _, c := range contacts {
if twilioResp, err := msgSender.Send(*msg, *c, nil); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if data, err := json.Marshal(twilioResp); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
dateStr := strings.TrimSpace(*twilioResp.DateUpdated)
if parsedTime, err := time.Parse(time.RFC1123Z, dateStr); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
mer := message.MessageEventResponse{}
mer.Response = data
mer.UserId = req.UserId
mer.Sent = parsedTime
if err := s.MERStore.Create(ctx, &mer); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, &mer)
}
}
}
}
}
} else {
statusCode = http.StatusInternalServerError
resp.Message = "Config not properly configured"
}
}
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
func (s *SendInstantMessageHandler) verify(ctx context.Context, req *SendInstantMessageRequest) (*message.Message, []*contact.Contact, error) {
if msg, err := s.verifyMessage(ctx, req.MessageId); err != nil {
log.Println("Huh?")
return nil, nil, fmt.Errorf("Error:%v", err)
} else {
log.Println("Okay")
if msg != nil {
if contacts, err := s.verifyContacts(ctx, req.ContactIds); err != nil {
return nil, nil, fmt.Errorf("Error:%v", err)
} else {
if len(contacts) == 0 {
return msg, nil, nil
} else {
return msg, contacts, nil
}
}
} else {
log.Println("How can this be?")
return nil, nil, fmt.Errorf("Could not verify message")
}
}
}
func (s *SendInstantMessageHandler) verifyMessage(ctx context.Context, messageId uuid.UUID) (*message.Message, error) {
if msg, err := s.MessageStore.GetMessageByID(ctx, messageId); err != nil {
return nil, fmt.Errorf("Error getting message:%v", err)
} else {
if msg == nil {
log.Println("Message is nil")
return nil, nil
} else {
return msg, nil
}
}
}
func (s *SendInstantMessageHandler) verifyContacts(ctx context.Context, contactIds []uuid.UUID) ([]*contact.Contact, error) {
var contacts []*contact.Contact
for _, contactId := range contactIds {
if c, err := s.ContactStore.GetContactByID(ctx, contactId); err != nil {
return nil, fmt.Errorf("Error getting contacts:%v", err)
} else {
if c != nil {
contacts = append(contacts, c)
}
}
}
if len(contacts) == 0 {
return nil, nil
} else {
return contacts, nil
}
}
+67 -23
View File
@@ -2,12 +2,14 @@ package handler
import ( import (
"fmt" "fmt"
"log"
"net/http" "net/http"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid" "github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
"git.kundeng.us/phoenix/textsender-models/pkg/message"
) )
type RequestAddMessage struct { type RequestAddMessage struct {
@@ -20,19 +22,29 @@ type AddMessageResponse struct {
Data []message.Message `json:"data"` Data []message.Message `json:"data"`
} }
type GetMessageResponse struct {
Message string `json:"message"`
Data []message.Message `json:"data"`
}
type MessageHandler struct { type MessageHandler struct {
App *app.App
MessageStore store.MessageStore MessageStore store.MessageStore
} }
func NewMessageHandler(str store.MessageStore) *MessageHandler { func NewMessageHandler(apiApp *app.App, str store.MessageStore) *MessageHandler {
return &MessageHandler{MessageStore: str} return &MessageHandler{App: apiApp, MessageStore: str}
} }
const Message_Limit = 200
// AddMessage godoc
// @Summary Add message
// @Description Add a message record to send a text to (requires JWT)
// @Tags messages
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body RequestAddMessage true "Data to add message"
// @Success 201 {object} AddMessageResponse
// @Failure 400 {object} AddMessageResponse
// @Failure 500 {object} AddMessageResponse
// @Router /message/new [post]
func (m *MessageHandler) AddMessage(w http.ResponseWriter, r *http.Request) { func (m *MessageHandler) AddMessage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -49,21 +61,52 @@ func (m *MessageHandler) AddMessage(w http.ResponseWriter, r *http.Request) {
var resp AddMessageResponse var resp AddMessageResponse
newMessage := message.Message{Content: req.Content, UserId: req.UserId} newMessage := message.Message{Content: req.Content, UserId: req.UserId}
ctx := r.Context() if isMessageValid(&newMessage) {
ctx := r.Context()
if err := m.MessageStore.CreateMessage(ctx, &newMessage); err != nil { if err := m.MessageStore.CreateMessage(ctx, &newMessage); err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
resp.Message = err.Error() resp.Message = err.Error()
} else {
statusCode = http.StatusCreated
resp.Message = "Message created"
resp.Data = append(resp.Data, newMessage)
}
} else { } else {
statusCode = http.StatusCreated statusCode = http.StatusBadRequest
resp.Message = "Message created" resp.Message = fmt.Sprintf("Message length exceeds maximum limit. Current size: %d Limit: %d", len(newMessage.Content), Message_Limit)
resp.Data = append(resp.Data, newMessage)
} }
RespondWithJSON(w, statusCode, &resp) RespondWithJSON(w, statusCode, &resp)
} }
func isMessageValid(msg *message.Message) bool {
if len(msg.Content) <= Message_Limit {
return true
} else {
return false
}
}
type GetMessageResponse struct {
Message string `json:"message"`
Data []message.Message `json:"data"`
}
// GetMessage godoc
// @Summary Get message
// @Description Get a message record to have a recipient to send a text to (requires JWT)
// @Tags messages
// @Accept json
// @Produce json
// @Param id path string true "Message Id"
// @Param user_id path string true "User Id"
// @Security BearerAuth
// @Success 200 {object} GetMessageResponse
// @Failure 400 {object} GetMessageResponse
// @Failure 500 {object} GetMessageResponse
// @Router /message [get]
func (c *MessageHandler) GetMessage(w http.ResponseWriter, r *http.Request) { func (c *MessageHandler) GetMessage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet { if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -103,7 +146,7 @@ func (c *MessageHandler) GetMessage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
if id != uuid.Nil { if id != uuid.Nil {
fmt.Println("Checking with Id") log.Println("Checking with Id")
if con, err := c.MessageStore.GetMessageByID(ctx, id); err == nil { if con, err := c.MessageStore.GetMessageByID(ctx, id); err == nil {
statusCode = http.StatusOK statusCode = http.StatusOK
resp.Message = "Successful" resp.Message = "Successful"
@@ -113,11 +156,12 @@ func (c *MessageHandler) GetMessage(w http.ResponseWriter, r *http.Request) {
resp.Message = err.Error() resp.Message = err.Error()
} }
} else if userId != uuid.Nil { } else if userId != uuid.Nil {
fmt.Println("Checking with User Id") log.Println("Checking with User Id")
if contacts, err := c.MessageStore.GetAllMessages(ctx); err == nil { if messages, err := c.MessageStore.GetAllMessages(ctx); err == nil {
for _, con := range contacts { log.Println("Amount of messages:", len(messages))
if con.UserId == userId { for _, msg := range messages {
resp.Data = append(resp.Data, *con) if msg.UserId == userId {
resp.Data = append(resp.Data, *msg)
} }
} }
@@ -126,7 +170,7 @@ func (c *MessageHandler) GetMessage(w http.ResponseWriter, r *http.Request) {
resp.Message = "Successful" resp.Message = "Successful"
} else { } else {
statusCode = http.StatusNotFound statusCode = http.StatusNotFound
resp.Message = "Contact not found" resp.Message = "Message not found"
} }
} else { } else {
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
@@ -0,0 +1,77 @@
package handler
import (
"net/http"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"git.kundeng.us/phoenix/textsender-models/tx0/types"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
type EventResponseHandler struct {
App *app.App
MessageEventResponseStore store.MessageEventResponseStore
}
func NewEventResponseHandler(apiApp *app.App, str store.MessageEventResponseStore) *EventResponseHandler {
return &EventResponseHandler{App: apiApp, MessageEventResponseStore: str}
}
type RecordEventRequest struct {
ScheduledMessageEventId uuid.UUID `json:"scheduled_message_event_id"`
Response types.JSONB `json:"response"`
UserId uuid.UUID `json:"user_id"`
Sent *time.Time `json:"sent,omitempty"`
}
type RecordEventResponse struct {
Message string `json:"message"`
Data []*message.MessageEventResponse `json:"data"`
}
func (e *EventResponseHandler) RecordResponse(w http.ResponseWriter, r *http.Request) {
var req RecordEventRequest
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
var statusCode int
var rp message.MessageEventResponse
var resp RecordEventResponse
if req.Sent == nil {
statusCode = http.StatusBadRequest
resp.Message = "Sent time is empty"
} else if req.UserId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "User Id is empty"
} else if len(req.Response) == 0 {
statusCode = http.StatusBadRequest
resp.Message = "Response is empty"
} else if req.ScheduledMessageEventId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "Scheduled message event Id is empty"
} else {
rp.ScheduledMessageEventId = req.ScheduledMessageEventId
rp.Response = req.Response
rp.UserId = req.UserId
rp.Sent = *req.Sent
ctx := r.Context()
if err := e.MessageEventResponseStore.Create(ctx, &rp); err != nil {
resp.Message = err.Error()
statusCode = http.StatusInternalServerError
} else {
statusCode = http.StatusCreated
resp.Message = "Successful"
resp.Data = append(resp.Data, &rp)
}
}
RespondWithJSON(w, statusCode, &resp)
}
@@ -0,0 +1,84 @@
package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-api/internal/store/mock"
)
func TestRecordMessageEventResponseWithMock(t *testing.T) {
now := time.Now()
mockStore := mock.NewMockScheduledMessageEventStore()
contactStore := mock.NewMockContactStore()
messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore()
merStore := mock.NewMockMessageEventResponseStore()
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewEventResponseHandler(apiApp, merStore)
recipientId := uuid.New()
messageId := uuid.New()
scheduledMessageId := uuid.New()
testUserId := uuid.New()
con := testContact(recipientId, testUserId)
msg := testMessage(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now)
event := scheduling.ScheduledMessageEvent{}
ctx := t.Context()
if err := contactStore.CreateContact(ctx, &con); err != nil {
assert.NoError(t, err, "Error creating contact: %v", err)
} else if err = messageStore.CreateMessage(ctx, &msg); err != nil {
assert.NoError(t, err, "Error creating message: %v", err)
} else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil {
assert.NoError(t, err, "Error creating scheduled message: %v", err)
}
event.MessageId = msg.Id
event.RecipientId = con.Id
event.ScheduledMessageId = schMsg.Id
if err := mockStore.CreateScheduledMessageEvent(ctx, &event); err != nil {
assert.NoError(t, err, "Error creating scheduled message event: %v", err)
}
sent := now.Add(30 * time.Minute)
bytes := []byte("{\"body\":\"Whoknows?\",\"num_segments\":\"0\",\"direction\":\"outbound-api\",\"from\":\"+12243026041\",\"to\":\"+16303831708\",\"date_updated\":\"Sat,29Nov202519:06:59+0000\",\"uri\":\"/2010-04-01/Accounts/ACefa1ef516314c9d1a68cbd657de49277/Messages/SM1193a529e7f7a840667cd1e0f13ea95a.json\",\"account_sid\":\"ACefa1ef516314c9d1a68cbd657de49277\",\"num_media\":\"0\",\"status\":\"scheduled\",\"messaging_service_sid\":\"MG803f3676706b92eb02e18dd820c447f2\",\"sid\":\"SM1193a529e7f7a840667cd1e0f13ea95a\",\"date_created\":\"Sat,29Nov202519:06:59+0000\",\"api_version\":\"2010-04-01\",\"subresource_uris\":{\"media\":\"/2010-04-01/Accounts/ACefa1ef516314c9d1a68cbd657de49277/Messages/SM1193a529e7f7a840667cd1e0f13ea95a/Media.json\"}}")
testReq := RecordEventRequest{ScheduledMessageEventId: event.Id, Response: bytes, UserId: schMsg.UserId, Sent: &sent}
jsonValue, _ := json.Marshal(testReq)
req, _ := http.NewRequest("POST", endpoint.RecordEventResponse, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.RecordResponse(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response RecordEventResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error parsing message event response:%v", err)
assert.NotEmpty(t, response.Data, "No message event response created")
var msgEventResp message.MessageEventResponse
msgEventResp = *response.Data[0]
assert.NotEmpty(t, msgEventResp.Response, "The response of the message event response is empty")
assert.Equal(t, msgEventResp.Response, testReq.Response, "Responses do not match")
}
+12 -7
View File
@@ -8,11 +8,12 @@ import (
"strings" "strings"
"testing" "testing"
"git.kundeng.us/phoenix/textsender-models/pkg/message" "git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-api/internal/store/mock"
) )
type CreateMessageRequest struct { type CreateMessageRequest struct {
@@ -21,8 +22,10 @@ type CreateMessageRequest struct {
} }
func TestCreateMessageWithMock(t *testing.T) { func TestCreateMessageWithMock(t *testing.T) {
mockStore := NewMockMessageStore() mockStore := mock.NewMockMessageStore()
handler := NewMessageHandler(mockStore) apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewMessageHandler(apiApp, mockStore)
testUserId := uuid.New() testUserId := uuid.New()
testBody := CreateMessageRequest{Content: "Who could tell?", UserId: testUserId} testBody := CreateMessageRequest{Content: "Who could tell?", UserId: testUserId}
@@ -36,7 +39,7 @@ func TestCreateMessageWithMock(t *testing.T) {
assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, http.StatusCreated, rr.Code)
var response AddMessageResponse var response AddMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response) err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error Creating message %v", err) assert.NoError(t, err, "Error Creating message %v", err)
assert.NotEmpty(t, response.Data, "No Message created") assert.NotEmpty(t, response.Data, "No Message created")
@@ -45,7 +48,7 @@ func TestCreateMessageWithMock(t *testing.T) {
} }
func TestGetMessageWithMock(t *testing.T) { func TestGetMessageWithMock(t *testing.T) {
mockstore := NewMockMessageStore() mockstore := mock.NewMockMessageStore()
testUserId := uuid.New() testUserId := uuid.New()
testCon := message.Message{Content: "Who is the one that benefits?", UserId: testUserId} testCon := message.Message{Content: "Who is the one that benefits?", UserId: testUserId}
@@ -59,13 +62,15 @@ func TestGetMessageWithMock(t *testing.T) {
req, _ := http.NewRequest("GET", url, nil) req, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
messageHandler := NewMessageHandler(mockstore) apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
messageHandler := NewMessageHandler(apiApp, mockstore)
messageHandler.GetMessage(rr, req) messageHandler.GetMessage(rr, req)
assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, http.StatusOK, rr.Code)
var response GetMessageResponse var response GetMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response) err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error getting message %v", err) assert.NoError(t, err, "Error getting message %v", err)
assert.NotEmpty(t, response.Data, "No Message retrieved") assert.NotEmpty(t, response.Data, "No Message retrieved")
+169
View File
@@ -0,0 +1,169 @@
package handler
import (
"net/http"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
type RequestScheduledMessageStatus struct {
ScheduledMessageId uuid.UUID `json:"scheduled_message_id"`
Status string `json:"status"`
}
type ScheduledMessageChange struct {
OldStatus string `json:"old_status"`
ScheduledMessage scheduling.ScheduledMessage `json:"scheduled_message"`
}
type ScheduledMessageStatusResponse struct {
Message string `json:"message"`
Data []ScheduledMessageChange `json:"data"`
}
type ScheduledMessageStatusHandler struct {
App *app.App
ScheduledMessageEventStore store.ScheduledMessageEventStore
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageStatusHandler(apiApp *app.App, str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageStatusHandler {
return &ScheduledMessageStatusHandler{App: apiApp, ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
}
// UpdateStatus godoc
// @Summary Update status
// @Description Update the status of a scheduled message (requires JWT)
// @Tags status
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body RequestScheduledMessageStatus true "Used to update scheduled message status"
// @Success 201 {object} ScheduledMessageStatusResponse
// @Failure 400 {object} ScheduledMessageStatusResponse
// @Failure 404 {object} ScheduledMessageStatusResponse
// @Failure 500 {object} ScheduledMessageStatusResponse
// @Router /schedule/message/status/update [patch]
func (s *ScheduledMessageStatusHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req RequestScheduledMessageStatus
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
var statusCode int
var resp ScheduledMessageStatusResponse
if req.ScheduledMessageId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "Scheduled messaged Id is nil"
} else if len(req.Status) == 0 {
statusCode = http.StatusBadRequest
resp.Message = "No status provided"
} else {
ctx := r.Context()
if schMsg, err := s.ScheduledMessageStore.Get(ctx, req.ScheduledMessageId); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if schMsg.Status == scheduling.Done {
statusCode = http.StatusNotFound
resp.Message = "Scheduled message has already completed"
} else {
switch req.Status {
case scheduling.Pending:
{
if schMsg.Status == scheduling.Processing {
statusCode = http.StatusBadRequest
resp.Message = "Message is currently processing"
} else {
if returnedStatus, err := s.ScheduledMessageStore.UpdateStatus(ctx, schMsg.Id, req.Status); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
chg := initScheduledMessageChange(schMsg, schMsg.Status, *returnedStatus)
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, chg)
}
}
}
case scheduling.Ready:
{
if events, err := s.ScheduledMessageEventStore.GetWithScheduleMessageId(ctx, schMsg.Id); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
oldStatus := schMsg.Status
if schMsg.Status != scheduling.Processing && schMsg.Status != scheduling.Ready {
// Update status
if len(events) > 0 {
if returnedStatus, err := s.ScheduledMessageStore.UpdateStatus(ctx, schMsg.Id, req.Status); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
chg := initScheduledMessageChange(schMsg, oldStatus, *returnedStatus)
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, chg)
}
} else {
statusCode = http.StatusInternalServerError
resp.Message = "Not enough scheduled message events"
}
} else {
if schMsg.Status == scheduling.Processing {
statusCode = http.StatusBadRequest
resp.Message = "Scheduled messages are processing"
} else {
statusCode = http.StatusNotModified
resp.Message = "Status is already set"
}
}
}
}
case scheduling.Processing, scheduling.Done:
{
if events, err := s.ScheduledMessageEventStore.GetWithScheduleMessageId(ctx, schMsg.Id); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if len(events) > 0 {
if returnedStatus, err := s.ScheduledMessageStore.UpdateStatus(ctx, schMsg.Id, req.Status); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
chg := initScheduledMessageChange(schMsg, schMsg.Status, *returnedStatus)
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, chg)
}
} else {
statusCode = http.StatusInternalServerError
resp.Message = "Not enough scheduled message events"
}
}
}
}
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
func initScheduledMessageChange(schMsg *scheduling.ScheduledMessage, oldStatus, newStatus string) ScheduledMessageChange {
schMsg.Status = newStatus
return ScheduledMessageChange{OldStatus: oldStatus, ScheduledMessage: *schMsg}
}
@@ -0,0 +1,75 @@
package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-api/internal/store/mock"
)
func TestUpdateScheduledMessageStatusWithMock(t *testing.T) {
now := time.Now()
schMsgEventStore := mock.NewMockScheduledMessageEventStore()
contactStore := mock.NewMockContactStore()
messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore()
recipientId := uuid.New()
messageId := uuid.New()
scheduledMessageId := uuid.New()
testUserId := uuid.New()
con := testContact(recipientId, testUserId)
msg := testMessage(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now)
event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id)
ctx := t.Context()
if err := contactStore.CreateContact(ctx, &con); err != nil {
assert.NoError(t, err, "Error creating contact: %v", err)
} else if err = messageStore.CreateMessage(ctx, &msg); err != nil {
assert.NoError(t, err, "Error creating message: %v", err)
} else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil {
assert.NoError(t, err, "Error creating scheduled message: %v", err)
} else if err = schMsgEventStore.CreateScheduledMessageEvent(ctx, &event); err != nil {
assert.NoError(t, err, "Error creating scheduled message event: %v", err)
}
testReq := RequestScheduledMessageStatus{}
testReq.Status = scheduling.Ready
testReq.ScheduledMessageId = schMsg.Id
jsonValue, _ := json.Marshal(testReq)
jsonBody := strings.NewReader(string(jsonValue))
req, _ := http.NewRequest("PATCH", endpoint.UpdateScheduledMessageStatusEndpoint, jsonBody)
rr := httptest.NewRecorder()
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageStatusHandler(apiApp, schMsgEventStore, schMsgStore)
handler.UpdateStatus(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response ScheduledMessageStatusResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error creating event %v", err)
assert.NotEmpty(t, response.Data, "Data should not be empty")
changes := response.Data[0]
assert.NotEqual(t, changes.OldStatus, changes.ScheduledMessage.Status, "The status should not match")
assert.Equal(t, scheduling.Pending, changes.OldStatus, "The Old status does not match Old %s New %s", changes.OldStatus, scheduling.Pending)
assert.Equal(t, scheduling.Ready, changes.ScheduledMessage.Status, "Status has not been updated")
}
+141 -8
View File
@@ -6,15 +6,15 @@ import (
"strings" "strings"
"time" "time"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid" "github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store" "git.kundeng.us/phoenix/textsender-api/internal/store"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
) )
type RequestAddScheduledMessage struct { type RequestAddScheduledMessage struct {
Scheduled time.Time `json:"scheduled"` Scheduled time.Time `json:"scheduled"`
Created time.Time `json:"created"`
Status string `json:"status"` Status string `json:"status"`
UserId uuid.UUID `json:"user_id"` UserId uuid.UUID `json:"user_id"`
} }
@@ -24,15 +24,38 @@ type AddScheduledMessageResponse struct {
Data []scheduling.ScheduledMessage `json:"data"` Data []scheduling.ScheduledMessage `json:"data"`
} }
type GetScheduledMessageResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessage `json:"data"`
}
type FetchNextMessageResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessage `json:"data"`
}
type ScheduledMessageHandler struct { type ScheduledMessageHandler struct {
App *app.App
ScheduledMessageStore store.ScheduledMessageStore ScheduledMessageStore store.ScheduledMessageStore
} }
func NewScheduledMessageHandler(str store.ScheduledMessageStore) *ScheduledMessageHandler { func NewScheduledMessageHandler(apiApp *app.App, str store.ScheduledMessageStore) *ScheduledMessageHandler {
return &ScheduledMessageHandler{ScheduledMessageStore: str} return &ScheduledMessageHandler{App: apiApp, ScheduledMessageStore: str}
} }
func (c *ScheduledMessageHandler) AddScheduledMessage(w http.ResponseWriter, r *http.Request) { // AddScheduledMessage godoc
// @Summary Add scheduled message
// @Description Adds a scheduled message (requires JWT)
// @Tags scheduled messages
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body RequestAddScheduledMessage true "Data to add scheduled message"
// @Success 201 {object} AddScheduledMessageResponse
// @Failure 400 {object} AddScheduledMessageResponse
// @Failure 500 {object} AddScheduledMessageResponse
// @Router /schedule/message [post]
func (s *ScheduledMessageHandler) AddScheduledMessage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
@@ -55,10 +78,10 @@ func (c *ScheduledMessageHandler) AddScheduledMessage(w http.ResponseWriter, r *
} else { } else {
ctx := r.Context() ctx := r.Context()
if validStatus, err := isStatusValid(&scheduledMessage); err == nil { if validStatus, err := IsStatusValid(&scheduledMessage); err == nil {
if validStatus { if validStatus {
if valid, err := isScheduledTimeValid(&scheduledMessage); err == nil && valid { if valid, err := isScheduledTimeValid(&scheduledMessage); err == nil && valid {
if err = c.ScheduledMessageStore.CreateScheduledMessage(ctx, &scheduledMessage); err == nil { if err = s.ScheduledMessageStore.CreateScheduledMessage(ctx, &scheduledMessage); err == nil {
statusCode = http.StatusCreated statusCode = http.StatusCreated
resp.Data = append(resp.Data, scheduledMessage) resp.Data = append(resp.Data, scheduledMessage)
resp.Message = "Successful" resp.Message = "Successful"
@@ -83,6 +106,116 @@ func (c *ScheduledMessageHandler) AddScheduledMessage(w http.ResponseWriter, r *
RespondWithJSON(w, statusCode, &resp) RespondWithJSON(w, statusCode, &resp)
} }
// GetScheduledMessage godoc
// @Summary Get scheduled message
// @Description Get a scheduled message (requires JWT)
// @Tags scheduled messages
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Contact Id"
// @Param user_id path string true "User Id"
// @Success 200 {object} GetScheduledMessageResponse
// @Failure 400 {object} GetScheduledMessageResponse
// @Failure 500 {object} GetScheduledMessageResponse
// @Router /schedule/message [get]
func (s *ScheduledMessageHandler) GetScheduledMessage(w http.ResponseWriter, r *http.Request) {
var id, userId uuid.UUID
if idParam, err := ParseQueryParams(r, "id"); err == nil {
var err error
id, err = uuid.Parse(*idParam)
if err != nil {
http.Error(w, "Error parsing Id", http.StatusBadRequest)
return
}
}
if userIdParam, err := ParseQueryParams(r, "user_id"); err == nil {
userId, err = uuid.Parse(*userIdParam)
if err != nil {
http.Error(w, "Error parsing Id", http.StatusBadRequest)
return
}
}
var resp GetScheduledMessageResponse
var statusCode int
ctx := r.Context()
if userId == uuid.Nil && id == uuid.Nil {
http.Error(w, "Invalid query parameters", http.StatusBadRequest)
return
} else if userId != uuid.Nil && id != uuid.Nil {
if schMsgs, err := s.ScheduledMessageStore.GetWithUserId(ctx, userId); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
for _, schMsg := range schMsgs {
if schMsg.Id == id {
resp.Data = append(resp.Data, *schMsg)
}
}
statusCode = http.StatusOK
resp.Message = "Successful"
}
} else if id != uuid.Nil {
if schMsg, err := s.ScheduledMessageStore.Get(ctx, id); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, *schMsg)
}
} else {
if schMsgs, err := s.ScheduledMessageStore.GetWithUserId(ctx, userId); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusOK
resp.Message = "Successful"
for _, schMsg := range schMsgs {
resp.Data = append(resp.Data, *schMsg)
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
// FetchNextMessage godoc
// @Summary Fetch scheduled message
// @Description Fetches a scheduled message that is available to be processed (requires JWT)
// @Tags scheduled messages
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} FetchNextMessageResponse
// @Failure 400 {object} FetchNextMessageResponse
// @Failure 500 {object} FetchNextMessageResponse
// @Router /schedule/message/fetch [get]
func (s *ScheduledMessageHandler) FetchNextMessage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var resp FetchNextMessageResponse
var statusCode int
if schMsg, err := s.ScheduledMessageStore.FetchNextScheduledMessage(ctx); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if schMsg != nil {
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, *schMsg)
} else {
statusCode = http.StatusNotFound
resp.Message = "No item"
}
}
RespondWithJSON(w, statusCode, &resp)
}
func isScheduledTimeValid(schMsg *scheduling.ScheduledMessage) (bool, error) { func isScheduledTimeValid(schMsg *scheduling.ScheduledMessage) (bool, error) {
now := time.Now() now := time.Now()
timeCutOff := now.Add(-5 * time.Minute) timeCutOff := now.Add(-5 * time.Minute)
@@ -99,7 +232,7 @@ func isScheduledTimeValid(schMsg *scheduling.ScheduledMessage) (bool, error) {
} }
} }
func isStatusValid(schMsg *scheduling.ScheduledMessage) (bool, error) { func IsStatusValid(schMsg *scheduling.ScheduledMessage) (bool, error) {
schMsg.Status = strings.ToUpper(schMsg.Status) schMsg.Status = strings.ToUpper(schMsg.Status)
if schMsg.Status == scheduling.Pending { if schMsg.Status == scheduling.Pending {
return true, nil return true, nil
+247
View File
@@ -0,0 +1,247 @@
package handler
import (
"net/http"
"strings"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/app"
"git.kundeng.us/phoenix/textsender-api/internal/store"
)
type RequestAddScheduledMessageEvent struct {
RecipientId uuid.UUID `json:"recipient_id"`
MessageId uuid.UUID `json:"message_id"`
ScheduledMessageId uuid.UUID `json:"scheduled_message_id"`
}
type AddScheduledMessageEventResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessageEvent `json:"data"`
}
type GetScheduledMessageEventResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessageEvent `json:"data"`
}
type DeleteScheduledMessageEventResponse struct {
Message string `json:"message"`
Data []scheduling.ScheduledMessageEvent `json:"data"`
}
type ScheduledMessageEventHandler struct {
App *app.App
ScheduledMessageEventStore store.ScheduledMessageEventStore
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageEventHandler(apiApp *app.App, str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageEventHandler {
return &ScheduledMessageEventHandler{App: apiApp, ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
}
// AddContact godoc
// @Summary Add scheduled message event
// @Description Add a scheduled message event (requires JWT)
// @Tags scheduled message events
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body RequestAddScheduledMessageEvent true "Data to add scheduled message event"
// @Success 201 {object} AddScheduledMessageEventResponse
// @Failure 400 {object} AddScheduledMessageEventResponse
// @Failure 500 {object} AddScheduledMessageEventResponse
// @Router /schedule/message/event [post]
func (s *ScheduledMessageEventHandler) AddScheduledMessageEvent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req RequestAddScheduledMessageEvent
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
event := scheduling.ScheduledMessageEvent{RecipientId: req.RecipientId, MessageId: req.MessageId, ScheduledMessageId: req.ScheduledMessageId}
var statusCode int
var resp AddScheduledMessageEventResponse
if event.RecipientId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "Recipient Id is nil"
} else if event.MessageId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "Message Id is nil"
} else if event.ScheduledMessageId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "Scheduled Message Id is nil"
} else {
ctx := r.Context()
if schMsg, err := s.ScheduledMessageStore.Get(ctx, event.ScheduledMessageId); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if schMsg.Status == scheduling.Pending {
if exists, err := s.ScheduledMessageEventStore.Exists(ctx, &event); err == nil {
if exists {
statusCode = http.StatusBadRequest
resp.Message = "Event already present"
} else {
if err = s.ScheduledMessageEventStore.CreateScheduledMessageEvent(ctx, &event); err == nil {
statusCode = http.StatusCreated
resp.Message = "Successful"
resp.Data = append(resp.Data, event)
} else {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
}
}
} else {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
}
} else {
statusCode = http.StatusBadRequest
resp.Message = "Scheduled message cannot be modified due to the status"
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
// GetScheduledMessageEvent godoc
// @Summary Get scheduled message event
// @Description Gets a scheduled message event (requires JWT)
// @Tags scheduled message events
// @Accept json
// @Produce json
// @Param id path string true "Contact Id"
// @Param scheduled_message_id path string true "Scheduled Message Id"
// @Security BearerAuth
// @Success 200 {object} GetScheduledMessageEventResponse
// @Failure 400 {object} GetScheduledMessageEventResponse
// @Failure 500 {object} GetScheduledMessageEventResponse
// @Router /schedule/message/event [get]
func (s *ScheduledMessageEventHandler) GetScheduledMessageEvent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Metnot allowed", http.StatusMethodNotAllowed)
return
}
var id, scheduledMessageId uuid.UUID
if idParam, err := ParseQueryParams(r, "id"); err == nil {
id, err = uuid.Parse(*idParam)
}
if scheduledMessageIdParam, err := ParseQueryParams(r, "scheduled_message_id"); err == nil {
scheduledMessageId, err = uuid.Parse(*scheduledMessageIdParam)
}
var statusCode int
var resp GetScheduledMessageEventResponse
ctx := r.Context()
if id == uuid.Nil && scheduledMessageId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "Query parameters missing"
} else if id != uuid.Nil {
if event, err := s.ScheduledMessageEventStore.Get(ctx, id); err != nil {
resp.Message = err.Error()
statusCode = http.StatusInternalServerError
} else {
if event != nil {
resp.Message = "Successful"
statusCode = http.StatusOK
resp.Data = append(resp.Data, *event)
} else {
statusCode = http.StatusNotFound
resp.Message = "Scheduled message event not found"
}
}
} else {
if events, err := s.ScheduledMessageEventStore.GetWithScheduleMessageId(ctx, scheduledMessageId); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusOK
resp.Message = "Successful"
for _, event := range events {
resp.Data = append(resp.Data, *event)
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
// DeleteScheduledMessageEvent godoc
// @Summary Delete scheduled message event
// @Description Deletes a scheduled message event (requires JWT)
// @Tags scheduled message events
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "scheduled message event Id" Format(uuid)
// @Success 201 {object} DeleteScheduledMessageEventResponse
// @Failure 400 {object} DeleteScheduledMessageEventResponse
// @Failure 500 {object} DeleteScheduledMessageEventResponse
// @Router /schedule/message/event/{id} [delete]
func (s *ScheduledMessageEventHandler) DeleteScheduledMessageEvent(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "Metnot allowed", http.StatusMethodNotAllowed)
return
}
id := chi.URLParam(r, "id")
if len(id) == 0 {
pathParts := strings.Split(r.URL.Path, "/")
if len(pathParts) < 7 {
http.Error(w, "Id not provided", http.StatusBadRequest)
return
} else {
id = pathParts[6]
}
}
var resp DeleteScheduledMessageEventResponse
var statusCode int
if parsedId, err := uuid.Parse(id); err != nil {
resp.Message = err.Error()
statusCode = http.StatusBadRequest
} else {
ctx := r.Context()
if event, err := s.ScheduledMessageEventStore.Get(ctx, parsedId); err != nil {
resp.Message = err.Error()
statusCode = http.StatusInternalServerError
} else {
if schMsg, err := s.ScheduledMessageStore.Get(ctx, event.ScheduledMessageId); err != nil {
resp.Message = err.Error()
statusCode = http.StatusInternalServerError
} else {
if schMsg.Status == scheduling.Pending {
if err = s.ScheduledMessageEventStore.Delete(ctx, event.Id); err != nil {
resp.Message = err.Error()
statusCode = http.StatusInternalServerError
} else {
resp.Message = "Successful"
resp.Data = append(resp.Data, *event)
statusCode = http.StatusOK
}
} else {
statusCode = http.StatusBadRequest
resp.Message = "Invalid status"
}
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
@@ -0,0 +1,212 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-api/internal/store/mock"
)
var (
recipientId = uuid.New()
messageId = uuid.New()
scheduledMessageId = uuid.New()
testUserId = uuid.New()
)
type CreateScheduledMessageEventRequest struct {
RecipientId uuid.UUID `json:"recipient_id"`
MessageId uuid.UUID `json:"message_id"`
ScheduledMessageId uuid.UUID `json:"scheduled_message_id"`
}
func TestCreateScheduledMessageEventWithMock(t *testing.T) {
now := time.Now()
mockStore := mock.NewMockScheduledMessageEventStore()
contactStore := mock.NewMockContactStore()
messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore()
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageEventHandler(apiApp, mockStore, schMsgStore)
recipientId := uuid.New()
messageId := uuid.New()
scheduledMessageId := uuid.New()
testUserId := uuid.New()
con := testContact(recipientId, testUserId)
msg := testMessage(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now)
ctx := t.Context()
if err := contactStore.CreateContact(ctx, &con); err != nil {
assert.NoError(t, err, "Error creating contact: %v", err)
} else if err = messageStore.CreateMessage(ctx, &msg); err != nil {
assert.NoError(t, err, "Error creating message: %v", err)
} else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil {
assert.NoError(t, err, "Error creating scheduled message: %v", err)
}
testReq := CreateScheduledMessageEventRequest{RecipientId: recipientId, MessageId: messageId, ScheduledMessageId: scheduledMessageId}
jsonValue, _ := json.Marshal(testReq)
req, _ := http.NewRequest("POST", endpoint.AddEventToScheduledMessageEndpoint, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.AddScheduledMessageEvent(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response AddScheduledMessageEventResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error creating event %v", err)
assert.NotEmpty(t, response.Data, "No event created")
var event scheduling.ScheduledMessageEvent
event = response.Data[0]
assert.NotEmpty(t, event.Created, "Created date should not be empty")
}
func TestGetScheduledMessageEventWithMock(t *testing.T) {
now := time.Now()
contactStore := mock.NewMockContactStore()
messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore()
schMsgEventStore := mock.NewMockScheduledMessageEventStore()
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schMsgStore)
recipientId := uuid.New()
messageId := uuid.New()
scheduledMessageId := uuid.New()
testUserId := uuid.New()
con := testContact(recipientId, testUserId)
msg := testMessage(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now)
event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id)
ctx := t.Context()
if err := contactStore.CreateContact(ctx, &con); err != nil {
assert.NoError(t, err, "Error creating contact: %v", err)
} else if err = messageStore.CreateMessage(ctx, &msg); err != nil {
assert.NoError(t, err, "Error creating message: %v", err)
} else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil {
assert.NoError(t, err, "Error creating scheduled message: %v", err)
} else if err = schMsgEventStore.CreateScheduledMessageEvent(ctx, &event); err != nil {
assert.NoError(t, err, "Error creating scheduled message event: %v", err)
}
url := fmt.Sprintf("%s?id=%s", endpoint.GetScheduledMessageEventEndpoint, event.Id.String())
req, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder()
handler.GetScheduledMessageEvent(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response GetScheduledMessageEventResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error creating event %v", err)
assert.NotEmpty(t, response.Data, "No event created")
msgEvent := response.Data[0]
assert.NotEmpty(t, msgEvent.Created, "Created date should not be empty")
}
func TestDeleteScheduledMessageEventWithMock(t *testing.T) {
now := time.Now()
contactStore := mock.NewMockContactStore()
messageStore := mock.NewMockMessageStore()
schMsgStore := mock.NewMockScheduledMessageStore()
schMsgEventStore := mock.NewMockScheduledMessageEventStore()
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schMsgStore)
con := testContact(recipientId, testUserId)
msg := testMessage(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now)
event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id)
ctx := t.Context()
if err := contactStore.CreateContact(ctx, &con); err != nil {
assert.NoError(t, err, "Error creating contact: %v", err)
} else if err = messageStore.CreateMessage(ctx, &msg); err != nil {
assert.NoError(t, err, "Error creating message: %v", err)
} else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil {
assert.NoError(t, err, "Error creating scheduled message: %v", err)
} else if err = schMsgEventStore.CreateScheduledMessageEvent(ctx, &event); err != nil {
assert.NoError(t, err, "Error creating scheduled message event: %v", err)
}
endpointValue := strings.Replace(endpoint.DeleteScheduledMessageEventEndpoint, "{id}", event.Id.String(), 1)
req, _ := http.NewRequest("DELETE", endpointValue, nil)
rr := httptest.NewRecorder()
handler.DeleteScheduledMessageEvent(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response DeleteScheduledMessageEventResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error creating event %v", err)
assert.NotEmpty(t, response.Data, "No event created")
msgEvent := response.Data[0]
assert.NotEmpty(t, msgEvent.Created, "Created date should not be empty")
}
func testContact(id uuid.UUID, userId uuid.UUID) contact.Contact {
if id == uuid.Nil {
return contact.Contact{Id: uuid.New(), PhoneNumber: "+10123456789", UserId: userId}
} else {
return contact.Contact{Id: id, PhoneNumber: "+10123456789", UserId: userId}
}
}
func testMessage(id uuid.UUID, userId uuid.UUID) message.Message {
if id == uuid.Nil {
return message.Message{Id: uuid.New(), Content: "Oh how the mighty have fallen", UserId: userId}
} else {
return message.Message{Id: id, Content: "Oh how the mighty have fallen", UserId: userId}
}
}
func testScheduledMessage(id uuid.UUID, userId uuid.UUID, now time.Time) scheduling.ScheduledMessage {
if id == uuid.Nil {
return scheduling.ScheduledMessage{Id: uuid.New(), UserId: userId, Scheduled: now.Add(20 * time.Minute), Status: scheduling.Pending}
} else {
return scheduling.ScheduledMessage{Id: id, UserId: userId, Scheduled: now.Add(20 * time.Minute), Status: scheduling.Pending}
}
}
func testScheduledMessageEvent(messageId, recipientId, scheduledMessageId uuid.UUID) scheduling.ScheduledMessageEvent {
return scheduling.ScheduledMessageEvent{MessageId: messageId, RecipientId: recipientId, ScheduledMessageId: scheduledMessageId}
}
+99 -6
View File
@@ -2,17 +2,19 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
"time" "time"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling" "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-api/internal/store/mock"
) )
type CreateScheduledMessageRequest struct { type CreateScheduledMessageRequest struct {
@@ -23,12 +25,13 @@ type CreateScheduledMessageRequest struct {
func TestCreateScheduledMessageWithMock(t *testing.T) { func TestCreateScheduledMessageWithMock(t *testing.T) {
now := time.Now() now := time.Now()
mockStore := NewMockScheduledMessageStore() mockStore := mock.NewMockScheduledMessageStore()
handler := NewScheduledMessageHandler(mockStore) apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageHandler(apiApp, mockStore)
testUserId := uuid.New() testUserId := uuid.New()
scheduled := now.Add(5 * time.Minute) testBody := testCreateScheduledMessageRequest(testUserId, now)
testBody := CreateScheduledMessageRequest{Status: scheduling.Pending, UserId: testUserId, Scheduled: scheduled}
jsonValue, _ := json.Marshal(testBody) jsonValue, _ := json.Marshal(testBody)
req, _ := http.NewRequest("POST", endpoint.ScheduleMessageEndpoint, strings.NewReader(string(jsonValue))) req, _ := http.NewRequest("POST", endpoint.ScheduleMessageEndpoint, strings.NewReader(string(jsonValue)))
@@ -39,10 +42,100 @@ func TestCreateScheduledMessageWithMock(t *testing.T) {
assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, http.StatusCreated, rr.Code)
var response AddScheduledMessageResponse var response AddScheduledMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response) err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error Creating message %v", err) assert.NoError(t, err, "Error Creating message %v", err)
assert.NotEmpty(t, response.Data, "No Message created") assert.NotEmpty(t, response.Data, "No Message created")
assert.NotNil(t, response.Data[0].Id, "Id should not be nil") assert.NotNil(t, response.Data[0].Id, "Id should not be nil")
} }
func TestGetScheduledMessageWithMock(t *testing.T) {
now := time.Now()
mockStore := mock.NewMockScheduledMessageStore()
testUserId := uuid.New()
testBody := testCreateScheduledMessageRequest(testUserId, now)
ctx := t.Context()
schMsg := scheduling.ScheduledMessage{}
schMsg.Scheduled = testBody.Scheduled
schMsg.Status = testBody.Status
schMsg.UserId = testBody.UserId
if err := mockStore.CreateScheduledMessage(ctx, &schMsg); err != nil {
assert.NoError(t, err, "Error Creating message %v", err)
}
url := fmt.Sprintf("%s?id=%s&user_id=%s", endpoint.GetScheduledMessageEndpoint, schMsg.Id, schMsg.UserId)
req, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder()
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageHandler(apiApp, mockStore)
handler.GetScheduledMessage(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response GetScheduledMessageResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error parsing response %v", err)
}
func TestFetchScheduledMessageWithMock(t *testing.T) {
now := time.Now()
mockStore := mock.NewMockScheduledMessageStore()
contactStore := mock.NewMockContactStore()
messageStore := mock.NewMockMessageStore()
schMsgEventStore := mock.NewMockScheduledMessageEventStore()
recipientId := uuid.New()
messageId := uuid.New()
scheduledMessageId := uuid.New()
testUserId := uuid.New()
con := testContact(recipientId, testUserId)
msg := testMessage(messageId, testUserId)
schMsg := testScheduledMessage(scheduledMessageId, testUserId, now)
event := testScheduledMessageEvent(msg.Id, con.Id, schMsg.Id)
ctx := t.Context()
if err := contactStore.CreateContact(ctx, &con); err != nil {
assert.NoError(t, err, "Error creating contact: %v", err)
} else if err = messageStore.CreateMessage(ctx, &msg); err != nil {
assert.NoError(t, err, "Error creating message: %v", err)
} else if err = mockStore.CreateScheduledMessage(ctx, &schMsg); err != nil {
assert.NoError(t, err, "Error creating scheduled message: %v", err)
} else if err = schMsgEventStore.CreateScheduledMessageEvent(ctx, &event); err != nil {
assert.NoError(t, err, "Error creating scheduled message event: %v", err)
} else if newStatus, err := mockStore.UpdateStatus(ctx, schMsg.Id, scheduling.Ready); err != nil {
assert.NoError(t, err, "Error updating status: %v", err)
} else {
assert.Equal(t, *newStatus, scheduling.Ready)
}
req, _ := http.NewRequest("GET", endpoint.FetchNextScheduledMessageEndpoint, nil)
rr := httptest.NewRecorder()
apiApp, err := GetApp()
assert.NoError(t, err, "Error getting app")
handler := NewScheduledMessageHandler(apiApp, mockStore)
handler.FetchNextMessage(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response FetchNextMessageResponse
err = json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error fetching scheduled message: %v", err)
assert.NotEmpty(t, response.Data, "No data")
fetchedSchMsg := response.Data[0]
assert.Equal(t, scheduling.Processing, fetchedSchMsg.Status, "The statuses do not match")
}
func testCreateScheduledMessageRequest(userId uuid.UUID, now time.Time) CreateScheduledMessageRequest {
scheduled := now.Add(5 * time.Minute)
return CreateScheduledMessageRequest{Status: scheduling.Pending, UserId: userId, Scheduled: scheduled}
}
+2 -3
View File
@@ -3,10 +3,9 @@ package services
import ( import (
"time" "time"
txtmodels_token "git.kundeng.us/phoenix/textsender-models/tx0/token"
txtmodels_user "git.kundeng.us/phoenix/textsender-models/tx0/user"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
txtmodels_token "git.kundeng.us/phoenix/textsender-models/pkg/token"
txtmodels_user "git.kundeng.us/phoenix/textsender-models/pkg/user"
) )
type JWTService struct { type JWTService struct {
+1 -2
View File
@@ -4,11 +4,10 @@ import (
"context" "context"
"fmt" "fmt"
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
) )
type ContactStore interface { type ContactStore interface {
@@ -0,0 +1,36 @@
package store
import (
"context"
"fmt"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
type MessageEventResponseStore interface {
Create(ctx context.Context, mer *message.MessageEventResponse) error
}
type PGMessageEventResponseStore struct {
db *pgxpool.Pool
}
func NewMessageEventResponseStore(db *pgxpool.Pool) *PGMessageEventResponseStore {
return &PGMessageEventResponseStore{db: db}
}
func (m *PGMessageEventResponseStore) Create(ctx context.Context, mer *message.MessageEventResponse) error {
var query string
queryBase := "INSERT INTO message_event_responses"
queryReturn := "RETURNING id"
if mer.ScheduledMessageEventId == uuid.Nil {
query = fmt.Sprintf("%s (response, user_id, sent) VALUES ($1, $2, $3) %s", queryBase, queryReturn)
return m.db.QueryRow(ctx, query, mer.Response, mer.UserId, mer.Sent).Scan(&mer.Id)
} else {
query = fmt.Sprintf("%s (scheduled_message_event_id, response, user_id, sent) %s", queryBase, queryReturn)
return m.db.QueryRow(ctx, query, mer.ScheduledMessageEventId, mer.Response, mer.UserId, mer.Sent).Scan(&mer.Id)
}
}
+1 -2
View File
@@ -4,11 +4,10 @@ import (
"context" "context"
"fmt" "fmt"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"git.kundeng.us/phoenix/textsender-models/pkg/message"
) )
type MessageStore interface { type MessageStore interface {
@@ -0,0 +1,62 @@
package mock
import (
"context"
"fmt"
"sync"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid"
)
type MessageEventResponseKey struct {
ScheduledMessageEventId uuid.UUID
UserId uuid.UUID
}
type MockMessageEventResponseStore struct {
MessageEventResponses map[uuid.UUID]*message.MessageEventResponse
MessageEventResponsesByKey map[MessageEventResponseKey]*message.MessageEventResponse
mu sync.RWMutex
Error error
}
func NewMockMessageEventResponseStore() *MockMessageEventResponseStore {
return &MockMessageEventResponseStore{
MessageEventResponses: make(map[uuid.UUID]*message.MessageEventResponse),
MessageEventResponsesByKey: make(map[MessageEventResponseKey]*message.MessageEventResponse),
}
}
func (m *MockMessageEventResponseStore) Create(ctx context.Context, mer *message.MessageEventResponse) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if mer.Id == uuid.Nil {
mer.Id = uuid.New()
}
if len(mer.Response) == 0 {
return fmt.Errorf("Response is empty")
} else if mer.UserId == uuid.Nil {
return fmt.Errorf("User Id is nil")
} else {
key := MessageEventResponseKey{ScheduledMessageEventId: mer.ScheduledMessageEventId, UserId: mer.UserId}
if mer.ScheduledMessageEventId == uuid.Nil {
key.ScheduledMessageEventId = uuid.Nil
} else {
key.ScheduledMessageEventId = mer.ScheduledMessageEventId
}
sent := time.Now()
mer.Sent = sent
m.MessageEventResponses[mer.Id] = mer
m.MessageEventResponsesByKey[key] = mer
return nil
}
}
@@ -0,0 +1,159 @@
package mock
import (
"context"
"fmt"
"sync"
"time"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
)
type ScheduledMessageEventKey struct {
RecipientId uuid.UUID
MessageId uuid.UUID
ScheduledMessageId uuid.UUID
}
type MockScheduledMessageEventStore struct {
ScheduledMessageEvents map[uuid.UUID]*scheduling.ScheduledMessageEvent
ScheduledMessageEventsByKey map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent
mu sync.RWMutex
Error error // Optional: simulate errors
}
func NewMockScheduledMessageEventStore() *MockScheduledMessageEventStore {
return &MockScheduledMessageEventStore{
ScheduledMessageEvents: make(map[uuid.UUID]*scheduling.ScheduledMessageEvent),
ScheduledMessageEventsByKey: make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent),
}
}
func (m *MockScheduledMessageEventStore) Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessageEvent, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
if id == uuid.Nil {
return nil, fmt.Errorf("Id is nil")
}
if _, exists := m.ScheduledMessageEvents[id]; exists {
return m.ScheduledMessageEvents[id], nil
} else {
return nil, fmt.Errorf("Not found")
}
}
func (m *MockScheduledMessageEventStore) GetWithScheduleMessageId(ctx context.Context, schMsgId uuid.UUID) ([]*scheduling.ScheduledMessageEvent, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
if schMsgId == uuid.Nil {
return nil, fmt.Errorf("Scheduled message Id is nil")
}
var events []*scheduling.ScheduledMessageEvent
for _, event := range m.ScheduledMessageEvents {
if event.ScheduledMessageId == schMsgId {
events = append(events, event)
}
}
if len(events) > 0 {
return events, nil
} else {
return nil, nil
}
}
func (m *MockScheduledMessageEventStore) Delete(ctx context.Context, id uuid.UUID) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if id == uuid.Nil {
return fmt.Errorf("Id is nil")
}
if _, exists := m.ScheduledMessageEvents[id]; exists {
originalAmount := len(m.ScheduledMessageEvents)
copiedEvents := make(map[uuid.UUID]*scheduling.ScheduledMessageEvent)
copiedEventsKey := make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent)
for i, schMsgEvent := range m.ScheduledMessageEvents {
key := ScheduledMessageEventKey{RecipientId: schMsgEvent.RecipientId, MessageId: schMsgEvent.MessageId, ScheduledMessageId: schMsgEvent.ScheduledMessageId}
if schMsgEvent.Id != id {
copiedEvents[i] = schMsgEvent
copiedEventsKey[key] = schMsgEvent
}
}
if originalAmount > 1 {
if len(copiedEvents) > 0 && len(copiedEventsKey) > 0 {
m.ScheduledMessageEvents = copiedEvents
m.ScheduledMessageEventsByKey = copiedEventsKey
return nil
} else {
return fmt.Errorf("Not removed")
}
} else {
m.ScheduledMessageEvents = make(map[uuid.UUID]*scheduling.ScheduledMessageEvent)
m.ScheduledMessageEventsByKey = make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent)
return nil
}
} else {
return fmt.Errorf("Not found")
}
}
func (m *MockScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if event.Id == uuid.Nil {
event.Id = uuid.New()
}
key := ScheduledMessageEventKey{RecipientId: event.RecipientId, MessageId: event.MessageId, ScheduledMessageId: event.ScheduledMessageId}
if _, exists := m.ScheduledMessageEventsByKey[key]; exists {
return fmt.Errorf("Already exists")
} else {
event.Created = time.Now()
m.ScheduledMessageEvents[event.Id] = event
m.ScheduledMessageEventsByKey[key] = event
return nil
}
}
func (m *MockScheduledMessageEventStore) Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return false, m.Error
}
key := ScheduledMessageEventKey{RecipientId: event.RecipientId, MessageId: event.MessageId, ScheduledMessageId: event.ScheduledMessageId}
_, exists := m.ScheduledMessageEventsByKey[key]
return exists, nil
}
@@ -0,0 +1,141 @@
package mock
import (
"context"
"fmt"
"sync"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
)
type ScheduledMessageKey struct {
UserId uuid.UUID
}
type MockScheduledMessageStore struct {
ScheduledMessages map[uuid.UUID]*scheduling.ScheduledMessage
ScheduledMessagesByKey map[ScheduledMessageKey]*scheduling.ScheduledMessage
mu sync.RWMutex
Error error // Optional: simulate errors
}
func NewMockScheduledMessageStore() *MockScheduledMessageStore {
return &MockScheduledMessageStore{
ScheduledMessages: make(map[uuid.UUID]*scheduling.ScheduledMessage),
ScheduledMessagesByKey: make(map[ScheduledMessageKey]*scheduling.ScheduledMessage),
}
}
func (m *MockScheduledMessageStore) Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessage, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
if id == uuid.Nil {
return nil, fmt.Errorf("Id is nil")
} else {
if schMsg := m.ScheduledMessages[id]; schMsg != nil {
return schMsg, nil
} else {
return nil, fmt.Errorf("Scheduled message does not exist")
}
}
}
func (m *MockScheduledMessageStore) FetchNextScheduledMessage(ctx context.Context) (*scheduling.ScheduledMessage, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
var schMsg *scheduling.ScheduledMessage
for _, msg := range m.ScheduledMessages {
if msg.Status == scheduling.Ready {
msg.Status = scheduling.Processing
schMsg = msg
break
}
}
if schMsg == nil {
return nil, fmt.Errorf("No scheduled message is ready")
} else {
return schMsg, nil
}
}
func (m *MockScheduledMessageStore) GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*scheduling.ScheduledMessage, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
if userId == uuid.Nil {
return nil, fmt.Errorf("User Id is nil")
} else {
var schMsgs []*scheduling.ScheduledMessage
for _, schMsg := range m.ScheduledMessages {
if schMsg.UserId == userId {
schMsgs = append(schMsgs, schMsg)
}
}
if len(schMsgs) == 0 {
return nil, nil
} else {
return schMsgs, nil
}
}
}
func (m *MockScheduledMessageStore) CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if schedMsg.Id == uuid.Nil {
schedMsg.Id = uuid.New()
}
key := ScheduledMessageKey{UserId: schedMsg.UserId}
m.ScheduledMessages[schedMsg.Id] = schedMsg
m.ScheduledMessagesByKey[key] = schedMsg
return nil
}
func (m *MockScheduledMessageStore) UpdateStatus(ctx context.Context, id uuid.UUID, updatedStatus string) (*string, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
if id == uuid.Nil {
return nil, fmt.Errorf("Id is nil")
} else {
if schMsg := m.ScheduledMessages[id]; schMsg != nil {
key := ScheduledMessageKey{UserId: schMsg.UserId}
schMsg.Status = updatedStatus
m.ScheduledMessages[schMsg.Id] = schMsg
m.ScheduledMessagesByKey[key] = schMsg
return &updatedStatus, nil
} else {
return nil, fmt.Errorf("Scheduled message does not exist")
}
}
}
@@ -1,15 +1,13 @@
package handler package mock
import ( import (
"context" "context"
"errors" "errors"
"sync" "sync"
"git.kundeng.us/phoenix/textsender-models/tx0/contact"
"git.kundeng.us/phoenix/textsender-models/tx0/message"
"github.com/google/uuid" "github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
"git.kundeng.us/phoenix/textsender-models/pkg/message"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
) )
type Key struct { type Key struct {
@@ -201,40 +199,3 @@ func (m *MockMessageStore) MessageExists(ctx context.Context, msg *message.Messa
return exists, nil return exists, nil
} }
type ScheduledMessageKey struct {
UserId uuid.UUID
}
type MockScheduledMessageStore struct {
ScheduledMessages map[uuid.UUID]*scheduling.ScheduledMessage
ScheduledMessagesByKey map[ScheduledMessageKey]*scheduling.ScheduledMessage
mu sync.RWMutex
Error error // Optional: simulate errors
}
func NewMockScheduledMessageStore() *MockScheduledMessageStore {
return &MockScheduledMessageStore{
ScheduledMessages: make(map[uuid.UUID]*scheduling.ScheduledMessage),
ScheduledMessagesByKey: make(map[ScheduledMessageKey]*scheduling.ScheduledMessage),
}
}
func (m *MockScheduledMessageStore) CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if schedMsg.Id == uuid.Nil {
schedMsg.Id = uuid.New()
}
key := ScheduledMessageKey{UserId: schedMsg.UserId}
m.ScheduledMessages[schedMsg.Id] = schedMsg
m.ScheduledMessagesByKey[key] = schedMsg
return nil
}
@@ -0,0 +1,113 @@
package store
import (
"context"
"fmt"
"git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type ScheduledMessageEventStore interface {
Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessageEvent, error)
GetWithScheduleMessageId(ctx context.Context, schMsgId uuid.UUID) ([]*scheduling.ScheduledMessageEvent, error)
Delete(ctx context.Context, id uuid.UUID) error
CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error
Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error)
}
type PGScheduledMessageEventStore struct {
db *pgxpool.Pool
}
func NewScheduledMessageEventStore(db *pgxpool.Pool) *PGScheduledMessageEventStore {
return &PGScheduledMessageEventStore{db: db}
}
func (s *PGScheduledMessageEventStore) Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessageEvent, error) {
query := `SELECT id, recipient_id, message_id, scheduled_message_id, created FROM scheduled_message_events WHERE id = $1`
var event scheduling.ScheduledMessageEvent
err := s.db.QueryRow(ctx, query, id).Scan(
&event.Id, &event.RecipientId, &event.MessageId, &event.ScheduledMessageId, &event.Created,
)
if err == pgx.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("getting scheduled message event by ID: %w", err)
} else {
return &event, nil
}
}
func (s *PGScheduledMessageEventStore) GetWithScheduleMessageId(ctx context.Context, schMsgId uuid.UUID) ([]*scheduling.ScheduledMessageEvent, error) {
var events []*scheduling.ScheduledMessageEvent
query := `SELECT id, recipient_id, message_id, scheduled_message_id, created FROM scheduled_message_events WHERE scheduled_message_id = $1
`
rows, err := s.db.Query(ctx, query, schMsgId)
if err != nil {
return nil, fmt.Errorf("Error retrieving rows from scheduled_message_Events: %w", err)
}
for rows.Next() {
var event scheduling.ScheduledMessageEvent
if err := rows.Scan(&event.Id, &event.RecipientId, &event.MessageId, &event.ScheduledMessageId, &event.Created); err != nil {
return nil, fmt.Errorf("Error fetching data from row: %w", err)
} else {
events = append(events, &event)
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return events, nil
}
func (s *PGScheduledMessageEventStore) Delete(ctx context.Context, id uuid.UUID) error {
query := `
DELETE FROM scheduled_message_events WHERE id = $1
`
commandTag, err := s.db.Exec(ctx, query, id)
if err != nil {
return fmt.Errorf("error deleting event: %w", err)
}
if commandTag.RowsAffected() == 0 {
return fmt.Errorf("no event found with id %d", id)
} else {
return nil
}
}
func (s *PGScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error {
query := `
INSERT INTO scheduled_message_events (recipient_id, message_id, scheduled_message_id)
VALUES ($1, $2, $3)
RETURNING id, created
`
return s.db.QueryRow(ctx, query, event.RecipientId, event.MessageId, event.ScheduledMessageId).Scan(
&event.Id, &event.Created,
)
}
func (s *PGScheduledMessageEventStore) Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) {
query := `SELECT EXISTS(SELECT 1 FROM scheduled_message_events WHERE scheduled_message_id = $1 AND recipient_id = $2)`
var exists bool
err := s.db.QueryRow(ctx, query, event.ScheduledMessageId, event.RecipientId).Scan(&exists)
if err != nil {
return false, fmt.Errorf("Scheduled message event does not exist: %w", err)
} else {
return exists, nil
}
}
+96 -5
View File
@@ -2,17 +2,20 @@ package store
import ( import (
"context" "context"
// "fmt" "fmt"
// "github.com/google/uuid" "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling"
// "github.com/jackc/pgx/v5" "github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
) )
type ScheduledMessageStore interface { type ScheduledMessageStore interface {
Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessage, error)
FetchNextScheduledMessage(ctx context.Context) (*scheduling.ScheduledMessage, error)
GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*scheduling.ScheduledMessage, error)
CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error
UpdateStatus(ctx context.Context, id uuid.UUID, updatedStatus string) (*string, error)
} }
type PGScheduledMessageStore struct { type PGScheduledMessageStore struct {
@@ -23,6 +26,79 @@ func NewScheduledMessageStore(db *pgxpool.Pool) *PGScheduledMessageStore {
return &PGScheduledMessageStore{db: db} return &PGScheduledMessageStore{db: db}
} }
func (s *PGScheduledMessageStore) Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessage, error) {
query := `
SELECT id, scheduled, created, status, user_id FROM scheduled_messages WHERE id = $1
`
var schMsg scheduling.ScheduledMessage
err := s.db.QueryRow(ctx, query, id).Scan(
&schMsg.Id, &schMsg.Scheduled, &schMsg.Created, &schMsg.Status, &schMsg.UserId,
)
if err == pgx.ErrNoRows {
return nil, fmt.Errorf("No rows")
} else if err != nil {
return nil, fmt.Errorf("Getting scheduled message: %w", err)
}
return &schMsg, nil
}
func (s *PGScheduledMessageStore) FetchNextScheduledMessage(ctx context.Context) (*scheduling.ScheduledMessage, error) {
query := `
UPDATE scheduled_messages
SET status = $1
WHERE id = (
SELECT id FROM scheduled_messages
WHERE status = $2
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
RETURNING id, scheduled, created, status, user_id
`
var schMsg scheduling.ScheduledMessage
err := s.db.QueryRow(ctx, query, scheduling.Processing, scheduling.Ready).Scan(
&schMsg.Id, &schMsg.Scheduled, &schMsg.Created, &schMsg.Status, &schMsg.UserId,
)
if err == pgx.ErrNoRows {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("Getting scheduled message: %w", err)
} else {
return &schMsg, nil
}
}
func (s *PGScheduledMessageStore) GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*scheduling.ScheduledMessage, error) {
query := `
SELECT id, scheduled, created, status, user_id FROM scheduled_messages WHERE user_id = $1
`
rows, err := s.db.Query(ctx, query, userId)
if err != nil {
return nil, fmt.Errorf("Error retrieving rows from scheduled_messages: %w", err)
}
var schMsgs []*scheduling.ScheduledMessage
for rows.Next() {
var schMsg scheduling.ScheduledMessage
if err := rows.Scan(&schMsg.Id, &schMsg.Scheduled, &schMsg.Created, &schMsg.Status, &schMsg.UserId); err != nil {
return nil, fmt.Errorf("Error fetching data from row: %w", err)
} else {
schMsgs = append(schMsgs, &schMsg)
}
}
if err := rows.Err(); err != nil {
return nil, err
} else {
return schMsgs, nil
}
}
func (s *PGScheduledMessageStore) CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error { func (s *PGScheduledMessageStore) CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error {
query := ` query := `
INSERT INTO scheduled_messages (scheduled, status, user_id) INSERT INTO scheduled_messages (scheduled, status, user_id)
@@ -34,3 +110,18 @@ func (s *PGScheduledMessageStore) CreateScheduledMessage(ctx context.Context, sc
&schedMsg.Id, &schedMsg.Created, &schedMsg.Id, &schedMsg.Created,
) )
} }
func (s *PGScheduledMessageStore) UpdateStatus(ctx context.Context, id uuid.UUID, updatedStatus string) (*string, error) {
query := `
UPDATE scheduled_messages set status = $1 WHERE id = $2
RETURNING status
`
var returnedStatus string
if err := s.db.QueryRow(ctx, query, updatedStatus, id).Scan(&returnedStatus); err != nil {
return nil, err
} else {
return &returnedStatus, nil
}
}
+23 -3
View File
@@ -1,23 +1,43 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
DROP TABLE IF EXISTS contacts CASCADE; DROP TABLE IF EXISTS contacts CASCADE;
DROP TABLE IF EXISTS messages CASCADE;
DROP TABLE IF EXISTS scheduled_messages CASCADE;
DROP TABLE IF EXISTS scheduled_message_events CASCADE;
DROP TABLE IF EXISTS message_event_responses CASCADE;
CREATE TABLE contacts ( CREATE TABLE IF NOT EXISTS contacts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phone_number TEXT NOT NULL, phone_number TEXT NOT NULL,
user_id UUID NOT NULL user_id UUID NOT NULL
); );
CREATE TABLE messages ( CREATE TABLE IF NOT EXISTS messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content TEXT NOT NULL, content TEXT NOT NULL,
user_id UUID NOT NULL user_id UUID NOT NULL
); );
CREATE TABLE scheduled_messages ( CREATE TABLE IF NOT EXISTS scheduled_messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
scheduled timestamptz NOT NULL, scheduled timestamptz NOT NULL,
created timestamptz DEFAULT now(), created timestamptz DEFAULT now(),
status TEXT CHECK (status IN ('PENDING', 'READY', 'PROCESSING', 'DONE')), status TEXT CHECK (status IN ('PENDING', 'READY', 'PROCESSING', 'DONE')),
user_id UUID NOT NULL user_id UUID NOT NULL
); );
CREATE TABLE IF NOT EXISTS scheduled_message_events (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
recipient_id UUID NOT NULL,
message_id UUID NOT NULL,
scheduled_message_id UUID NOT NULL,
created timestamptz DEFAULT now()
);
CREATE TABLE IF NOT EXISTS message_event_responses (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
scheduled_message_event_id UUID NULL,
response JSONB NOT NULL,
user_id UUID NOT NULL,
sent timestamptz NOT NULL
);