83 Commits
Author SHA1 Message Date
phoenix 270c4e5675 Adding license (#34)
schedtxt_api Build / Rustfmt (push) Successful in 1m15s
schedtxt_api Build / Clippy (push) Successful in 1m15s
schedtxt_api Build / Check (push) Successful in 3m14s
schedtxt_api Build / Test Suite (push) Successful in 4m24s
schedtxt_api Build / build (push) Successful in 2m37s
Reviewed-on: #34
2026-07-14 15:33:21 -04:00
phoenix 89b347b3f6 Dependency name change (#33)
schedtxt_api Build / Rustfmt (push) Successful in 40s
schedtxt_api Build / Check (push) Successful in 1m21s
schedtxt_api Build / Test Suite (push) Successful in 2m50s
schedtxt_api Build / Clippy (push) Successful in 2m51s
schedtxt_api Build / build (push) Successful in 3m49s
Reviewed-on: #33
2026-07-13 18:06:17 -04:00
phoenix 57709ab87d Update rust (#32)
textsender_api Build / Check (push) Successful in 2m22s
textsender_api Build / Rustfmt (push) Successful in 33s
textsender_api Build / Test Suite (push) Successful in 4m6s
textsender_api Build / Clippy (push) Successful in 1m10s
textsender_api Build / build (push) Successful in 4m6s
Reviewed-on: phoenix/textsender_api#32
2026-07-10 18:21:10 -04:00
phoenix 79f9fc3972 Update rust (#31)
Rust Build / Check (push) Successful in 2m28s
Rust Build / Test Suite (push) Successful in 3m12s
Rust Build / Rustfmt (push) Successful in 1m8s
Rust Build / Clippy (push) Successful in 1m27s
Rust Build / build (push) Successful in 3m59s
textsender_api PR / Clippy (pull_request) Has been cancelled
textsender_api PR / Check (pull_request) Has been cancelled
textsender_api PR / Rustfmt (pull_request) Has been cancelled
Reviewed-on: phoenix/textsender_api#31
2026-07-06 22:31:22 -04:00
phoenix 23f9a1430a Update dependencies (#30)
Rust Build / Test Suite (push) Failing after 58s
Rust Build / Rustfmt (push) Successful in 27s
Rust Build / Clippy (push) Successful in 1m21s
Rust Build / Check (push) Successful in 2m7s
Rust Build / build (push) Successful in 3m5s
textsender_api PR / Rustfmt (pull_request) Successful in 2m0s
textsender_api PR / Check (pull_request) Successful in 2m2s
textsender_api PR / Clippy (pull_request) Successful in 2m37s
Reviewed-on: phoenix/textsender_api#30
2026-07-04 21:07:30 -04:00
phoenix ba485261da Fixed issue (#29)
Rust Build / Rustfmt (push) Successful in 46s
Rust Build / Test Suite (push) Successful in 1m21s
Rust Build / Check (push) Successful in 2m20s
Rust Build / Clippy (push) Successful in 1m48s
Rust Build / build (push) Successful in 2m11s
textsender_api PR / Rustfmt (pull_request) Successful in 1m6s
textsender_api PR / Clippy (pull_request) Successful in 1m22s
textsender_api PR / Check (pull_request) Successful in 2m9s
Reviewed-on: phoenix/textsender_api#29
2026-07-04 18:34:51 -04:00
phoenix 71da521a53 CORS bug fix (#28)
Rust Build / Test Suite (push) Successful in 2m15s
Rust Build / Rustfmt (push) Successful in 48s
Rust Build / Check (push) Successful in 1m59s
Rust Build / Clippy (push) Successful in 1m38s
Rust Build / build (push) Successful in 3m44s
Reviewed-on: phoenix/textsender_api#28
2026-06-30 11:55:32 -04:00
phoenix 166b6baa85 Merge pull request 'A lot of changes' (#12) from alot_of_changes into main
Rust Build / Rustfmt (push) Successful in 1m23s
Rust Build / Check (push) Successful in 2m8s
Rust Build / Clippy (push) Successful in 1m40s
Rust Build / Test Suite (push) Failing after 48s
Rust Build / build (push) Successful in 2m28s
textsender_api PR / Rustfmt (pull_request) Successful in 1m7s
textsender_api PR / Clippy (pull_request) Successful in 1m56s
textsender_api PR / Check (pull_request) Successful in 2m3s
Reviewed-on: phoenix/textsender_api#12
2026-06-27 17:56:22 -04:00
phoenix 56cbd7710f Updated workflow
textsender_api PR / Rustfmt (pull_request) Successful in 47s
textsender_api PR / Check (pull_request) Successful in 1m46s
textsender_api PR / Clippy (pull_request) Successful in 1m37s
2026-06-27 17:52:47 -04:00
phoenix 44b1e50dc4 bump: textsender_api
textsender_api PR / Rustfmt (pull_request) Successful in 38s
textsender_api PR / Check (pull_request) Successful in 2m1s
textsender_api PR / Clippy (pull_request) Successful in 3m22s
2026-06-27 17:51:22 -04:00
phoenix 09e2a0e78e cargo update 2026-06-27 17:50:47 -04:00
phoenix 5220646eb9 bump: swoosh and textsender_models 2026-06-27 17:49:34 -04:00
phoenix c982d5595a Adding catapult to docker 2026-06-27 17:41:21 -04:00
phoenix 708e3e86f3 Twilio Config refactor (#27)
textsender_api PR / Rustfmt (pull_request) Successful in 38s
textsender_api PR / Check (pull_request) Successful in 1m59s
textsender_api PR / Clippy (pull_request) Successful in 2m52s
Reviewed-on: phoenix/textsender_api#27
2026-06-21 21:13:06 -04:00
phoenix d37d087d44 bump: sqlx (#26)
textsender_api PR / Rustfmt (pull_request) Successful in 40s
textsender_api PR / Check (pull_request) Successful in 1m48s
textsender_api PR / Clippy (pull_request) Successful in 2m53s
Reviewed-on: phoenix/textsender_api#26
2026-06-20 23:11:14 -04:00
phoenix 276d5b60d4 Postgres update (#25)
textsender_api PR / Rustfmt (pull_request) Successful in 43s
textsender_api PR / Clippy (pull_request) Successful in 1m47s
textsender_api PR / Check (pull_request) Successful in 2m49s
Reviewed-on: phoenix/textsender_api#25
2026-06-20 20:15:04 -04:00
phoenix 58fa5e0d26 Instant message (#24)
textsender_api PR / Rustfmt (pull_request) Successful in 47s
textsender_api PR / Clippy (pull_request) Successful in 1m57s
textsender_api PR / Check (pull_request) Successful in 2m7s
Reviewed-on: phoenix/textsender_api#24
2026-06-20 20:07:19 -04:00
phoenix 295140a216 Get Message Event Response endpoint (#23)
textsender_api PR / Clippy (pull_request) Successful in 1m22s
textsender_api PR / Check (pull_request) Successful in 2m11s
textsender_api PR / Rustfmt (pull_request) Successful in 50s
Rust Build / Rustfmt (pull_request) Successful in 37s
Rust Build / Test Suite (pull_request) Successful in 40s
Reviewed-on: phoenix/textsender_api#23
2026-06-20 15:05:20 -04:00
phoenix a9d7429d75 Record Message Event Response endpoint (#22)
textsender_api PR / Rustfmt (pull_request) Successful in 48s
textsender_api PR / Check (pull_request) Successful in 1m39s
textsender_api PR / Clippy (pull_request) Successful in 1m39s
Reviewed-on: phoenix/textsender_api#22
2026-06-20 13:38:23 -04:00
phoenix e6f14917f0 Fetch schedule message endpoint (#21)
textsender_api PR / Check (pull_request) Successful in 1m22s
textsender_api PR / Clippy (pull_request) Successful in 2m5s
Rust Build / Rustfmt (pull_request) Successful in 41s
textsender_api PR / Rustfmt (pull_request) Successful in 49s
Rust Build / Test Suite (pull_request) Successful in 35s
Reviewed-on: phoenix/textsender_api#21
2026-06-20 12:03:46 -04:00
phoenix c98d545afe Update scheduled message status endpoint (#20)
textsender_api PR / Rustfmt (pull_request) Successful in 38s
textsender_api PR / Check (pull_request) Successful in 1m14s
textsender_api PR / Clippy (pull_request) Failing after 1m46s
Reviewed-on: phoenix/textsender_api#20
2026-06-19 18:34:39 -04:00
phoenix 3a387813d7 Delete scheduled message event endpoint (#19)
textsender_api PR / Check (pull_request) Successful in 1m32s
textsender_api PR / Clippy (pull_request) Successful in 1m37s
textsender_api PR / Rustfmt (pull_request) Successful in 56s
Reviewed-on: phoenix/textsender_api#19
2026-06-19 16:02:53 -04:00
phoenix c1557d09f6 Get scheduled message event (#18)
textsender_api PR / Check (pull_request) Successful in 1m25s
textsender_api PR / Rustfmt (pull_request) Successful in 1m34s
textsender_api PR / Clippy (pull_request) Successful in 2m0s
Reviewed-on: phoenix/textsender_api#18
2026-06-18 19:59:22 -04:00
phoenix 7873e8ed00 Create scheduled message event (#17)
textsender_api PR / Rustfmt (pull_request) Successful in 40s
textsender_api PR / Check (pull_request) Successful in 1m33s
textsender_api PR / Clippy (pull_request) Successful in 2m12s
Reviewed-on: phoenix/textsender_api#17
2026-06-18 18:39:24 -04:00
phoenix 825575930e Get schedule message endpoint (#16)
textsender_api PR / Rustfmt (pull_request) Successful in 38s
textsender_api PR / Check (pull_request) Successful in 1m38s
textsender_api PR / Clippy (pull_request) Successful in 2m3s
Reviewed-on: phoenix/textsender_api#16
2026-06-18 14:19:21 -04:00
phoenix d1bc5eb830 Schedule message (#15)
textsender_api PR / Rustfmt (pull_request) Successful in 52s
textsender_api PR / Clippy (pull_request) Successful in 1m32s
textsender_api PR / Check (pull_request) Successful in 2m12s
Reviewed-on: phoenix/textsender_api#15
2026-06-17 17:51:33 -04:00
phoenix 5740bce97c Get message endpoint (#14)
textsender_api PR / Rustfmt (pull_request) Successful in 45s
textsender_api PR / Clippy (pull_request) Successful in 1m30s
textsender_api PR / Check (pull_request) Successful in 2m10s
Reviewed-on: phoenix/textsender_api#14
2026-06-17 15:54:26 -04:00
phoenix d86958035e Add message endpoint (#13)
textsender_api PR / Clippy (pull_request) Successful in 1m8s
textsender_api PR / Check (pull_request) Successful in 2m47s
textsender_api PR / Rustfmt (pull_request) Successful in 38s
Reviewed-on: phoenix/textsender_api#13
2026-06-17 14:25:53 -04:00
phoenix 98e93b175e The bulk (#11)
Rust Build / Rustfmt (push) Successful in 1m26s
Rust Build / Test Suite (push) Successful in 1m41s
Rust Build / Check (push) Successful in 2m19s
Rust Build / Clippy (push) Successful in 1m36s
textsender_api PR / Rustfmt (pull_request) Successful in 31s
Rust Build / build (push) Successful in 2m11s
textsender_api PR / Clippy (pull_request) Successful in 1m39s
textsender_api PR / Check (pull_request) Successful in 2m30s
Reviewed-on: phoenix/textsender_api#11
2026-06-17 13:40:15 -04:00
phoenix a226adcd0f Get contact (#10)
Rust Build / build (push) Successful in 2m57s
Rust Build / Test Suite (push) Successful in 49s
Rust Build / Rustfmt (push) Successful in 56s
Rust Build / Clippy (push) Successful in 1m53s
Rust Build / Check (push) Successful in 3m0s
Reviewed-on: phoenix/textsender_api#10
2026-06-14 23:25:35 -04:00
phoenix 3117f46a73 Updating workflow for main workflows (#9)
Rust Build / Rustfmt (push) Successful in 43s
Rust Build / Clippy (push) Successful in 1m48s
Rust Build / build (push) Successful in 2m40s
Rust Build / Check (push) Successful in 1m55s
Rust Build / Test Suite (push) Successful in 2m42s
Reviewed-on: phoenix/textsender_api#9
2026-06-14 19:12:25 -04:00
phoenix ff35a8c3aa Merge pull request 'v0.2.0' (#3) from v0.2.0 into main
Reviewed-on: phoenix/textsender_api#3
2026-06-13 19:49:37 -04:00
phoenix e940d6b159 First one (#8)
Rust Build / Test Suite (push) Successful in 59s
Rust Build / Check (push) Successful in 1m40s
Rust Build / Rustfmt (push) Successful in 1m52s
Rust Build / Clippy (push) Successful in 1m25s
Rust Build / build (push) Successful in 2m20s
Reviewed-on: phoenix/textsender_api#8
2026-06-13 19:49:14 -04:00
phoenix 7107a50d58 Update name (#7)
Rust Build / Test Suite (push) Successful in 1m35s
Rust Build / Check (push) Successful in 1m39s
Rust Build / build (push) Successful in 1m37s
Rust Build / Rustfmt (push) Successful in 54s
Rust Build / Clippy (push) Successful in 2m33s
Rust Build / Check (pull_request) Successful in 1m13s
Rust Build / Test Suite (pull_request) Successful in 1m27s
Rust Build / Rustfmt (pull_request) Successful in 1m28s
Rust Build / Clippy (pull_request) Successful in 1m19s
Rust Build / build (pull_request) Successful in 1m46s
Reviewed-on: phoenix/textsender_api#7
2026-06-13 14:04:50 -04:00
phoenix 1d9267e036 Add docker (#5)
Rust Build / Rustfmt (push) Successful in 36s
Rust Build / Test Suite (push) Successful in 1m9s
Rust Build / Check (push) Successful in 1m14s
Rust Build / Clippy (push) Successful in 2m25s
Rust Build / build (push) Successful in 3m5s
Rust Build / Rustfmt (pull_request) Successful in 40s
Rust Build / Check (pull_request) Successful in 2m12s
Rust Build / Clippy (pull_request) Successful in 1m30s
Rust Build / Test Suite (pull_request) Successful in 2m39s
Rust Build / build (pull_request) Successful in 2m51s
Reviewed-on: phoenix/textsender-api#5
2026-06-12 22:17:46 -04:00
phoenix 2610162da1 Adding rust files (#4)
Rust Build / Check (push) Successful in 31s
Rust Build / Test Suite (push) Successful in 34s
Rust Build / Rustfmt (push) Successful in 27s
Rust Build / Clippy (push) Successful in 36s
Rust Build / build (push) Successful in 24s
Reviewed-on: phoenix/textsender-api#4
2026-05-31 18:42:08 -04:00
phoenix 31e0162cc8 Clearing files (#2)
Reviewed-on: phoenix/textsender-api#2
2026-05-31 18:28:28 -04:00
phoenix 94b80aeecc Docker fixes (#1)
Go / build (push) Successful in 8s
Go / Test (push) Successful in 16s
Go / build (pull_request) Successful in 1m46s
Go / Test (pull_request) Successful in 1m47s
Reviewed-on: phoenix/textsender-api#1
2026-05-29 19:11:17 -04:00
phoenixandphoenix 9dc4336761 Update go (#69)
Go / build (pull_request) Failing after 10s
Go / Test (pull_request) Failing after 1m33s
Reviewed-on: phoenix/textsender-api#69
Co-authored-by: phoenix <mail@kundeng.us>
Co-committed-by: phoenix <mail@kundeng.us>
2026-05-03 17:16:15 -04:00
phoenixandphoenix 69a20cfbbe Update go (#68)
Reviewed-on: phoenix/textsender-api#68
Co-authored-by: phoenix <mail@kundeng.us>
Co-committed-by: phoenix <mail@kundeng.us>
2026-04-04 22:16:49 -04:00
phoenixandphoenix a51979e724 tsk-58: Update names of Contact endpoint (#67)
Closes #58

Reviewed-on: phoenix/textsender-api#67
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-07 20:24:30 +00:00
phoenixandphoenix 4e250d095c tsk-60: Save extra fields to DB for Contact (#66)
Closes #60

Reviewed-on: phoenix/textsender-api#66
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-06 02:40:01 +00:00
phoenix cac1a9d97d bug_fix (#64)
Reviewed-on: phoenix/textsender-api#64
2026-01-01 21:04:28 +00:00
phoenix a33af13f45 textsender-models and swoosh version bump 2026-01-01 16:00:49 -05:00
phoenix 0e956da330 Missed another one 2026-01-01 14:33:44 -05:00
phoenix a0bab48f40 Could it be? 2026-01-01 14:28:55 -05:00
phoenix 400490b633 Whoops 2026-01-01 14:19:36 -05:00
phoenix b33c05531b Test change 2026-01-01 14:18:56 -05:00
phoenix 08650619dc Minor refactor 2026-01-01 14:11:44 -05:00
phoenix 161d0da377 Another one 2026-01-01 14:09:20 -05:00
phoenix 0334c67c36 Fix another test issue 2026-01-01 14:07:09 -05:00
phoenix 49f5e0ea40 Fix build issue 2026-01-01 14:04:06 -05:00
phoenix f73c959830 More test changes 2026-01-01 14:01:58 -05:00
phoenix 57619759d5 Test changes 2026-01-01 13:56:51 -05:00
phoenixandphoenix 59c9aa0ff1 tsk-59: Get MessageEventResponse endpoint (#61)
Closes #59

Reviewed-on: phoenix/textsender-api#61
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-01 05:28:49 +00:00
phoenixandphoenix 6641f384ab tsk-62: Modify how the Response from event.MessageEventResponse is handled (#63)
Closes #62

Reviewed-on: phoenix/textsender-api#63
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-01 01:39:12 +00:00
phoenixandphoenix 8860d6c801 tsk-57: Change schema of scheduled_message_events (#65)
Closes #57

Reviewed-on: phoenix/textsender-api#65
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2026-01-01 00:59:14 +00:00
phoenix 29f8c943a6 tsk-59: Tweaking code 2025-12-31 15:11:04 -05:00
phoenix 996b67d890 tsk-59: Made endpoint available 2025-12-31 14:49:49 -05:00
phoenix b58174eef2 tsk-59: Got the code for MER and added TODO 2025-12-31 14:44:43 -05:00
phoenix 1a3a244c07 tsk-59: Changes to store and mock package 2025-12-31 14:28:09 -05:00
phoenix aefa26199d tsk-59: Added endpoint constant and made changes to handler 2025-12-30 19:30:23 -05:00
phoenix 8728fe9b60 tsk-59: Added code to get MessageEventResponse 2025-12-30 19:13:51 -05:00
phoenix d3ce3583a2 changes (#55)
Reviewed-on: phoenix/textsender-api#55
2025-12-22 22:54:40 +00:00
phoenix 14f50a118f Test fixes 2025-12-22 17:52:45 -05:00
phoenix fb17c02f69 Code formatting 2025-12-22 17:48:37 -05:00
phoenix 0062811cb6 Modified mock package 2025-12-22 17:48:07 -05:00
phoenix 84fed38a67 Added mock Contact store 2025-12-22 17:47:51 -05:00
phoenixandphoenix 6fa5ede358 tsk-54: Tweak instant message endpoint to save contact and message identification (#56)
Closes #54

Reviewed-on: phoenix/textsender-api#56
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-22 22:39:29 +00:00
phoenixandphoenix f5f753fe02 tsk-4: Adding CORS support (#53)
Closes #4

Reviewed-on: phoenix/textsender-api#53
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-12-21 00:28:17 +00:00
phoenix 69b5f4ff89 tsk-4: Formatted code 2025-12-13 15:30:09 -05:00
phoenix 09744d4cdf tsk-4: Fixed code 2025-12-13 15:29:54 -05:00
phoenix fa8c7062fc tsk-4: Switched cors package 2025-12-13 15:29:39 -05:00
phoenix fd8fdd93b9 tsk-4: Adding CORS support 2025-12-12 13:59:04 -05:00
phoenix fbaa95b2e9 tsk-4: Added cors package 2025-12-12 13:57:55 -05:00
phoenix 37dcc437de tsk-4: Adding cors package 2025-12-11 17:37:07 -05:00
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
67 changed files with 8327 additions and 3521 deletions
+2 -2
View File
@@ -1,5 +1,3 @@
vendor/
# Ignore git directory # Ignore git directory
.git/ .git/
@@ -7,3 +5,5 @@ vendor/
# Ignore environment files (configure via docker-compose instead) # Ignore environment files (configure via docker-compose instead)
.env* .env*
target/
+13 -6
View File
@@ -1,7 +1,14 @@
JWT_SECRET=NULqYIzgt28bTiyziCd7IOO7b6LnWDW! JWT_SECRET=NULqYIzgt28bTiyziCd7IOO7b6LnWDW!
DB_NAME=textsender_db DB_MAIN_NAME=schedtxt_db
DB_USER=textsender DB_MAIN_USER=schedtxt
DB_PASSWORD=password DB_MAIN_PASSWORD=password
DB_HOST=main_db DB_MAIN_HOST=main_db
DB_PORT=5432 DB_MAIN_PORT=5432
DB_SSLMODE=disable DB_MAIN_SSLMODE=disable
DATABASE_URL=postgres://${DB_MAIN_USER}:${DB_MAIN_PASSWORD}@${DB_MAIN_HOST}:${DB_MAIN_PORT}/${DB_MAIN_NAME}
TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U
TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T
TWILIO_AUTH_TOKEN="MA08HJp9tYtkfs72m8JIIROofRVm74oR0ixEYBHGYpB9"
TWILIO_PHONE_NUMBER=+10123456789
ALLOWED_ORIGINS="http://schedtxt.com,http://localhost:5173,http://localhost:9080,http://localhost:9081"
APP_ENV=production
+14
View File
@@ -0,0 +1,14 @@
JWT_SECRET=NULqYIzgt28bTiyziCd7IOO7b6LnWDW!
DB_MAIN_NAME=schedtxt_db
DB_MAIN_USER=schedtxt
DB_MAIN_PASSWORD=password
DB_MAIN_HOST=localhost
DB_MAIN_PORT=5432
DB_MAIN_SSLMODE=disable
DATABASE_URL=postgres://${DB_MAIN_USER}:${DB_MAIN_PASSWORD}@${DB_MAIN_HOST}:${DB_MAIN_PORT}/${DB_MAIN_NAME}
TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U
TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T
TWILIO_AUTH_TOKEN="MA08HJp9tYtkfs72m8JIIROofRVm74oR0ixEYBHGYpB9"
TWILIO_PHONE_NUMBER=+10123456789
ALLOWED_ORIGINS="http://schedtxt.com,http://localhost:5173,http://localhost:9080,http://localhost:9081"
APP_ENV=production
-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
+71
View File
@@ -0,0 +1,71 @@
name: schedtxt_api PR
on:
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
name: Check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo check
fmt:
name: Rustfmt
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- run: rustup component add clippy
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo clippy -- -D warnings
+91
View File
@@ -0,0 +1,91 @@
name: schedtxt_api Test
on:
pull_request:
branches:
- next
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Test Suite
runs-on: ubuntu-24.04
# --- Add database service definition ---
services:
postgres:
image: postgres:18.4-alpine
env:
# Use secrets for DB init, with fallbacks for flexibility
POSTGRES_USER: ${{ secrets.DB_TEST_USER || 'testuser' }}
POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}
POSTGRES_DB: ${{ secrets.DB_TEST_NAME || 'testdb' }}
POSTGRES_PORT: ${{ secrets.DB_PORT || 5432 }}
# Options to wait until the database is ready
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
# --- Add this step for explicit verification ---
- name: Verify Docker Environment
run: |
echo "Runner User Info:"
id
echo "Checking Docker Version:"
docker --version
echo "Checking Docker Daemon Status (info):"
docker info
echo "Checking Docker Daemon Status (ps):"
docker ps -a
echo "Docker environment check complete."
# NOTE: Do NOT use continue-on-error here.
# If Docker isn't working as expected, the job SHOULD fail here.
- name: Run tests
env:
# Define DATABASE_URL for tests to use
DATABASE_URL: postgresql://${{ secrets.DB_TEST_USER || 'testuser' }}:${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}@postgres:${{ secrets.DB_PORT || 5432 }}/${{ secrets.DB_TEST_NAME || 'testdb' }}
RUST_LOG: info # Optional: configure test log level
SECRET_MAIN_KEY: ${{ secrets.SECRET_KEY }}
# Make SSH agent available if tests fetch private dependencies
SSH_AUTH_SOCK: ${{ env.SSH_AUTH_SOCK }}
ENABLE_REGISTRATION: 'TRUE'
run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo test
fmt:
name: Rustfmt
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo fmt --all -- --check
-128
View File
@@ -1,128 +0,0 @@
name: Go
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-24.04 # You can change this to macos-latest or windows-latest if needed
steps:
- uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.25.3' # You can specify a specific version or 'stable'
- name: Build
run: |
echo "Initializing config"
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender_models_deploy_key
chmod 600 ~/.ssh/textsender_models_deploy_key
ssh-keyscan ${{ vars.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender_models_deploy_key
go env -w GOPRIVATE='${{ secrets.GIT_HOST_ROOT }}'
echo "Creating local .gitconfig"
touch ~/.gitconfig
cat > ~/.gitconfig << "EOF"
[url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"]
insteadOf = https://${{ secrets.GIT_HOST_ROOT }}
EOF
echo "Building binary"
make build
echo "Binary built"
file textsender-api
test:
name: Test
runs-on: ubuntu-24.04
services:
postgres:
image: postgres:18.0
env:
POSTGRES_USER: ${{ secrets.DB_TEST_USER }}
POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }}
POSTGRES_DB: ${{ secrets.DB_TEST_NAME }}
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.25.3'
- name: Install PostgreSQL client
run: sudo apt update && sudo apt-get install -y postgresql-client
- name: Wait for PostgreSQL to be ready
run: |
for i in {1..30}; do
if pg_isready -h postgres -p 5432; then
echo "PostgreSQL is ready"
exit 0
fi
echo "Waiting for PostgreSQL... Attempt $i"
sleep 2
done
echo "PostgreSQL did not start in time"
exit 1
- name: Run tests
env:
DB_NAME: ${{ secrets.DB_TEST_NAME }}
DB_USER: ${{ secrets.DB_TEST_USER }}
DB_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }}
DB_HOST: postgres
DB_PORT: 5432
DB_SSLMODE: disable
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: |
echo "Parent directory"
echo `pwd`
echo "JWT_SECRET=$SECRET_KEY" >> .env
echo "DB_NAME=$DB_NAME" > .env
echo "DB_USER=$DB_USER" >> .env
echo "DB_PASSWORD=$DB_PASSWORD" >> .env
echo "DB_HOST=$DB_HOST" >> .env
echo "DB_PORT=$DB_PORT" >> .env
echo "DB_SSLMODE=$DB_SSLMODE" >> .env
echo "Initializing config"
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender_models_deploy_key
chmod 600 ~/.ssh/textsender_models_deploy_key
ssh-keyscan ${{ vars.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender_models_deploy_key
go env -w GOPRIVATE='${{ secrets.GIT_HOST_ROOT }}'
echo "Creating local .gitconfig"
touch ~/.gitconfig
cat > ~/.gitconfig << "EOF"
[url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"]
insteadOf = https://${{ secrets.GIT_HOST_ROOT }}
EOF
go test -v ./...
+150
View File
@@ -0,0 +1,150 @@
name: schedtxt_api Build
on:
push:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check:
name: Check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo check
test:
name: Test Suite
runs-on: ubuntu-24.04
# --- Add database service definition ---
services:
postgres:
image: postgres:18.4-alpine
env:
# Use secrets for DB init, with fallbacks for flexibility
POSTGRES_USER: ${{ secrets.DB_TEST_USER || 'testuser' }}
POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}
POSTGRES_DB: ${{ secrets.DB_TEST_NAME || 'testdb' }}
POSTGRES_PORT: ${{ secrets.DB_PORT || 5432 }}
# Options to wait until the database is ready
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
# --- Add this step for explicit verification ---
- name: Verify Docker Environment
run: |
echo "Runner User Info:"
id
echo "Checking Docker Version:"
docker --version
echo "Checking Docker Daemon Status (info):"
docker info
echo "Checking Docker Daemon Status (ps):"
docker ps -a
echo "Docker environment check complete."
# NOTE: Do NOT use continue-on-error here.
# If Docker isn't working as expected, the job SHOULD fail here.
- name: Run tests
env:
# Define DATABASE_URL for tests to use
DATABASE_URL: postgresql://${{ secrets.DB_TEST_USER || 'testuser' }}:${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}@postgres:${{ secrets.DB_PORT || 5432 }}/${{ secrets.DB_TEST_NAME || 'testdb' }}
RUST_LOG: info # Optional: configure test log level
SECRET_MAIN_KEY: ${{ secrets.SECRET_KEY }}
# Make SSH agent available if tests fetch private dependencies
SSH_AUTH_SOCK: ${{ env.SSH_AUTH_SOCK }}
ENABLE_REGISTRATION: 'TRUE'
run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo test
fmt:
name: Rustfmt
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo fmt --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- run: rustup component add clippy
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo clippy -- -D warnings
build:
name: build
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.97
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/schedtxt-models_deploy_key
chmod 600 ~/.ssh/schedtxt-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/schedtxt-models_deploy_key
cargo build --release
+1 -3
View File
@@ -1,6 +1,4 @@
/textsender-api
/vendor/
.env .env
.env.local .env.local
.env.docker .env.docker
target/*
Generated
+3648
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "schedtxt_api"
version = "0.2.4"
edition = "2024"
license = "MIT"
description = "Core API for sending text messages"
rust-version = "1.97"
[dependencies]
axum = { version = "0.8.9", features = ["multipart"] }
axum-extra = { version = "0.12.6", features = ["cookie"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = { version = "1.0.150" }
tokio = { version = "1.52.3", features = ["full"] }
tower-http = { version = "0.7.0", features = ["cors", "timeout"] }
tracing-subscriber = "0.3.23"
uuid = { version = "1.23.5", features = ["v4", "serde"] }
sqlx = { version = "0.9.0", features = ["runtime-tokio", "tls-native-tls", "postgres", "time", "uuid"] }
time = { version = "0.3.53", features = ["formatting", "macros", "parsing", "serde"] }
jsonwebtoken = { version = "10.4.0", features = ["rust_crypto"] }
utoipa = { version = "5.5.0", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] }
schedtxt_models = { git = "ssh://git@git.kundeng.us/phoenix/schedtxt_models.git", tag = "v0.5.3" }
swoosh = { git = "ssh://git@git.kundeng.us/phoenix/swoosh.git", tag = "v0.5.4" }
[dev-dependencies]
tower = { version = "0.5.3", features = ["full"] }
josekit = { version = "0.10.3" }
url = { version = "2.5.8" }
+32 -34
View File
@@ -1,52 +1,50 @@
# Multi-stage Dockerfile for Go application FROM rust:1.97 as builder
FROM golang:1.25.3 AS builder
WORKDIR /app # Set the working directory inside the container
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config libssl3 \
ca-certificates \ ca-certificates \
openssh-client git openssh-client git \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p -m 0700 ~/.ssh && \ RUN mkdir -p -m 0700 ~/.ssh && \
ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts echo "Host git.kundeng.us" >> ~/.ssh/config && \
echo " User git" >> ~/.ssh/config && \
chmod 600 ~/.ssh/config
# Configure Git to use SSH for GitHub RUN ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts
RUN git config --global url."ssh://git@git.kundeng.us".insteadOf "https://git.kundeng.us"
# Set up the Go environment for private modules COPY Cargo.toml Cargo.lock ./
ENV GOPRIVATE=git.kundeng.us
# Copy go mod and sum files
COPY go.mod go.sum ./
RUN --mount=type=ssh mkdir src && \ RUN --mount=type=ssh mkdir src && \
go mod download echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \
cargo build --release --quiet && \
rm -rf src target/release/deps/schedtxt_api*
# Copy source code # Copy the actual source code
COPY ./cmd ./cmd COPY src ./src
COPY ./internal ./internal COPY .env ./.env
COPY ./Makefile . COPY migrations ./migrations
COPY ./.env .
COPY ./migrations ./migrations
# Build the application RUN --mount=type=ssh \
RUN CGO_ENABLED=0 GOOS=linux make build cargo build --release --quiet
# Runtime stage FROM debian:trixie-slim
FROM alpine:latest AS production
RUN apk --no-cache add ca-certificates # Install runtime dependencies if needed (e.g., SSL certificates)
RUN apt-get update && apt-get install -y ca-certificates libssl-dev libssl3 && rm -rf /var/lib/apt/lists/*
WORKDIR /root/ # Set the working directory
WORKDIR /usr/local/bin
# Copy the pre-built binary file from the previous stage COPY --from=builder /usr/src/app/target/release/schedtxt_api .
COPY --from=builder /app/textsender-api .
COPY --from=builder /app/.env ./
COPY --from=builder /app/migrations ./migrations
# Expose port COPY --from=builder /usr/src/app/.env .
EXPOSE 8080 COPY --from=builder /usr/src/app/migrations ./migrations
# Command to run the executable EXPOSE 9081
CMD ["./textsender-api"]
# Set the command to run your application
CMD ["./schedtxt_api"]
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2026 Kun Deng.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
-21
View File
@@ -1,21 +0,0 @@
VERSION ?= $(shell git describe --tags 2>/dev/null || echo "dev")
COMMIT ?= $(shell git rev-parse --short HEAD)
BUILD_TIME ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
GO_VERSION ?= $(shell go version | awk '{print $$3}')
.PHONY: build
build:
go build -ldflags="\
-X 'git.kundeng.us/phoenix/textsender-api/internal/version.Version=$(VERSION)' \
-X 'git.kundeng.us/phoenix/textsender-api/internal/version.BuildTime=$(BUILD_TIME)' \
-X 'git.kundeng.us/phoenix/textsender-api/internal/version.Commit=$(COMMIT)' \
-X 'git.kundeng.us/phoenix/textsender-api/internal/version.GoVersion=$(GO_VERSION)'" \
-o textsender-api cmd/api/main.go
.PHONY: install
install:
go install -ldflags="\
-X 'git.kundeng.us/phoenix/textsender-api/internal/version.Version=$(VERSION)' \
-X 'git.kundeng.us/phoenix/textsender-api/internal/version.BuildTime=$(BUILD_TIME)' \
-X 'git.kundeng.us/phoenix/textsender-api/internal/version.Commit=$(COMMIT)' \
-X 'git.kundeng.us/phoenix/textsender-api/internal/version.GoVersion=$(GO_VERSION)'"
+41 -5
View File
@@ -1,10 +1,46 @@
# TextSender-API # schedtxt_api
A software system to process Text messaging queueing Core API to send text messages
## Getting started ## Getting started
Fastest way to get started is with docker. Make sure that is installed before moving forward.
This application requires `schedtxt_auth` and `catapult` repositories to be located in the
parent directory.
### Building ```
```BASH .
make build ..
- schedtxt_auth/
- schedtxt_api/
- catapult/
```
Make sure the othe repositories are configured as it is required to operate successfully.
Copy over the `.env.docker.sample` to `.env` to get started.
### Token service
The `JWT_SECRET` environment variable should be the same variable as `SECRET_MAIN_KEY` in
`schedtxt_auth`.
### Messaging configuration
schedtxt uses `twilio` as the service to send the messages. You will need to sign up with
them and be provided an account SID, service SID, auth token, and phone number. Update the
corresponding `TWILIO_*` variables.
### Database configuration
Make sure to provide values for the `DB_MAIN_*` variables. For the sake of development, it
can be left as it is.
Build the container
```
docker build
```
Run the container
```
docker compose up
``` ```
-125
View File
@@ -1,125 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"git.kundeng.us/phoenix/textsender-api/internal/config"
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/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"
)
func main() {
cfg := config.Load()
if cfg == nil {
fmt.Println("Error initializing config")
os.Exit(-1)
} else if cfg.JWTSecret == "" {
fmt.Println("Error: JWTSecret not initialized")
os.Exit(-1)
}
db, err := database.NewDatabase(cfg.GetDBConnString())
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()
ctx := context.Background()
if cfg.ResetDB {
log.Println("Resetting database")
if err := db.ResetDatabase(ctx); err != nil {
log.Fatalf("Failed to reset database: %v", err)
}
log.Println("Database reset completed. Exiting.")
return
} else {
if exists, err := database.TableExists(ctx, db.Pool, "contacts "); err == nil && !exists {
fmt.Println("Resetting database")
err = db.ResetDatabase(ctx)
if err != nil {
fmt.Printf("Error:%v", err)
}
} else {
fmt.Printf("Error:%v", err)
}
}
jwtService := services.NewJWTService(cfg.JWTSecret)
contactStore := store.NewContactStore(db.Pool)
messageStore := store.NewMessageStore(db.Pool)
schStore := store.NewScheduledMessageStore(db.Pool)
schMsgEventStore := store.NewScheduledMessageEventStore(db.Pool)
contactHandler := handler.NewContactHandler(contactStore)
messageHandler := handler.NewMessageHandler(messageStore)
scheduledMessageHandler := handler.NewScheduledMessageHandler(schStore)
scheduledMessageEventHandler := handler.NewScheduledMessageEventHandler(schMsgEventStore, schStore)
scheduledMessageStatusHandler := handler.NewScheduledMessageStatusHandler(schMsgEventStore, schStore)
router := chi.NewRouter()
router.Use(middleware.Logger)
router.Use(middleware.Recoverer)
router.Use(middleware.Timeout(60 * time.Second))
router.Use(mdlware.JSONContentType)
router.Handle(endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact)))
router.Handle(endpoint.GET_CONTACT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.GetContact)))
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.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)))
// Start server
server := &http.Server{
Addr: ":" + cfg.ServerPort,
Handler: router,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Graceful shutdown
go func() {
log.Printf("Server starting on port %s", cfg.ServerPort)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
-116
View File
@@ -1,116 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"net/http"
"os"
"path"
"testing"
"github.com/go-chi/chi/v5"
"github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-api/internal/config"
"git.kundeng.us/phoenix/textsender-api/internal/db"
"git.kundeng.us/phoenix/textsender-api/internal/handler"
"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"
)
var testRouter *chi.Mux
func TestMain(m *testing.M) {
cfg := load()
database, err := db.NewDatabase(cfg.GetDBConnString())
if err != nil {
fmt.Println(err.Error())
panic("Failed to initialize database")
}
defer database.Close()
ctx := context.Background()
if cfg.ResetDB {
if err = database.ResetDatabase(ctx); err != nil {
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)
}
}
jwtService := services.NewJWTService(cfg.JWTSecret)
contactStore := store.NewContactStore(database.Pool)
messageStore := store.NewMessageStore(db.Pool)
schStore := store.NewScheduledMessageStore(db.Pool)
schMsgEventStore := store.NewScheduledMessageEventStore(db.Pool)
contactHandler := handler.NewContactHandler(contactStore)
messageHandler := handler.NewMessageHandler(messageStore)
scheduledMessageHandler := handler.NewScheduledMessageHandler(schStore)
scheduledMessageEventHandler := handler.NewScheduledMessageEventHandler(schMsgEventStore, schStore)
scheduledMessageStatusHandler := handler.NewScheduledMessageStatusHandler(schMsgEventStore, schStore)
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)))
code := m.Run()
os.Exit(code)
}
func load() *config.Config {
resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit")
port := flag.String("port", config.PORT, "Server port")
flag.Parse()
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())
}
}
unpackedConnString := config.UnpackDBConnString()
dbConnString := unpackedConnString.Parse()
return &config.Config{
DBConnString: dbConnString,
ServerPort: *port,
ResetDB: *resetDb,
}
}
func resetTestDB(t *testing.T) {
t.Helper()
_, err := db.Pool.Exec(context.Background(), "DELETE FROM contacts")
if err != nil {
t.Fatalf("Failed to reset test database: %v", err)
}
}
-97
View File
@@ -1,97 +0,0 @@
version: '3.8' # Use a recent version
services:
api:
build: # Tells docker-compose to build the Dockerfile in the current directory
context: .
ssh: ["default"] # Uses host's SSH agent
container_name: textsender # Optional: Give the container a specific name
ports:
# Map host port 8000 to container port 3000 (adjust as needed)
- "8080:8080"
env_file:
- .env
depends_on:
main_db:
condition: service_healthy # Wait for the DB to be healthy before starting the app
networks:
- textsender-network
restart: unless-stopped # Optional: Restart policy
auth_api:
build: # Tells docker-compose to build the Dockerfile in the current directory
context: ../textsender-auth
ssh: ["default"] # Uses host's SSH agent
dockerfile: Dockerfile
container_name: textsender_auth # Optional: Give the container a specific name
ports:
# Map host port 8000 to container port 3000 (adjust as needed)
- "9080:9080"
env_file:
- ../textsender-auth/.env
depends_on:
auth_db:
condition: service_healthy # Wait for the DB to be healthy before starting the app
networks:
- textsender-network
restart: unless-stopped # Optional: Restart policy
# PostgreSQL Database Service
main_db:
image: postgres:18.0-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: textsender_db # Optional: Give the container a specific name
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${POSTGRES_AUTH_USER:-textsender}
POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_db}
volumes:
# Persist database data using a named volume
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
# Checks if Postgres is ready to accept connections
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
networks:
- textsender-network
restart: always # Optional: Restart policy
auth_db:
image: postgres:18.0-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: textsender_auth_db # Optional: Give the container a specific name
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${POSTGRES_AUTH_USER:-textsender_auth}
POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password}
POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_auth_db}
volumes:
# Persist database data using a named volume
- postgres_data_auth:/var/lib/postgresql/data
ports:
- "5433:5432"
healthcheck:
# Checks if Postgres is ready to accept connections
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
networks:
- textsender-network
restart: always # Optional: Restart policy
# Define the named volume for data persistence
volumes:
postgres_data:
driver: local # Use the default local driver
postgres_data_auth:
driver: local # Use the default local driver
networks:
textsender-network:
driver: bridge
+115
View File
@@ -0,0 +1,115 @@
version: '3.8' # Use a recent version
services:
# --- Web API ---
api:
build:
context: .
ssh: ["default"]
container_name: schedtxt_api
ports:
- "9081:9081"
env_file:
- .env
depends_on:
main_db:
condition: service_healthy
networks:
- schedtxt_api-network
restart: unless-stopped
# --- Auth API ---
auth_api:
build:
context: ../schedtxt_auth
ssh: ["default"]
dockerfile: Dockerfile
container_name: auth_api
restart: unless-stopped
ports:
- "9080:9080"
env_file:
- ../schedtxt_auth/.env
depends_on:
auth_db:
condition: service_healthy
networks:
- schedtxt_api-network
# --- catapult service ---
catapult:
build:
context: ../catapult
ssh: ["default"]
dockerfile: Dockerfile
container_name: catapult
restart: unless-stopped
env_file:
- ../catapult/.env
depends_on:
- api
- main_db
- auth_api
- auth_db
networks:
- schedtxt_api-network
# PostgreSQL Database Service
main_db:
image: postgres:18.4-alpine
container_name: schedtxt_api_db
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${DB_MAIN_USER:-schedtxt_api}
POSTGRES_PASSWORD: ${DB_MAIN_PASSWORD:-password}
POSTGRES_DB: ${DB_MAIN_NAME:-schedtxt_api_db}
volumes:
- postgres_data:/var/lib/postgresql
ports:
- "5432:5432"
healthcheck:
# Checks if Postgres is ready to accept connections
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: always
networks:
- schedtxt_api-network
# --- schedtxt_api web auth api db ---
auth_db:
image: postgres:18.4-alpine
container_name: schedtxt_api_auth_db
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${DB_AUTH_USER:-schedtxt_auth}
POSTGRES_PASSWORD: ${DB_AUTH_PASSWORD:-password}
POSTGRES_DB: ${DB_AUTH_NAME:-schedtxt_auth_db}
volumes:
# Persist database data using a named volume
- postgres_data_auth:/var/lib/postgresql
ports:
- "5433:5432"
healthcheck:
# Checks if Postgres is ready to accept connections
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
restart: always
networks:
- schedtxt_api-network
volumes:
postgres_data:
driver: local
postgres_data_auth:
driver: local
networks:
schedtxt_api-network:
driver: bridge
-28
View File
@@ -1,28 +0,0 @@
module git.kundeng.us/phoenix/textsender-api
go 1.25.3
require (
git.kundeng.us/phoenix/textsender-models v0.0.6-4-e124bcfda8-556
github.com/go-chi/chi/v5 v5.2.3
github.com/golang-jwt/jwt/v5 v5.3.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/joho/godotenv v1.5.1
github.com/stretchr/testify v1.11.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-49
View File
@@ -1,49 +0,0 @@
git.kundeng.us/phoenix/textsender-models v0.0.6-4-e124bcfda8-556 h1:2shgOKmglCODswpCJeCuyPiUAph3wfc0Ep1z6b/ifis=
git.kundeng.us/phoenix/textsender-models v0.0.6-4-e124bcfda8-556/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
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/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/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/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/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
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/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-129
View File
@@ -1,129 +0,0 @@
package config
import (
"flag"
"fmt"
"log"
"os"
"strconv"
"github.com/joho/godotenv"
"git.kundeng.us/phoenix/textsender-api/internal/version"
)
type Config struct {
DBConnString string
ServerPort string
ResetDB bool
JWTSecret string `env:"JWT_SECRET" required:"true"`
}
type ConnectionInfo struct {
Username string
Password string
Database string
Host string
Port int
SslMode string
}
const PORT = "8080"
const APP_NAME = "textsender-api"
func PortString() string {
portMessage := fmt.Sprintf(":%s", PORT)
return portMessage
}
func (ci ConnectionInfo) Parse() string {
return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", ci.Username, ci.Password, ci.Host, ci.Port, ci.Database, ci.SslMode)
}
func PrintName() {
fmt.Println(APP_NAME)
fmt.Println(version.String())
}
func Load() *Config {
versionFlag := flag.Bool("version", false, "Print version information")
resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit")
port := flag.String("port", PORT, "Server port")
flag.Parse()
if *versionFlag {
fmt.Println(version.String())
os.Exit(-1)
}
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
unpackedConnString := UnpackDBConnString()
dbConnString := unpackedConnString.Parse()
return &Config{
DBConnString: dbConnString,
ServerPort: *port,
ResetDB: *resetDb,
JWTSecret: os.Getenv("JWT_SECRET"),
}
}
func UnpackDBConnString() (connInfo ConnectionInfo) {
username := os.Getenv("DB_USER")
password := os.Getenv("DB_PASSWORD")
host := os.Getenv("DB_HOST")
port := os.Getenv("DB_PORT")
database := os.Getenv("DB_NAME")
sslMode := os.Getenv("DB_SSLMODE")
if username != "" {
connInfo.Username = username
} else {
connInfo.Username = "user"
}
if password != "" {
connInfo.Password = password
} else {
connInfo.Password = "password"
}
if host != "" {
connInfo.Host = host
} else {
connInfo.Host = "localhost"
}
if port != "" {
num, err := strconv.Atoi(port)
if err != nil {
return
}
connInfo.Port = num
} else {
connInfo.Port = 5432
}
if database != "" {
connInfo.Database = database
} else {
connInfo.Database = "textsender_db"
}
if sslMode != "" {
connInfo.SslMode = sslMode
} else {
connInfo.SslMode = "disable"
}
return
}
func (c *Config) GetDBConnString() string {
return c.DBConnString
}
-136
View File
@@ -1,136 +0,0 @@
// db/connection.go
package db
import (
"context"
"fmt"
"log"
"os"
"path"
"strings"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
var Pool *pgxpool.Pool
type Database struct {
Pool *pgxpool.Pool
}
func NewDatabase(connString string) (*Database, error) {
ctx := context.Background()
// Parse the connection string and create pool configuration
config, err := pgxpool.ParseConfig(connString)
if err != nil {
return nil, fmt.Errorf("unable to parse DSN: %v", err)
}
// Configure connection pool settings
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = time.Hour
config.MaxConnIdleTime = 30 * time.Minute
config.HealthCheckPeriod = time.Minute
// Configure connection timeouts
config.ConnConfig.ConnectTimeout = 10 * time.Second
config.ConnConfig.RuntimeParams["timezone"] = "UTC"
// Create the connection pool
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("unable to create connection pool: %v", err)
}
// Test the connection with a short timeout
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("unable to ping database: %v", err)
}
log.Printf("Successfully connected to database")
return &Database{Pool: pool}, nil
}
func TableExists(ctx context.Context, conn *pgxpool.Pool, tableName string) (bool, error) {
var exists bool
query := `
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = $1
);
`
err := conn.QueryRow(ctx, query, tableName).Scan(&exists)
if err != nil {
return false, fmt.Errorf("error checking if table exists: %w", err)
}
return exists, nil
}
func (db *Database) Close() {
if db.Pool != nil {
db.Pool.Close()
log.Println("Database connection pool closed")
}
}
// HealthCheck verifies the database connection is still alive
func (db *Database) HealthCheck(ctx context.Context) error {
return db.Pool.Ping(ctx)
}
// GetPoolStats returns connection pool statistics
func (db *Database) GetPoolStats() string {
stats := db.Pool.Stat()
return fmt.Sprintf("TotalConns: %d, IdleConns: %d, AcquiredConns: %d",
stats.TotalConns(), stats.IdleConns(), stats.AcquiredConns())
}
func (db *Database) ResetDatabase(ctx context.Context) error {
tx, err := db.Pool.Begin(ctx)
if err != nil {
return fmt.Errorf("Transaction unable to begin: %v", err)
}
defer tx.Rollback(ctx)
schemaContent, err := os.ReadFile("migrations/schema.sql")
if err != nil {
log.Println("Default migrations not found. Checking different directory")
cwd, _ := os.Getwd()
migrationsPath := path.Join(cwd, "../..", "migrations/schema.sql")
schemaContent, err = os.ReadFile(migrationsPath)
if err != nil {
return fmt.Errorf("error reading schema file: %v", err)
}
}
for _, stmt := range strings.Split(string(schemaContent), ";") {
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
if strings.HasPrefix(stmt, "--") {
continue
}
_, err := tx.Exec(ctx, stmt)
if err != nil {
return fmt.Errorf("error executing SQL statement: %v\nStatement: %s", err, stmt)
}
}
if err := tx.Commit(ctx); err != nil {
return fmt.Errorf("unable to commit transaction: %v", err)
}
log.Println("Database schema applied successfully")
return nil
}
-154
View File
@@ -1,154 +0,0 @@
package handler
import (
"fmt"
"net/http"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/store"
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
)
type RequestAddContact struct {
PhoneNumber string `json:"phone_number"`
UserId uuid.UUID `json:"user_id"`
}
type AddContactResponse struct {
Message string `json:"message"`
Data []contact.Contact `json:"data"`
}
type GetContactResponse struct {
Message string `json:"message"`
Data []contact.Contact `json:"data"`
}
type ContactHandler struct {
ContactStore store.ContactStore
}
func NewContactHandler(str store.ContactStore) *ContactHandler {
return &ContactHandler{ContactStore: str}
}
func (c *ContactHandler) AddContact(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req RequestAddContact
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
newContact := contact.Contact{PhoneNumber: req.PhoneNumber, UserId: req.UserId}
var statusCode int
var resp AddContactResponse
ctx := r.Context()
if exists, err := c.ContactStore.ContactExists(ctx, newContact.PhoneNumber, newContact.UserId); err != nil {
fmt.Printf("Error: %v", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if exists {
statusCode = http.StatusBadRequest
resp.Message = "Cannot create contact"
} else {
if err := c.ContactStore.CreateContact(ctx, &newContact); err != nil {
fmt.Printf("Error: %v", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusCreated
resp.Message = "Contact created"
resp.Data = append(resp.Data, newContact)
}
}
}
RespondWithJSON(w, statusCode, &resp)
}
func (c *ContactHandler) GetContact(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
fmt.Println("Method:", r.Method)
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var id, userId uuid.UUID
queryParams := r.URL.Query()
// Check if parameter exists
if _, exists := queryParams["id"]; !exists {
if _, exists := queryParams["user_id"]; !exists {
fmt.Fprintf(w, "Name parameter not provided")
http.Error(w, "Query params", http.StatusBadRequest)
return
} else {
var err error
userId, err = uuid.Parse(queryParams.Get("user_id"))
if err != nil {
fmt.Fprintf(w, "Name parameter exists: %s", userId)
}
}
} else {
var err error
idTmp := queryParams.Get("id")
id, err = uuid.Parse(idTmp)
if err != nil {
fmt.Println("Error:", err)
http.Error(w, "Error parsing Id", http.StatusBadRequest)
return
}
}
var statusCode int
var resp GetContactResponse
ctx := r.Context()
if id != uuid.Nil {
fmt.Println("Checking with Id")
if con, err := c.ContactStore.GetContactByID(ctx, id); err == nil {
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, *con)
} else {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
}
} else if userId != uuid.Nil {
fmt.Println("Checking with User Id")
if contacts, err := c.ContactStore.GetAllContacts(ctx); err == nil {
for _, con := range contacts {
if con.UserId == userId {
resp.Data = append(resp.Data, *con)
}
}
if len(resp.Data) > 0 {
statusCode = http.StatusOK
resp.Message = "Successful"
} else {
statusCode = http.StatusNotFound
resp.Message = "Contact not found"
}
} else {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
}
} else {
statusCode = http.StatusBadRequest
resp.Message = "Invalid query parameter"
}
RespondWithJSON(w, statusCode, &resp)
}
-85
View File
@@ -1,85 +0,0 @@
package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"git.kundeng.us/phoenix/textsender-api/internal/db"
"git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint"
"git.kundeng.us/phoenix/textsender-api/internal/store/mock"
)
type Request struct {
PhoneNumber string
UserId uuid.UUID
}
func TestCreateContactWithMock(t *testing.T) {
mockstore := mock.NewMockContactStore()
handler := NewContactHandler(mockstore)
testUserId := uuid.New()
testBody := Request{PhoneNumber: "+12335403383", UserId: testUserId}
jsonValue, _ := json.Marshal(testBody)
req, _ := http.NewRequest("POST", endpoint.ADD_CONTACT_ENDPOINT, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.AddContact(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response AddContactResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error Creating contact %v", err)
assert.NotEmpty(t, response.Data, "No Contact created")
assert.NotNil(t, response.Data[0].Id, "Id should not be nil")
}
func TestGetContactWithMock(t *testing.T) {
mockstore := mock.NewMockContactStore()
testUserId := uuid.New()
testCon := contact.Contact{PhoneNumber: "+12335403383", UserId: testUserId}
ctx := t.Context()
if err := mockstore.CreateContact(ctx, &testCon); err != nil {
assert.NoError(t, err, "Error creating contact")
return
}
url := fmt.Sprintf("%s?user_id=%s", endpoint.GET_CONTACT, testCon.UserId)
req, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder()
contactHandler := NewContactHandler(mockstore)
contactHandler.GetContact(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response GetContactResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error getting contact %v", err)
assert.NotEmpty(t, response.Data, "No Contact retrieved")
assert.NotNil(t, response.Data[0].Id, "Id should not be nil")
}
func resetTestDB(t *testing.T) {
t.Helper()
_, err := db.Pool.Exec(context.Background(), "DELETE FROM contacts")
if err != nil {
t.Fatalf("Failed to reset test database: %v", err)
}
}
-14
View File
@@ -1,14 +0,0 @@
package handler
import "net/http"
import "git.kundeng.us/phoenix/textsender-api/internal/model"
func DraftMessageHandler(w http.ResponseWriter, r *http.Request) {
da := model.MessageItem{
Id: 1,
Message: "The Word",
}
RespondWithJSON(w, http.StatusOK, da)
}
-14
View File
@@ -1,14 +0,0 @@
package endpoint
const MESSAGE_DRAFT_ENDPOINT = "/api/v1/message/draft"
const ADD_MESSAGE = "/api/v1/message/new"
const GET_MESSAGE = "/api/v1/message"
const GET_CONTACT = "/api/v1/contact"
const ADD_CONTACT_ENDPOINT = "/api/v1/contact/new"
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"
-41
View File
@@ -1,41 +0,0 @@
package handler
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
func ExtractFromRequest(r *http.Request, reqItem interface{}) error {
err := json.NewDecoder(r.Body).Decode(&reqItem)
if err != nil {
return err
} else {
return nil
}
}
// Helper function to send JSON responses
func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(data); err != nil {
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)
}
}
-141
View File
@@ -1,141 +0,0 @@
package handler
import (
"fmt"
"net/http"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/store"
"git.kundeng.us/phoenix/textsender-models/pkg/message"
)
type RequestAddMessage struct {
Content string `json:"content"`
UserId uuid.UUID `json:"user_id"`
}
type AddMessageResponse struct {
Message string `json:"message"`
Data []message.Message `json:"data"`
}
type GetMessageResponse struct {
Message string `json:"message"`
Data []message.Message `json:"data"`
}
type MessageHandler struct {
MessageStore store.MessageStore
}
func NewMessageHandler(str store.MessageStore) *MessageHandler {
return &MessageHandler{MessageStore: str}
}
func (m *MessageHandler) AddMessage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req RequestAddMessage
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 AddMessageResponse
newMessage := message.Message{Content: req.Content, UserId: req.UserId}
ctx := r.Context()
if err := m.MessageStore.CreateMessage(ctx, &newMessage); err != nil {
fmt.Println("Error:", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
statusCode = http.StatusCreated
resp.Message = "Message created"
resp.Data = append(resp.Data, newMessage)
}
RespondWithJSON(w, statusCode, &resp)
}
func (c *MessageHandler) GetMessage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var id, userId uuid.UUID
queryParams := r.URL.Query()
// Check if parameter exists
if _, exists := queryParams["id"]; !exists {
if _, exists := queryParams["user_id"]; !exists {
fmt.Fprintf(w, "Name parameter not provided")
http.Error(w, "Query params", http.StatusBadRequest)
return
} else {
var err error
userId, err = uuid.Parse(queryParams.Get("user_id"))
if err != nil {
fmt.Fprintf(w, "Name parameter exists: %s", userId)
}
}
} else {
var err error
idTmp := queryParams.Get("id")
id, err = uuid.Parse(idTmp)
if err != nil {
fmt.Println("Error:", err)
http.Error(w, "Error parsing Id", http.StatusBadRequest)
return
}
}
var statusCode int
var resp GetMessageResponse
ctx := r.Context()
if id != uuid.Nil {
fmt.Println("Checking with Id")
if con, err := c.MessageStore.GetMessageByID(ctx, id); err == nil {
statusCode = http.StatusOK
resp.Message = "Successful"
resp.Data = append(resp.Data, *con)
} else {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
}
} else if userId != uuid.Nil {
fmt.Println("Checking with User Id")
if contacts, err := c.MessageStore.GetAllMessages(ctx); err == nil {
for _, con := range contacts {
if con.UserId == userId {
resp.Data = append(resp.Data, *con)
}
}
if len(resp.Data) > 0 {
statusCode = http.StatusOK
resp.Message = "Successful"
} else {
statusCode = http.StatusNotFound
resp.Message = "Contact not found"
}
} else {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
}
} else {
statusCode = http.StatusBadRequest
resp.Message = "Invalid query parameter"
}
RespondWithJSON(w, statusCode, &resp)
}
-75
View File
@@ -1,75 +0,0 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"git.kundeng.us/phoenix/textsender-models/pkg/message"
"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"
)
type CreateMessageRequest struct {
Content string `json:"content"`
UserId uuid.UUID `json:"user_id"`
}
func TestCreateMessageWithMock(t *testing.T) {
mockStore := mock.NewMockMessageStore()
handler := NewMessageHandler(mockStore)
testUserId := uuid.New()
testBody := CreateMessageRequest{Content: "Who could tell?", UserId: testUserId}
jsonValue, _ := json.Marshal(testBody)
req, _ := http.NewRequest("POST", endpoint.ADD_MESSAGE, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.AddMessage(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response AddMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error Creating message %v", err)
assert.NotEmpty(t, response.Data, "No Message created")
assert.NotNil(t, response.Data[0].Id, "Id should not be nil")
}
func TestGetMessageWithMock(t *testing.T) {
mockstore := mock.NewMockMessageStore()
testUserId := uuid.New()
testCon := message.Message{Content: "Who is the one that benefits?", UserId: testUserId}
ctx := t.Context()
if err := mockstore.CreateMessage(ctx, &testCon); err != nil {
assert.NoError(t, err, "Error creating message")
return
}
url := fmt.Sprintf("%s?user_id=%s", endpoint.GET_MESSAGE, testCon.UserId)
req, _ := http.NewRequest("GET", url, nil)
rr := httptest.NewRecorder()
messageHandler := NewMessageHandler(mockstore)
messageHandler.GetMessage(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
var response GetMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error getting message %v", err)
assert.NotEmpty(t, response.Data, "No Message retrieved")
assert.NotNil(t, response.Data[0].Id, "Id should not be nil")
}
-154
View File
@@ -1,154 +0,0 @@
package handler
import (
"net/http"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/store"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
)
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 {
ScheduledMessageEventStore store.ScheduledMessageEventStore
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageStatusHandler(str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageStatusHandler {
return &ScheduledMessageStatusHandler{ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
}
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}
}
@@ -1,73 +0,0 @@
package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.kundeng.us/phoenix/textsender-models/pkg/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()
handler := NewScheduledMessageStatusHandler(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")
}
-205
View File
@@ -1,205 +0,0 @@
package handler
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/store"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
)
type RequestAddScheduledMessage struct {
Scheduled time.Time `json:"scheduled"`
Created time.Time `json:"created"`
Status string `json:"status"`
UserId uuid.UUID `json:"user_id"`
}
type AddScheduledMessageResponse struct {
Message string `json:"message"`
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 {
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageHandler(str store.ScheduledMessageStore) *ScheduledMessageHandler {
return &ScheduledMessageHandler{ScheduledMessageStore: str}
}
func (s *ScheduledMessageHandler) AddScheduledMessage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req RequestAddScheduledMessage
if err := ExtractFromRequest(r, &req); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest)
}
defer r.Body.Close()
scheduledMessage := scheduling.ScheduledMessage{Scheduled: req.Scheduled, Status: req.Status, UserId: req.UserId}
var statusCode int
var resp AddScheduledMessageResponse
if scheduledMessage.UserId == uuid.Nil {
statusCode = http.StatusBadRequest
resp.Message = "No UserId"
} else {
ctx := r.Context()
if validStatus, err := IsStatusValid(&scheduledMessage); err == nil {
if validStatus {
if valid, err := isScheduledTimeValid(&scheduledMessage); err == nil && valid {
if err = s.ScheduledMessageStore.CreateScheduledMessage(ctx, &scheduledMessage); err == nil {
statusCode = http.StatusCreated
resp.Data = append(resp.Data, scheduledMessage)
resp.Message = "Successful"
} else {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
}
} else {
statusCode = http.StatusBadRequest
resp.Message = "Invalid scheduled time"
}
} else {
statusCode = http.StatusBadRequest
resp.Message = "Invalid status"
}
} else {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
}
}
RespondWithJSON(w, statusCode, &resp)
}
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)
}
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) {
now := time.Now()
timeCutOff := now.Add(-5 * time.Minute)
futureCutOff := now.Add((24 * 7) * time.Hour)
if schMsg.Scheduled.After(timeCutOff) {
if schMsg.Scheduled.Before(futureCutOff) {
return true, nil
} else {
return false, fmt.Errorf("Scheduled Time is too far in the future")
}
} else {
return false, nil
}
}
func IsStatusValid(schMsg *scheduling.ScheduledMessage) (bool, error) {
schMsg.Status = strings.ToUpper(schMsg.Status)
if schMsg.Status == scheduling.Pending {
return true, nil
} else {
return false, fmt.Errorf("Status is not valid for creating scheduled message %s", schMsg.Status)
}
}
-208
View File
@@ -1,208 +0,0 @@
package handler
import (
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-api/internal/store"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
)
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 {
ScheduledMessageEventStore store.ScheduledMessageEventStore
ScheduledMessageStore store.ScheduledMessageStore
}
func NewScheduledMessageEventHandler(str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageEventHandler {
return &ScheduledMessageEventHandler{ScheduledMessageEventStore: str, ScheduledMessageStore: schStore}
}
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)
}
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)
}
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)
}
@@ -1,206 +0,0 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"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"
"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()
handler := NewScheduledMessageEventHandler(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()
handler := NewScheduledMessageEventHandler(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()
handler := NewScheduledMessageEventHandler(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}
}
-135
View File
@@ -1,135 +0,0 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.kundeng.us/phoenix/textsender-models/pkg/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"
)
type CreateScheduledMessageRequest struct {
Scheduled time.Time `json:"scheduled"`
Status string `json:"status"`
UserId uuid.UUID `json:"user_id"`
}
func TestCreateScheduledMessageWithMock(t *testing.T) {
now := time.Now()
mockStore := mock.NewMockScheduledMessageStore()
handler := NewScheduledMessageHandler(mockStore)
testUserId := uuid.New()
testBody := testCreateScheduledMessageRequest(testUserId, now)
jsonValue, _ := json.Marshal(testBody)
req, _ := http.NewRequest("POST", endpoint.ScheduleMessageEndpoint, strings.NewReader(string(jsonValue)))
rr := httptest.NewRecorder()
handler.AddScheduledMessage(rr, req)
assert.Equal(t, http.StatusCreated, rr.Code)
var response AddScheduledMessageResponse
err := json.Unmarshal(rr.Body.Bytes(), &response)
assert.NoError(t, err, "Error Creating message %v", err)
assert.NotEmpty(t, response.Data, "No Message created")
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()
handler := NewScheduledMessageHandler(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()
handler := NewScheduledMessageHandler(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}
}
-47
View File
@@ -1,47 +0,0 @@
package middleware
import (
"context"
"net/http"
"strings"
"git.kundeng.us/phoenix/textsender-api/internal/services"
)
type contextKey string
const (
UserContextKey contextKey = "user"
)
func AuthMiddleware(authService *services.JWTService) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// Extract token from "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
return
}
token := parts[1]
// Validate token with auth service
user, err := authService.ValidateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add user to context
ctx := context.WithValue(r.Context(), UserContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
-22
View File
@@ -1,22 +0,0 @@
package middleware
import (
"log"
"net/http"
"time"
)
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
func JSONContentType(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
next.ServeHTTP(w, r)
})
}
-6
View File
@@ -1,6 +0,0 @@
package model
type MessageItem struct {
Id int `json:"id"`
Message string `json:"message"`
}
-50
View File
@@ -1,50 +0,0 @@
package services
import (
"time"
"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 {
secretKey []byte
}
func NewJWTService(secretKey string) *JWTService {
return &JWTService{
secretKey: []byte(secretKey),
}
}
func (s *JWTService) ValidateToken(tokenString string) (*txtmodels_user.User, error) {
// TODO: Include more user information in the claims to populate user
claims := &txtmodels_token.Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
// Validate the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return s.secretKey, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, jwt.ErrSignatureInvalid
}
// Check token expiration
if time.Now().After(claims.ExpiresAt.Time) {
return nil, jwt.ErrTokenExpired
}
return &txtmodels_user.User{
Id: claims.UserId,
}, nil
}
-115
View File
@@ -1,115 +0,0 @@
package store
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
)
type ContactStore interface {
CreateContact(ctx context.Context, con *contact.Contact) error
GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error)
GetContactByPhone(ctx context.Context, phone string, userId uuid.UUID) (*contact.Contact, error)
GetAllContacts(ctx context.Context) ([]*contact.Contact, error)
ContactExists(ctx context.Context, phone string, userId uuid.UUID) (bool, error)
}
type PGContactStore struct {
db *pgxpool.Pool
}
func NewContactStore(db *pgxpool.Pool) *PGContactStore {
return &PGContactStore{db: db}
}
func (s *PGContactStore) CreateContact(ctx context.Context, con *contact.Contact) error {
query := `
INSERT INTO contacts (phone_number, user_id)
VALUES ($1, $2)
RETURNING id, phone_number, user_id
`
return s.db.QueryRow(ctx, query, con.PhoneNumber, con.UserId).Scan(
&con.Id, &con.PhoneNumber, &con.UserId,
)
}
func (s *PGContactStore) GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error) {
query := `SELECT id, phone_number, user_id FROM contacts WHERE id = $1`
var con contact.Contact
err := s.db.QueryRow(ctx, query, id).Scan(
&con.Id, &con.PhoneNumber, &con.UserId,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("getting contact by ID: %w", err)
}
return &con, nil
}
func (s *PGContactStore) GetContactByPhone(ctx context.Context, phone string, userId uuid.UUID) (*contact.Contact, error) {
query := `SELECT id, phone_number, user_id FROM contacts WHERE phone_number = $1 AND user_id = $2`
var con contact.Contact
err := s.db.QueryRow(ctx, query, phone, userId).Scan(
&con.Id, &con.PhoneNumber, &con.UserId,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("getting contact by phone and User ID: %w", err)
}
return &con, nil
}
func (s *PGContactStore) GetAllContacts(ctx context.Context) ([]*contact.Contact, error) {
query := `SELECT id, phone_number, user_id FROM contacts`
rows, err := s.db.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("querying all contacts: %w", err)
}
defer rows.Close()
var cons []*contact.Contact
for rows.Next() {
var con contact.Contact
if err := rows.Scan(
&con.Id, &con.PhoneNumber, &con.UserId,
); err != nil {
return nil, fmt.Errorf("scanning contact row: %w", err)
}
cons = append(cons, &con)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating contact rows: %w", err)
}
return cons, nil
}
func (s *PGContactStore) ContactExists(ctx context.Context, phone string, userId uuid.UUID) (bool, error) {
query := `SELECT EXISTS(SELECT 1 FROM contacts WHERE phone_number = $1 AND user_id = $2)`
var exists bool
err := s.db.QueryRow(ctx, query, phone, userId).Scan(&exists)
if err != nil {
return false, fmt.Errorf("checking if contact exists: %w", err)
}
return exists, nil
}
-96
View File
@@ -1,96 +0,0 @@
package store
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.kundeng.us/phoenix/textsender-models/pkg/message"
)
type MessageStore interface {
GetMessageByID(ctx context.Context, id uuid.UUID) (*message.Message, error)
CreateMessage(ctx context.Context, msg *message.Message) error
GetAllMessages(ctx context.Context) ([]*message.Message, error)
MessageExists(ctx context.Context, msg *message.Message) (bool, error)
}
type PGMessageStore struct {
db *pgxpool.Pool
}
func NewMessageStore(db *pgxpool.Pool) *PGMessageStore {
return &PGMessageStore{db: db}
}
func (m *PGMessageStore) CreateMessage(ctx context.Context, msg *message.Message) error {
query := `
INSERT INTO messages (content, user_id)
VALUES ($1, $2)
RETURNING id
`
return m.db.QueryRow(ctx, query, msg.Content, msg.UserId).Scan(
&msg.Id,
)
}
func (m *PGMessageStore) MessageExists(ctx context.Context, msg *message.Message) (bool, error) {
query := `SELECT EXISTS(SELECT 1 FROM messages WHERE content = $1 AND user_id = $2)`
var exists bool
err := m.db.QueryRow(ctx, query, msg.Content, msg.UserId).Scan(&exists)
if err != nil {
return false, fmt.Errorf("checking if message exists: %w", err)
} else {
return exists, nil
}
}
func (m *PGMessageStore) GetAllMessages(ctx context.Context) ([]*message.Message, error) {
query := `SELECT id, content, user_id FROM messages`
rows, err := m.db.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("querying all messages: %w", err)
}
defer rows.Close()
var messages []*message.Message
for rows.Next() {
var msg message.Message
if err := rows.Scan(
&msg.Id, &msg.Content, &msg.UserId,
); err != nil {
return nil, fmt.Errorf("scanning message row: %w", err)
}
messages = append(messages, &msg)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating message rows: %w", err)
}
return messages, nil
}
func (m *PGMessageStore) GetMessageByID(ctx context.Context, id uuid.UUID) (*message.Message, error) {
query := `SELECT id, content, user_id FROM messages WHERE id = $1`
var msg message.Message
err := m.db.QueryRow(ctx, query, id).Scan(
&msg.Id, &msg.Content, &msg.UserId,
)
if err == pgx.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("getting message by ID: %w", err)
}
return &msg, nil
}
-202
View File
@@ -1,202 +0,0 @@
package mock
import (
"context"
"errors"
"sync"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-models/pkg/contact"
"git.kundeng.us/phoenix/textsender-models/pkg/message"
)
type Key struct {
PhoneNumber string
UserId uuid.UUID
}
type MockContactStore struct {
Contacts map[uuid.UUID]*contact.Contact
ContactsByKey map[Key]*contact.Contact
mu sync.RWMutex
Error error // Optional: simulate errors
}
func NewMockContactStore() *MockContactStore {
return &MockContactStore{
Contacts: make(map[uuid.UUID]*contact.Contact),
ContactsByKey: make(map[Key]*contact.Contact),
}
}
func (m *MockContactStore) CreateContact(ctx context.Context, con *contact.Contact) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if con.Id == uuid.Nil {
con.Id = uuid.New()
}
key := Key{PhoneNumber: con.PhoneNumber, UserId: con.UserId}
if _, exists := m.ContactsByKey[key]; exists {
return errors.New("Contact with phone number already exists")
}
m.Contacts[con.Id] = con
m.ContactsByKey[key] = con
return nil
}
func (m *MockContactStore) GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
if m.Error != nil {
return nil, m.Error
}
con, exists := m.Contacts[id]
if !exists {
return nil, errors.New("Contact not found")
}
return con, nil
}
func (m *MockContactStore) GetContactByPhone(ctx context.Context, phoneNumber string, userId uuid.UUID) (*contact.Contact, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
con, exists := m.ContactsByKey[Key{PhoneNumber: phoneNumber, UserId: userId}]
if !exists {
return nil, errors.New("Contact not found")
}
return con, nil
}
func (m *MockContactStore) GetAllContacts(ctx context.Context) ([]*contact.Contact, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
cons := make([]*contact.Contact, 0, len(m.Contacts))
for _, con := range m.Contacts {
cons = append(cons, con)
}
return cons, nil
}
func (m *MockContactStore) ContactExists(ctx context.Context, phoneNumber string, userId uuid.UUID) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return false, m.Error
}
_, exists := m.ContactsByKey[Key{PhoneNumber: phoneNumber, UserId: userId}]
return exists, nil
}
type MessageKey struct {
Content string
UserId uuid.UUID
}
type MockMessageStore struct {
Messages map[uuid.UUID]*message.Message
MessagesByKey map[MessageKey]*message.Message
mu sync.RWMutex
Error error // Optional: simulate errors
}
func NewMockMessageStore() *MockMessageStore {
return &MockMessageStore{
Messages: make(map[uuid.UUID]*message.Message),
MessagesByKey: make(map[MessageKey]*message.Message),
}
}
func (m *MockMessageStore) CreateMessage(ctx context.Context, msg *message.Message) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return m.Error
}
if msg.Id == uuid.Nil {
msg.Id = uuid.New()
}
key := MessageKey{Content: msg.Content, UserId: msg.UserId}
if _, exists := m.MessagesByKey[key]; exists {
return errors.New("Message already exists")
}
m.Messages[msg.Id] = msg
m.MessagesByKey[key] = msg
return nil
}
func (m *MockMessageStore) GetAllMessages(ctx context.Context) ([]*message.Message, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
var msgs []*message.Message
for _, msg := range m.Messages {
msgs = append(msgs, msg)
}
return msgs, nil
}
func (m *MockMessageStore) GetMessageByID(ctx context.Context, id uuid.UUID) (*message.Message, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return nil, m.Error
}
msg := m.Messages[id]
return msg, nil
}
func (m *MockMessageStore) MessageExists(ctx context.Context, msg *message.Message) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.Error != nil {
return false, m.Error
}
_, exists := m.MessagesByKey[MessageKey{Content: msg.Content, UserId: msg.UserId}]
return exists, nil
}
@@ -1,160 +0,0 @@
package mock
import (
"context"
"fmt"
"sync"
"time"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
)
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
}
@@ -1,142 +0,0 @@
package mock
import (
"context"
"fmt"
"sync"
"github.com/google/uuid"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
)
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,114 +0,0 @@
package store
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
)
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
}
}
-128
View File
@@ -1,128 +0,0 @@
package store
import (
"context"
"fmt"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"git.kundeng.us/phoenix/textsender-models/pkg/message/scheduling"
)
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
UpdateStatus(ctx context.Context, id uuid.UUID, updatedStatus string) (*string, error)
}
type PGScheduledMessageStore struct {
db *pgxpool.Pool
}
func NewScheduledMessageStore(db *pgxpool.Pool) *PGScheduledMessageStore {
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 {
query := `
INSERT INTO scheduled_messages (scheduled, status, user_id)
VALUES ($1, $2, $3)
RETURNING id, created
`
return s.db.QueryRow(ctx, query, schedMsg.Scheduled, schedMsg.Status, schedMsg.UserId).Scan(
&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
}
}
-17
View File
@@ -1,17 +0,0 @@
package version
import "fmt"
var (
Version = "dev"
BuildTime = "none"
Commit = "unknown"
GoVersion = "unknown"
)
func String() string {
return fmt.Sprintf(
"Version: %s\nBuild Date: %s\nCommit: %s\nGo Version: %s",
Version, BuildTime, Commit, GoVersion,
)
}
+45
View File
@@ -0,0 +1,45 @@
-- Add migration script here
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS "contacts" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
phone_number TEXT NOT NULL,
user_id UUID NOT NULL,
firstname TEXT NULL,
lastname TEXT NULL,
nickname TEXT NULL
);
CREATE TABLE IF NOT EXISTS "messages" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
user_id UUID NOT NULL
);
CREATE TABLE IF NOT EXISTS "scheduled_messages" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scheduled timestamptz NOT NULL,
created timestamptz DEFAULT now(),
status TEXT CHECK (status IN ('PENDING', 'READY', 'PROCESSING', 'DONE')),
user_id UUID NOT NULL
);
CREATE TABLE IF NOT EXISTS "scheduled_message_events" (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contact_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 gen_random_uuid(),
scheduled_message_event_id UUID NULL,
response JSONB NOT NULL,
user_id UUID NOT NULL,
sent timestamptz NOT NULL,
contact_id UUID NULL,
message_id UUID NULL,
status TEXT CHECK (status IN ('INSTANT', 'SCHEDULED'))
);
-31
View File
@@ -1,31 +0,0 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
DROP TABLE IF EXISTS contacts CASCADE;
CREATE TABLE IF NOT EXISTS contacts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phone_number TEXT NOT NULL,
user_id UUID NOT NULL
);
CREATE TABLE IF NOT EXISTS messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content TEXT NOT NULL,
user_id UUID NOT NULL
);
CREATE TABLE IF NOT EXISTS scheduled_messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
scheduled timestamptz NOT NULL,
created timestamptz DEFAULT now(),
status TEXT CHECK (status IN ('PENDING', 'READY', 'PROCESSING', 'DONE')),
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()
)
+59
View File
@@ -0,0 +1,59 @@
use axum::{
Json,
http::{Request, StatusCode},
middleware::Next,
response::IntoResponse,
};
use axum_extra::extract::cookie::CookieJar;
use jsonwebtoken::{DecodingKey, Validation, decode};
#[derive(Debug, serde::Serialize)]
pub struct ErrorResponse {
pub status: &'static str,
pub message: String,
}
pub async fn auth<B>(
cookie_jar: CookieJar,
req: Request<axum::body::Body>,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
let token = cookie_jar
.get("token")
.map(|cookie| cookie.value().to_string())
.or_else(|| {
req.headers()
.get(axum::http::header::AUTHORIZATION)
.and_then(|auth_header| auth_header.to_str().ok())
.and_then(|auth_value| auth_value.strip_prefix("Bearer ").map(String::from))
});
let token = token.ok_or_else(|| {
let json_error = ErrorResponse {
status: "fail",
message: "You are not logged in, please provide token".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?;
let secret_key = schedtxt_models::envy::environment::get_secret_main_key().value;
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_audience(&["schedtxt"]); // Must match exactly what's in the token
let _claims = decode::<schedtxt_models::token::Claims>(
&token,
&DecodingKey::from_secret(secret_key.as_ref()),
&validation,
)
.map_err(|err| {
eprintln!("Error: {err:?}");
let json_error = ErrorResponse {
status: "fail",
message: "Invalid token - Error decoding claims".to_string(),
};
(StatusCode::UNAUTHORIZED, Json(json_error))
})?
.claims;
Ok(next.run(req).await)
}
+302
View File
@@ -0,0 +1,302 @@
pub mod request {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct AddContactRequest {
pub phone_number: String,
pub firstname: Option<String>,
pub lastname: Option<String>,
pub nickname: Option<String>,
pub user_id: uuid::Uuid,
}
impl AddContactRequest {
pub fn is_valid(&self) -> bool {
!self.phone_number.is_empty() || !self.user_id.is_nil()
}
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct GetContactParams {
pub id: Option<uuid::Uuid>,
pub user_id: Option<uuid::Uuid>,
}
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct UpdateContactNamesRequest {
pub id: uuid::Uuid,
pub firstname: Option<String>,
pub lastname: Option<String>,
pub nickname: Option<String>,
}
impl UpdateContactNamesRequest {
pub fn is_valid(&self) -> bool {
if self.id.is_nil() {
false
} else {
self.firstname.is_some() || self.lastname.is_some() || self.nickname.is_some()
}
}
}
}
pub mod response {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct AddContactResponse {
pub message: String,
pub data: Vec<schedtxt_models::contact::Contact>,
}
pub use AddContactResponse as GetContactResponse;
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct UpdateContactNamesResponse {
pub message: String,
pub data: Vec<UpdateContactNameChange>,
}
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct UpdateContactNameChange {
pub old_firstname: Option<String>,
pub old_lastname: Option<String>,
pub old_nickname: Option<String>,
pub new_firstname: Option<String>,
pub new_lastname: Option<String>,
pub new_nickname: Option<String>,
}
}
pub mod endpoint {
use crate::repo::contact as contact_repo;
/// Endpoint to create Contact
#[utoipa::path(
post,
path = super::super::endpoints::ADD_CONTACT,
request_body(
content = super::request::AddContactRequest,
description = "Data needed to create a Contact",
content_type = "application/json"
),
responses(
(status = 201, description = "Contact created", body = super::response::AddContactResponse),
(status = 400, description = "Error", body = super::response::AddContactResponse),
(status = 500, description = "Error creating Contact", body = super::response::AddContactResponse)
)
)]
pub async fn create_contact(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::AddContactRequest>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::AddContactResponse>,
) {
let mut response = super::response::AddContactResponse::default();
if payload.is_valid() {
match contact_repo::get_with_user_id_and_phone(
&pool,
&payload.user_id,
&payload.phone_number,
)
.await
{
Ok(_) => {
println!("Already created");
response.message = String::from("Already created");
(axum::http::StatusCode::NOT_MODIFIED, axum::Json(response))
}
Err(err) => {
match err {
sqlx::Error::RowNotFound => {
println!("Good to create");
// Put code here
let mut contact = schedtxt_models::contact::Contact {
firstname: payload.firstname,
lastname: payload.lastname,
phone_number: payload.phone_number,
nickname: payload.nickname,
user_id: Some(payload.user_id),
..Default::default()
};
match contact_repo::insert(&pool, &contact).await {
Ok(id) => {
contact.id = Some(id);
response.message =
String::from(super::super::response::SUCCESSFUL);
response.data.push(contact);
(axum::http::StatusCode::CREATED, axum::Json(response))
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Contact not created");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
}
_ => {
eprintln!("Error: {err:?}");
response.message = String::from("Something went wrong");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
}
}
} else {
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
/// Endpoint to get Contacts
#[utoipa::path(
get,
path = super::super::endpoints::GET_CONTACT,
params(
("id" = uuid::Uuid, Path, description = "Id of Contact"),
("user_id" = uuid::Uuid, Path, description = "User Id associated with the Contact")
),
responses(
(status = 200, description = "Songs found", body = super::response::GetContactResponse),
(status = 400, description = "Error getting songs", body = super::response::GetContactResponse)
)
)]
pub async fn get_contacts(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::GetContactParams>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::GetContactResponse>,
) {
let mut response = super::response::GetContactResponse::default();
let contacts = match params.id {
Some(id) => match contact_repo::get(&pool, &id).await {
Ok(contact) => {
vec![contact]
}
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
},
None => match params.user_id {
Some(user_id) => match contact_repo::get_with_user_id(&pool, &user_id).await {
Ok(contacts) => contacts,
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
},
None => {
response.message = String::from("Invalid parameter");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
},
};
response.data = contacts;
response.message = String::from(super::super::response::SUCCESSFUL);
(axum::http::StatusCode::OK, axum::Json(response))
}
/// Endpoint to update Contact
#[utoipa::path(
patch,
path = super::super::endpoints::UPDATE_CONTACT_NAME,
request_body(
content = super::request::UpdateContactNamesRequest,
description = "Data needed to update Contact names",
content_type = "application/json"
),
responses(
(status = 201, description = "Contact created", body = super::response::UpdateContactNamesResponse),
(status = 400, description = "Error", body = super::response::UpdateContactNamesResponse),
(status = 500, description = "Error creating Contact", body = super::response::UpdateContactNamesResponse)
)
)]
pub async fn update_contact_names(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::UpdateContactNamesRequest>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::UpdateContactNamesResponse>,
) {
let mut response = super::response::UpdateContactNamesResponse::default();
if payload.is_valid() {
match contact_repo::get(&pool, &payload.id).await {
Ok(contact) => {
let old_firstname: Option<String> = contact.firstname.clone();
let old_lastname: Option<String> = contact.lastname.clone();
let old_nickname: Option<String> = contact.nickname.clone();
let new_firstname = payload.firstname;
let new_lastname = payload.lastname;
let new_nickname = payload.nickname;
match contact_repo::update_names(
&pool,
&payload.id,
&new_firstname,
&new_lastname,
&new_nickname,
)
.await
{
Ok(()) => {
response.message = String::from(super::super::response::SUCCESSFUL);
let data = super::response::UpdateContactNameChange {
old_firstname,
old_lastname,
old_nickname,
new_firstname,
new_lastname,
new_nickname,
};
response.data = vec![data];
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Something went wrong");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Something went wrong");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
} else {
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
+176
View File
@@ -0,0 +1,176 @@
pub mod request {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct InstantMessageRequest {
pub contact_ids: Vec<uuid::Uuid>,
pub message_id: uuid::Uuid,
pub user_id: uuid::Uuid,
}
impl InstantMessageRequest {
pub fn is_valid(&self) -> bool {
!self.contact_ids.is_empty() || !self.message_id.is_nil() || !self.user_id.is_nil()
}
}
}
pub mod response {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct InstantMessageResponse {
pub message: String,
pub data: Vec<schedtxt_models::message::event::MessageEventResponse>,
}
}
pub mod endpoint {
use crate::repo::contact as contact_repo;
use crate::repo::message as message_repo;
use message_repo::event as event_repo;
/// Endpoint to create Message
#[utoipa::path(
post,
path = super::super::endpoints::ADD_MESSAGE,
request_body(
content = super::request::InstantMessageRequest,
description = "Data needed to create a Message",
content_type = "application/json"
),
responses(
(status = 201, description = "Message created", body = super::response::InstantMessageResponse),
(status = 400, description = "Error", body = super::response::InstantMessageResponse),
(status = 500, description = "Error creating Message", body = super::response::InstantMessageResponse)
)
)]
pub async fn send_message(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::InstantMessageRequest>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::InstantMessageResponse>,
) {
let mut response = super::response::InstantMessageResponse::default();
if payload.is_valid() {
let mut contacts: Vec<schedtxt_models::contact::Contact> = Vec::new();
for contact_id in &payload.contact_ids {
match contact_repo::get(&pool, contact_id).await {
Ok(contact) => {
contacts.push(contact);
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Invalid");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
}
}
if !contacts.is_empty() {
println!("Valid contacts");
match message_repo::get(&pool, &payload.message_id).await {
Ok(message) => {
println!("Valid message");
let t_config = match schedtxt_models::config::auxiliary::load_config() {
Ok(config) => config,
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Error getting config");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
};
let mut results: Vec<
schedtxt_models::message::event::MessageEventResponse,
> = Vec::new();
let pp = swoosh::twilio::types::Parameters {
schedule: false,
schedule_at: None,
};
let mut sendmsg = swoosh::SendMsg::default();
sendmsg.load_config(
&t_config.auth_token,
&t_config.phone_number,
&t_config.service_sid,
&t_config.account_sid,
);
for contact in &contacts {
sendmsg.load_message(&message.content);
sendmsg.load_recipient(&contact.phone_number);
match sendmsg.send_message(&pp).await {
Ok(result) => {
let val = swoosh::twilio::api::response_to_json(result).await;
let mer = schedtxt_models::message::event::MessageEventResponse {
user_id: payload.user_id,
// TODO: Make this more accurate
sent: Some(time::OffsetDateTime::now_utc()),
status: String::from(schedtxt_models::message::event::MESSAGE_EVENT_RESPONSE_STATUS_INSTANT),
contact_id: contact.id.unwrap(),
message_id: message.id.unwrap(),
response: val,
..Default::default()
};
results.push(mer);
}
Err(err) => {
eprintln!("Error: {err:?}");
}
}
}
if results.is_empty() {
eprintln!("Error sending");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
} else {
for result in &mut results {
match event_repo::insert(&pool, result).await {
Ok(i) => {
result.id = i;
}
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
}
}
response.data = results;
response.message = String::from(super::super::response::SUCCESSFUL);
(axum::http::StatusCode::OK, axum::Json(response))
}
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
} else {
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
} else {
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
+167
View File
@@ -0,0 +1,167 @@
pub mod request {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct RecordMessageEventRequest {
pub scheduled_message_event_id: uuid::Uuid,
pub response: serde_json::Value,
pub user_id: uuid::Uuid,
#[serde(with = "time::serde::rfc3339::option")]
pub sent: Option<time::OffsetDateTime>,
pub status: String,
pub contact_id: uuid::Uuid,
pub message_id: uuid::Uuid,
}
impl RecordMessageEventRequest {
pub fn is_valid(&self) -> bool {
!self.scheduled_message_event_id.is_nil()
|| !self.response.is_null()
|| !self.user_id.is_nil()
|| self.sent.is_some()
|| !self.status.is_empty()
|| !self.contact_id.is_nil()
|| !self.message_id.is_nil()
}
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct GetMessageEventResponseParams {
pub id: Option<uuid::Uuid>,
pub user_id: Option<uuid::Uuid>,
}
}
pub mod response {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct RecordMessageEventResponse {
pub message: String,
pub data: Vec<schedtxt_models::message::event::MessageEventResponse>,
}
pub use RecordMessageEventResponse as GetMERResponse;
}
pub mod endpoint {
use crate::repo::message::event as event_repo;
/// Endpoint to create a Message Event Response
#[utoipa::path(
post,
path = crate::caller::endpoints::RECORD_MESSAGE_EVENT_RESPONSE,
request_body(
content = super::request::RecordMessageEventRequest,
description = "Data needed to create a Message Event Response",
content_type = "application/json"
),
responses(
(status = 201, description = "Message Event Response created", body = super::response::RecordMessageEventResponse),
(status = 400, description = "Error", body = super::response::RecordMessageEventResponse),
(status = 500, description = "Error creating Message Event Response", body = super::response::RecordMessageEventResponse)
)
)]
pub async fn record_message_event_response(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::RecordMessageEventRequest>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::RecordMessageEventResponse>,
) {
let mut response = super::response::RecordMessageEventResponse::default();
if payload.is_valid() {
let mut mer = schedtxt_models::message::event::MessageEventResponse {
scheduled_message_event_id: Some(payload.scheduled_message_event_id),
user_id: payload.user_id,
sent: payload.sent,
status: payload.status,
contact_id: payload.contact_id,
message_id: payload.message_id,
response: payload.response,
..Default::default()
};
match event_repo::insert(&pool, &mer).await {
Ok(id) => {
mer.id = id;
response.message = String::from(super::super::super::response::SUCCESSFUL);
response.data.push(mer);
(axum::http::StatusCode::CREATED, axum::Json(response))
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
} else {
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
/// Endpoint to get Messages
#[utoipa::path(
get,
path = crate::caller::endpoints::GET_MESSAGE_EVENT_RESPONSE,
params(
("id" = uuid::Uuid, Path, description = "Id of Message Event Response"),
("user_id" = uuid::Uuid, Path, description = "User Id associated with the Message Event Response")
),
responses(
(status = 200, description = "Message Event Response found", body = super::response::GetMERResponse),
(status = 400, description = "Error getting Message Event Response", body = super::response::GetMERResponse)
)
)]
pub async fn get_record_message_event_response(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<
super::request::GetMessageEventResponseParams,
>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::GetMERResponse>,
) {
let mut response = super::response::GetMERResponse::default();
let messages = match params.id {
Some(id) => match event_repo::get(&pool, &id).await {
Ok(message) => {
vec![message]
}
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
},
None => match params.user_id {
Some(user_id) => match event_repo::get_with_user_id(&pool, &user_id).await {
Ok(messages) => messages,
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
},
None => {
response.message = String::from("Invalid parameter");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
},
};
response.data = messages;
response.message = String::from(super::super::super::response::SUCCESSFUL);
(axum::http::StatusCode::OK, axum::Json(response))
}
}
+154
View File
@@ -0,0 +1,154 @@
pub mod event;
pub mod scheduling;
pub mod request {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct AddMessageRequest {
pub content: String,
pub user_id: uuid::Uuid,
}
impl AddMessageRequest {
pub fn is_valid(&self) -> bool {
!self.content.is_empty() || !self.user_id.is_nil()
}
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct GetMessageParams {
pub id: Option<uuid::Uuid>,
pub user_id: Option<uuid::Uuid>,
}
}
pub mod response {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct AddMessageResponse {
pub message: String,
pub data: Vec<schedtxt_models::message::Message>,
}
pub use AddMessageResponse as GetMessageResponse;
}
pub mod endpoint {
use crate::repo::message as message_repo;
/// Endpoint to create Message
#[utoipa::path(
post,
path = super::super::endpoints::ADD_MESSAGE,
request_body(
content = super::request::AddMessageRequest,
description = "Data needed to create a Message",
content_type = "application/json"
),
responses(
(status = 201, description = "Message created", body = super::response::AddMessageResponse),
(status = 400, description = "Error", body = super::response::AddMessageResponse),
(status = 500, description = "Error creating Message", body = super::response::AddMessageResponse)
)
)]
pub async fn create_message(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::AddMessageRequest>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::AddMessageResponse>,
) {
let mut response = super::response::AddMessageResponse::default();
if payload.is_valid() {
let mut msg = schedtxt_models::message::Message {
content: payload.content,
user_id: Some(payload.user_id),
..Default::default()
};
match message_repo::insert(&pool, &msg, &payload.user_id).await {
Ok(message_id) => {
msg.id = Some(message_id);
response.data.push(msg);
response.message = String::from("Message created");
(axum::http::StatusCode::CREATED, axum::Json(response))
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Contact not created");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
} else {
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
/// Endpoint to get Messages
#[utoipa::path(
get,
path = super::super::endpoints::GET_MESSAGE,
params(
("id" = uuid::Uuid, Path, description = "Id of Message"),
("user_id" = uuid::Uuid, Path, description = "User Id associated with the Message")
),
responses(
(status = 200, description = "Songs found", body = super::response::GetMessageResponse),
(status = 400, description = "Error getting songs", body = super::response::GetMessageResponse)
)
)]
pub async fn get_messages(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<super::request::GetMessageParams>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::GetMessageResponse>,
) {
let mut response = super::response::GetMessageResponse::default();
let messages = match params.id {
Some(id) => match message_repo::get(&pool, &id).await {
Ok(message) => {
vec![message]
}
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
},
None => match params.user_id {
Some(user_id) => match message_repo::get_with_user_id(&pool, &user_id).await {
Ok(messages) => messages,
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
},
None => {
response.message = String::from("Invalid parameter");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
},
};
response.data = messages;
response.message = String::from(super::super::response::SUCCESSFUL);
(axum::http::StatusCode::OK, axum::Json(response))
}
}
+549
View File
@@ -0,0 +1,549 @@
pub mod request {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct ScheduleMessageRequest {
#[serde(with = "time::serde::rfc3339::option")]
pub scheduled: Option<time::OffsetDateTime>,
pub status: String,
pub user_id: uuid::Uuid,
}
impl ScheduleMessageRequest {
pub fn is_valid(&self) -> bool {
self.scheduled.is_some() || !self.status.is_empty() || !self.user_id.is_nil()
}
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct GetScheduledMessageParams {
pub id: Option<uuid::Uuid>,
pub user_id: Option<uuid::Uuid>,
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct CreateScheduleMessageEventRequest {
pub contact_id: uuid::Uuid,
pub message_id: uuid::Uuid,
pub scheduled_message_id: uuid::Uuid,
}
impl CreateScheduleMessageEventRequest {
pub fn is_valid(&self) -> bool {
!self.contact_id.is_nil()
|| !self.message_id.is_nil()
|| !self.scheduled_message_id.is_nil()
}
}
#[derive(Debug, Default, serde::Deserialize, serde::Serialize, utoipa::ToSchema)]
pub struct GetScheduledMessageEventParams {
pub id: Option<uuid::Uuid>,
pub scheduled_message_id: Option<uuid::Uuid>,
}
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct UpdateScheduledMessageStatusRequest {
pub status: String,
pub scheduled_message_id: uuid::Uuid,
}
impl UpdateScheduledMessageStatusRequest {
pub fn is_valid(&self) -> bool {
if !self.status.is_empty() || !self.scheduled_message_id.is_nil() {
self.status == schedtxt_models::message::scheduling::PENDING
|| self.status == schedtxt_models::message::scheduling::READY
|| self.status == schedtxt_models::message::scheduling::PROCESSING
|| self.status == schedtxt_models::message::scheduling::DONE
} else {
false
}
}
}
}
pub mod response {
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct ScheduleMessageResponse {
pub message: String,
pub data: Vec<schedtxt_models::message::scheduling::ScheduledMessage>,
}
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct GetScheduledMessageResponse {
pub message: String,
pub data: Vec<schedtxt_models::message::scheduling::ScheduledMessage>,
}
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct CreateScheduleMessageEventResponse {
pub message: String,
pub data: Vec<schedtxt_models::message::scheduling::ScheduledMessageEvent>,
}
pub use CreateScheduleMessageEventResponse as GetScheduledMessageEventResponse;
pub use CreateScheduleMessageEventResponse as DeleteScheduledMessageEventResponse;
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct UpdateScheduledMessageStatusResponse {
pub message: String,
pub data: Vec<ChangedStatus>,
}
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct ChangedStatus {
pub old_status: String,
}
pub use ScheduleMessageResponse as FetchScheduledMessageResponse;
}
pub mod endpoint {
use crate::repo::contact as contact_repo;
use crate::repo::message as message_repo;
use message_repo::scheduling as scheduling_repo;
/// Endpoint to create a Scheudled Message
#[utoipa::path(
post,
path = crate::caller::endpoints::SCHEDULE_MESSAGE,
request_body(
content = super::request::ScheduleMessageRequest,
description = "Data needed to create a Scheduled Message",
content_type = "application/json"
),
responses(
(status = 201, description = "Scheduled Message created", body = super::response::ScheduleMessageResponse),
(status = 400, description = "Error", body = super::response::ScheduleMessageResponse),
(status = 500, description = "Error creating Scheduled Message", body = super::response::ScheduleMessageResponse)
)
)]
pub async fn schedule_message(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::ScheduleMessageRequest>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::ScheduleMessageResponse>,
) {
let mut response = super::response::ScheduleMessageResponse::default();
if payload.is_valid() {
if payload.status != schedtxt_models::message::scheduling::PENDING {
response.message = String::from("scheduled message must be pending");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
} else {
if can_schedule(&payload.scheduled) {
let mut scheduled_message =
schedtxt_models::message::scheduling::ScheduledMessage {
scheduled: payload.scheduled,
status: payload.status,
..Default::default()
};
match scheduling_repo::insert(&pool, &scheduled_message, &payload.user_id).await
{
Ok((id, created)) => {
scheduled_message.id = id;
scheduled_message.created = Some(created);
scheduled_message.user_id = payload.user_id;
response.message = String::from("Message scheduled");
response.data.push(scheduled_message);
(axum::http::StatusCode::CREATED, axum::Json(response))
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Error inserting");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
} else {
response.message = String::from("Cannot schedule message");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
} else {
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
/// Endpoint to get Scheduled Message
#[utoipa::path(
get,
path = crate::caller::endpoints::GET_SCHEDULE_MESSAGE,
params(
("id" = uuid::Uuid, Path, description = "Id of Scheduled Message"),
("user_id" = uuid::Uuid, Path, description = "User Id associated with the Scheduled Message")
),
responses(
(status = 200, description = "Scheduled Message found", body = super::response::GetScheduledMessageResponse),
(status = 400, description = "Error getting Scheduled Message", body = super::response::GetScheduledMessageResponse)
)
)]
pub async fn get_scheduled_messages(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<
super::request::GetScheduledMessageParams,
>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::GetScheduledMessageResponse>,
) {
let mut response = super::response::GetScheduledMessageResponse::default();
let scheduled_messages = match params.id {
Some(id) => match scheduling_repo::get(&pool, &id).await {
Ok(scheduled_message) => {
vec![scheduled_message]
}
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
},
None => match params.user_id {
Some(user_id) => match scheduling_repo::get_with_user_id(&pool, &user_id).await {
Ok(scheduled_messages) => scheduled_messages,
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
},
None => {
response.message = String::from("Invalid parameter");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
},
};
response.data = scheduled_messages;
response.message = String::from(super::super::super::response::SUCCESSFUL);
(axum::http::StatusCode::OK, axum::Json(response))
}
fn can_schedule(scheduled_time: &Option<time::OffsetDateTime>) -> bool {
scheduled_time
.is_some_and(|t| t - time::OffsetDateTime::now_utc() >= time::Duration::minutes(10))
}
/// Endpoint to fetch Scheduled Message that is ready
#[utoipa::path(
get,
path = crate::caller::endpoints::FETCH_SCHEDULED_MESSAGE,
responses(
(status = 200, description = "Scheduled Message found", body = super::response::FetchScheduledMessageResponse),
(status = 400, description = "Error getting Scheduled Message", body = super::response::FetchScheduledMessageResponse)
)
)]
pub async fn fetch_scheduled_message(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::FetchScheduledMessageResponse>,
) {
let mut response = super::response::FetchScheduledMessageResponse::default();
match scheduling_repo::fetch(&pool).await {
Ok(scheduled_message) => {
response.data.push(scheduled_message);
response.message = String::from(super::super::super::response::SUCCESSFUL);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => match err {
sqlx::Error::RowNotFound => {
response.message = String::from("Nothing to fetch");
(axum::http::StatusCode::NO_CONTENT, axum::Json(response))
}
_ => {
eprintln!("Error: {err:?}");
response.message = String::from("Error");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
},
}
}
/// Endpoint to update status of Scheduled Message
#[utoipa::path(
patch,
path = crate::caller::endpoints::UPDATE_SCHEDULED_MESSAGE_STATUS,
request_body(
content = super::request::UpdateScheduledMessageStatusRequest,
description = "Data needed to update status of Scheduled Message",
content_type = "application/json"
),
responses(
(status = 200, description = "Status updated", body = super::response::UpdateScheduledMessageStatusResponse),
(status = 400, description = "Error", body = super::response::UpdateScheduledMessageStatusResponse),
(status = 500, description = "Error updating", body = super::response::UpdateScheduledMessageStatusResponse)
)
)]
pub async fn update_scheduled_message_status(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::UpdateScheduledMessageStatusRequest>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::UpdateScheduledMessageStatusResponse>,
) {
let mut response = super::response::UpdateScheduledMessageStatusResponse::default();
if payload.is_valid() {
match scheduling_repo::get(&pool, &payload.scheduled_message_id).await {
Ok(scheduled_message) => {
let old_status = scheduled_message.status;
if old_status == payload.status {
response.message = String::from("No change");
(axum::http::StatusCode::NOT_MODIFIED, axum::Json(response))
} else {
match scheduling_repo::update_status(
&pool,
&scheduled_message.id,
&payload.status,
)
.await
{
Ok(_) => {
response.message =
String::from(super::super::super::response::SUCCESSFUL);
response.data = vec![super::response::ChangedStatus { old_status }];
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Invalid");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
}
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Invalid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
} else {
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
/// Endpoint to create a Scheudled Message Event
#[utoipa::path(
post,
path = crate::caller::endpoints::CREATE_SCHEDULED_MESSAGE_EVENT,
request_body(
content = super::request::CreateScheduleMessageEventRequest,
description = "Data needed to create a Scheduled Message Event",
content_type = "application/json"
),
responses(
(status = 201, description = "Scheduled Message Event created", body = super::response::CreateScheduleMessageEventResponse),
(status = 400, description = "Error", body = super::response::CreateScheduleMessageEventResponse),
(status = 500, description = "Error creating Scheduled Message Event", body = super::response::CreateScheduleMessageEventResponse)
)
)]
pub async fn create_scheduled_message_event(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::Json(payload): axum::Json<super::request::CreateScheduleMessageEventRequest>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::CreateScheduleMessageEventResponse>,
) {
let mut response = super::response::CreateScheduleMessageEventResponse::default();
if payload.is_valid() {
match scheduling_repo::get(&pool, &payload.scheduled_message_id).await {
Ok(_) => {}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Scheduled Message does not exist");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
}
match contact_repo::get(&pool, &payload.contact_id).await {
Ok(_) => {}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Contact does not exist");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
}
match message_repo::get(&pool, &payload.message_id).await {
Ok(_) => {}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Contact does not exist");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
}
let mut scme = schedtxt_models::message::scheduling::ScheduledMessageEvent {
contact_id: payload.contact_id,
message_id: payload.message_id,
scheduled_message_id: payload.scheduled_message_id,
..Default::default()
};
match scheduling_repo::sched_msg_event::insert(&pool, &scme).await {
Ok((id, created)) => {
scme.id = id;
scme.created = Some(created);
response.message = String::from(super::super::super::response::SUCCESSFUL);
response.data.push(scme);
(axum::http::StatusCode::CREATED, axum::Json(response))
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Error inserting");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
} else {
response.message = String::from("Request body is not valid");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
/// Endpoint to get Scheduled Message Event
#[utoipa::path(
get,
path = crate::caller::endpoints::GET_SCHEDULED_MESSAGE_EVENT,
params(
("id" = uuid::Uuid, Path, description = "Id of Scheduled Message Event"),
("scheduled_message_id" = uuid::Uuid, Path, description = "Scheduled Message Id associated with the Scheduled Message Event")
),
responses(
(status = 200, description = "Scheduled Message Event found", body = super::response::GetScheduledMessageEventResponse),
(status = 400, description = "Invalid request", body = super::response::GetScheduledMessageEventResponse),
(status = 500, description = "Error getting Scheduled Message Event", body = super::response::GetScheduledMessageEventResponse)
)
)]
pub async fn get_scheduled_message_events(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Query(params): axum::extract::Query<
super::request::GetScheduledMessageEventParams,
>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::GetScheduledMessageEventResponse>,
) {
let mut response = super::response::GetScheduledMessageEventResponse::default();
let scheduled_message_events = match params.id {
Some(id) => match scheduling_repo::sched_msg_event::get(&pool, &id).await {
Ok(scheduled_message) => {
vec![scheduled_message]
}
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
},
None => match params.scheduled_message_id {
Some(scheduled_message_id) => {
match scheduling_repo::sched_msg_event::get_with_scheduled_message_id(
&pool,
&scheduled_message_id,
)
.await
{
Ok(scheduled_messages) => scheduled_messages,
Err(err) => {
eprintln!("Error: {err:?}");
return (
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
);
}
}
}
None => {
response.message = String::from("Invalid parameter");
return (axum::http::StatusCode::BAD_REQUEST, axum::Json(response));
}
},
};
response.data = scheduled_message_events;
response.message = String::from(super::super::super::response::SUCCESSFUL);
(axum::http::StatusCode::OK, axum::Json(response))
}
/// Endpoint to delete the Scheduled Message Event
#[utoipa::path(
delete,
path = crate::caller::endpoints::DELETE_SCHEDULED_MESSAGE_EVENT,
params(("id" = uuid::Uuid, Path, description = "Scheduled Message Event Id")),
responses(
(status = 200, description = "Deleted", body = super::response::DeleteScheduledMessageEventResponse),
(status = 400, description = "Bad request", body = super::response::DeleteScheduledMessageEventResponse),
(status = 500, description = "Error deleting", body = super::response::DeleteScheduledMessageEventResponse)
)
)]
pub async fn delete_scheduled_message_event(
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> (
axum::http::StatusCode,
axum::Json<super::response::DeleteScheduledMessageEventResponse>,
) {
let mut response = super::response::DeleteScheduledMessageEventResponse::default();
match scheduling_repo::sched_msg_event::get(&pool, &id).await {
Ok(scheduled_message_event) => {
match scheduling_repo::sched_msg_event::delete(&pool, &scheduled_message_event)
.await
{
Ok(_) => {
response.message = String::from(super::super::super::response::SUCCESSFUL);
response.data.push(scheduled_message_event);
(axum::http::StatusCode::OK, axum::Json(response))
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Error deleting");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(response),
)
}
}
}
Err(err) => {
eprintln!("Error: {err:?}");
response.message = String::from("Bad request");
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
pub mod contact;
pub mod instant;
pub mod message;
pub mod endpoints {
pub const ROOT: &str = "/";
/// Constant for adding Contact endpoint
pub const ADD_CONTACT: &str = "/api/v1/contact/new";
/// Constant for getting Contact endpoint
pub const GET_CONTACT: &str = "/api/v1/contact";
/// Constant for updating names of a Contact endpoint
pub const UPDATE_CONTACT_NAME: &str = "/api/v1/contact/update";
/// Constant for adding message endpoint
pub const ADD_MESSAGE: &str = "/api/v1/message/new";
/// Constant for getting messages endpoint
pub const GET_MESSAGE: &str = "/api/v1/message";
/// Constant for scheduling message endpoint
pub const SCHEDULE_MESSAGE: &str = "/api/v1/schedule/message";
/// Constant for getting scheduled message endpoint
pub const GET_SCHEDULE_MESSAGE: &str = "/api/v1/schedule/message";
/// Constant for updating scheduled message status endpoint
pub const UPDATE_SCHEDULED_MESSAGE_STATUS: &str = "/api/v1/schedule/message/status/update";
/// Constant for creating Scheduled Message Event endpoint
pub const CREATE_SCHEDULED_MESSAGE_EVENT: &str = "/api/v1/schedule/message/event";
/// Constant for getting Scheduled Message Event endpoint
pub const GET_SCHEDULED_MESSAGE_EVENT: &str = "/api/v1/schedule/message/event";
/// Constant for deleting Scheduled Message Event endpoint
pub const DELETE_SCHEDULED_MESSAGE_EVENT: &str = "/api/v1/schedule/message/event/{id}";
/// Constant for fetching Scheduled Message endpoint
pub const FETCH_SCHEDULED_MESSAGE: &str = "/api/v1/schedule/message/fetch";
/// Constant for recording Message Event Response endpoint
pub const RECORD_MESSAGE_EVENT_RESPONSE: &str =
"/api/v1/schedule/message/event/response/record";
/// Constant for getting Message Event Response endpoint
pub const GET_MESSAGE_EVENT_RESPONSE: &str = "/api/v1/schedule/message/event/response";
/// Constant for sending a message instantly. No scheduling
pub const INSTANT_MESSAGE: &str = "/api/v1/instant/message";
}
pub mod response {
pub const SUCCESSFUL: &str = "SUCCESSFUL";
}
/// Basic handler that responds with a static string
pub async fn root() -> &'static str {
"Hello, World!"
}
+219
View File
@@ -0,0 +1,219 @@
pub mod host {
pub const PORT: i64 = 9081;
pub fn get_full() -> String {
let address: &str = "0.0.0.0";
format!("{}:{}", address, PORT)
}
}
pub mod init {
use std::time::Duration;
use axum::routing::{delete, get, patch, post};
use tower_http::timeout::TimeoutLayer;
use utoipa::OpenApi;
use crate::caller::contact as contact_caller;
use crate::caller::message as message_caller;
use crate::caller::message::event as event_caller;
use crate::caller::message::scheduling as scheduling_caller;
use contact_caller::endpoint as contact_endpoints;
use contact_caller::response as contact_responses;
use event_caller::endpoint as event_endpoints;
use event_caller::response as event_responses;
use message_caller::endpoint as message_endpoints;
use message_caller::response as message_responses;
use scheduling_caller::endpoint as scheduling_endpoints;
use scheduling_caller::response as scheduling_responses;
mod cors {
pub async fn configure_cors() -> tower_http::cors::CorsLayer {
let cors = tower_http::cors::CorsLayer::new()
.allow_methods([
axum::http::Method::GET,
axum::http::Method::PATCH,
axum::http::Method::POST,
axum::http::Method::PUT,
axum::http::Method::DELETE,
]) // Specify allowed methods:cite[2]
.allow_headers([
axum::http::header::CONTENT_TYPE,
axum::http::header::AUTHORIZATION,
]) // Specify allowed headers:cite[2]
.allow_credentials(true) // If you need to send cookies or authentication headers:cite[2]
.max_age(std::time::Duration::from_secs(3600)); // Cache the preflight response for 1 hour:cite[2]
// Dynamically set the allowed origin based on the environment
match std::env::var(schedtxt_models::envy::keys::APP_ENV).as_deref() {
Ok("production") => {
// In production, allow only your specific, trusted origins
let allowed_origins_env =
schedtxt_models::envy::environment::get_allowed_origins();
match schedtxt_models::envy::utility::delimitize(&allowed_origins_env) {
Ok(alwd) => {
let allowed_origins: Vec<axum::http::HeaderValue> = alwd
.into_iter()
.map(|a| a.parse::<axum::http::HeaderValue>().unwrap())
.collect();
cors.allow_origin(allowed_origins)
}
Err(err) => {
eprintln!("Error getting allowed origins: Error: {err:?}");
std::process::exit(-1);
}
}
}
_ => {
// Development (default): Allow localhost origins
cors.allow_origin(vec![
format!("http://localhost:{}", super::super::host::PORT)
.parse()
.unwrap(),
format!("http://127.0.0.1:{}", super::super::host::PORT)
.parse()
.unwrap(),
"http://localhost:4200".parse().unwrap(),
"http://127.0.0.1:4200".parse().unwrap(),
])
}
}
}
}
#[derive(utoipa::OpenApi)]
#[openapi(
paths(contact_endpoints::create_contact,
message_endpoints::create_message,
scheduling_endpoints::schedule_message,
),
components(schemas(contact_responses::AddContactResponse, message_responses::AddMessageResponse,
scheduling_responses::ScheduleMessageResponse,
event_responses::RecordMessageEventResponse
)),
tags(
(name = "schedtxt API", description = "Web API to manage texting")
)
)]
struct ApiDoc;
pub async fn routes() -> axum::Router {
axum::Router::new()
.route(
crate::caller::endpoints::ADD_CONTACT,
post(contact_endpoints::create_contact).route_layer(axum::middleware::from_fn(
crate::auth::auth::<axum::body::Body>,
)),
)
.route(
crate::caller::endpoints::GET_CONTACT,
get(contact_endpoints::get_contacts).route_layer(axum::middleware::from_fn(
crate::auth::auth::<axum::body::Body>,
)),
)
.route(
crate::caller::endpoints::UPDATE_CONTACT_NAME,
patch(contact_endpoints::update_contact_names).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::caller::endpoints::ADD_MESSAGE,
post(message_endpoints::create_message).route_layer(axum::middleware::from_fn(
crate::auth::auth::<axum::body::Body>,
)),
)
.route(
crate::caller::endpoints::GET_MESSAGE,
get(message_endpoints::get_messages).route_layer(axum::middleware::from_fn(
crate::auth::auth::<axum::body::Body>,
)),
)
.route(
crate::caller::endpoints::SCHEDULE_MESSAGE,
post(scheduling_endpoints::schedule_message).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::caller::endpoints::GET_SCHEDULE_MESSAGE,
get(scheduling_endpoints::get_scheduled_messages).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::caller::endpoints::CREATE_SCHEDULED_MESSAGE_EVENT,
post(scheduling_endpoints::create_scheduled_message_event).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::caller::endpoints::GET_SCHEDULED_MESSAGE_EVENT,
get(scheduling_endpoints::get_scheduled_message_events).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::caller::endpoints::DELETE_SCHEDULED_MESSAGE_EVENT,
delete(scheduling_endpoints::delete_scheduled_message_event).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::caller::endpoints::UPDATE_SCHEDULED_MESSAGE_STATUS,
patch(scheduling_endpoints::update_scheduled_message_status).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::caller::endpoints::FETCH_SCHEDULED_MESSAGE,
get(scheduling_endpoints::fetch_scheduled_message).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::caller::endpoints::RECORD_MESSAGE_EVENT_RESPONSE,
post(event_endpoints::record_message_event_response).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::caller::endpoints::GET_MESSAGE_EVENT_RESPONSE,
get(event_endpoints::get_record_message_event_response).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.route(
crate::caller::endpoints::INSTANT_MESSAGE,
post(crate::caller::instant::endpoint::send_message).route_layer(
axum::middleware::from_fn(crate::auth::auth::<axum::body::Body>),
),
)
.layer(cors::configure_cors().await)
}
pub async fn app() -> axum::Router {
let pool = crate::db::create_pool()
.await
.expect("Failed to create pool");
crate::db::migrations(&pool).await;
let cors = cors::configure_cors().await;
routes()
.await
.merge(
utoipa_swagger_ui::SwaggerUi::new("/swagger-ui")
.url("/api-docs/openapi.json", ApiDoc::openapi()),
)
.layer(axum::Extension(pool))
.layer(axum::extract::DefaultBodyLimit::max(1024 * 1024 * 1024))
.layer(TimeoutLayer::with_status_code(
axum::http::StatusCode::OK,
Duration::from_secs(300),
))
.layer(cors)
}
}
+24
View File
@@ -0,0 +1,24 @@
use sqlx::postgres::PgPoolOptions;
pub mod connection_settings {
pub const MAXCONN: u32 = 10;
}
pub async fn create_pool() -> Result<sqlx::PgPool, sqlx::Error> {
let database_url = schedtxt_models::envy::environment::get_db_url().value;
println!("Database url: {database_url}");
PgPoolOptions::new()
.max_connections(connection_settings::MAXCONN)
.connect(&database_url)
.await
}
pub async fn migrations(pool: &sqlx::PgPool) {
// Run migrations using the sqlx::migrate! macro
// Assumes your migrations are in a ./migrations folder relative to Cargo.toml
sqlx::migrate!("./migrations")
.run(pool)
.await
.expect("Failed to run migrations");
}
+5
View File
@@ -0,0 +1,5 @@
pub mod auth;
pub mod caller;
pub mod config;
pub mod db;
pub mod repo;
+16
View File
@@ -0,0 +1,16 @@
#[tokio::main]
async fn main() {
// initialize tracing
tracing_subscriber::fmt::init();
match tokio::net::TcpListener::bind(schedtxt_api::config::host::get_full()).await {
Ok(listener) => {
// build our application with routes
let app = schedtxt_api::config::init::app().await;
axum::serve(listener, app).await.unwrap();
}
Err(err) => {
eprintln!("Error: {err:?}")
}
}
}
+200
View File
@@ -0,0 +1,200 @@
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
contact: &schedtxt_models::contact::Contact,
) -> Result<uuid::Uuid, sqlx::Error> {
match sqlx::query(
r#"
INSERT INTO "contacts" (firstname, lastname, nickname, phone_number, user_id)
VALUES($1, $2, $3, $4, $5) RETURNING id;
"#,
)
.bind(&contact.firstname)
.bind(&contact.lastname)
.bind(&contact.nickname)
.bind(&contact.phone_number)
.bind(contact.user_id)
.fetch_one(pool)
.await
{
Ok(row) => {
let id: uuid::Uuid = row.try_get("id")?;
Ok(id)
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<schedtxt_models::contact::Contact, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, firstname, lastname, nickname, phone_number, user_id FROM "contacts"
WHERE
id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
{
Ok(row) => {
let id: uuid::Uuid = row.try_get("id")?;
let phone_number: String = row.try_get("phone_number")?;
let user_id: uuid::Uuid = row.try_get("user_id")?;
Ok(schedtxt_models::contact::Contact {
id: Some(id),
firstname: match row.try_get("firstname") {
Ok(val) => val,
Err(err) => {
eprintln!("Error: {err:?}");
None
}
},
lastname: match row.try_get("lastname") {
Ok(val) => val,
Err(err) => {
eprintln!("Error: {err:?}");
None
}
},
nickname: match row.try_get("nickname") {
Ok(val) => val,
Err(err) => {
eprintln!("Error: nickname has no value for contacts record {err:?}");
None
}
},
phone_number,
user_id: Some(user_id),
})
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_user_id_and_phone(
pool: &sqlx::PgPool,
user_id: &uuid::Uuid,
phone_number: &str,
) -> Result<schedtxt_models::contact::Contact, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, firstname, lastname, nickname, phone_number, user_id FROM "contacts"
WHERE
phone_number = $1 AND user_id = $2;
"#,
)
.bind(phone_number)
.bind(user_id)
.fetch_one(pool)
.await
{
Ok(row) => {
println!("In here");
let id: uuid::Uuid = row.try_get("id")?;
let firstname: String = row.try_get("firstname")?;
let lastname: String = row.try_get("lastname")?;
let nickname: String = row.try_get("nickname")?;
let phone_number: String = row.try_get("phone_number")?;
let user_id: uuid::Uuid = row.try_get("user_id")?;
println!("Found");
Ok(schedtxt_models::contact::Contact {
id: Some(id),
firstname: Some(firstname),
lastname: Some(lastname),
nickname: Some(nickname),
phone_number,
user_id: Some(user_id),
})
}
Err(err) => match err {
sqlx::Error::RowNotFound => Err(err),
_ => Err(sqlx::Error::WorkerCrashed),
},
}
}
pub async fn get_with_user_id(
pool: &sqlx::PgPool,
user_id: &uuid::Uuid,
) -> Result<Vec<schedtxt_models::contact::Contact>, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, firstname, lastname, nickname, phone_number, user_id FROM "contacts"
WHERE
user_id = $1;
"#,
)
.bind(user_id)
.fetch_all(pool)
.await
{
Ok(rows) => {
let mut contacts: Vec<schedtxt_models::contact::Contact> = Vec::new();
for row in rows {
let id: uuid::Uuid = row.try_get("id")?;
let phone_number: String = row.try_get("phone_number")?;
let user_id: uuid::Uuid = row.try_get("user_id")?;
let contact = schedtxt_models::contact::Contact {
id: Some(id),
firstname: match row.try_get("firstname") {
Ok(val) => val,
Err(err) => {
eprintln!("Error: {err:?}");
None
}
},
lastname: match row.try_get("lastname") {
Ok(val) => val,
Err(err) => {
eprintln!("Error: {err:?}");
None
}
},
nickname: match row.try_get("nickname") {
Ok(val) => val,
Err(err) => {
eprintln!("Error: nickname has no value for contacts record {err:?}");
None
}
},
phone_number,
user_id: Some(user_id),
};
contacts.push(contact);
}
Ok(contacts)
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn update_names(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
firstname: &Option<String>,
lastname: &Option<String>,
nickname: &Option<String>,
) -> Result<(), sqlx::Error> {
match sqlx::query(
r#"
UPDATE "contacts" SET firstname = $1, lastname = $2, nickname = $3 WHERE id = $4;
"#,
)
.bind(firstname)
.bind(lastname)
.bind(nickname)
.bind(id)
.execute(pool)
.await
{
Ok(_row) => Ok(()),
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
+114
View File
@@ -0,0 +1,114 @@
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
event: &schedtxt_models::message::event::MessageEventResponse,
) -> Result<uuid::Uuid, sqlx::Error> {
match sqlx::query(
r#"
INSERT INTO "message_event_responses" (scheduled_message_event_id, response, user_id, sent, contact_id, message_id, status)
VALUES($1, $2, $3, $4, $5, $6, $7) RETURNING id;
"#,
)
.bind(event.scheduled_message_event_id)
.bind(&event.response)
.bind(event.user_id)
.bind(event.sent)
.bind(event.contact_id)
.bind(event.message_id)
.bind(&event.status)
.fetch_one(pool)
.await
{
Ok(row) => {
let id: uuid::Uuid = row.try_get("id")?;
Ok(id)
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<schedtxt_models::message::event::MessageEventResponse, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, scheduled_message_event_id, response, user_id, sent, contact_id, message_id, status FROM "message_event_responses"
WHERE
id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
{
Ok(row) => match parse_row(&row).await {
Ok(event) => Ok(event),
Err(err) => Err(err),
},
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_user_id(
pool: &sqlx::PgPool,
user_id: &uuid::Uuid,
) -> Result<Vec<schedtxt_models::message::event::MessageEventResponse>, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, scheduled_message_event_id, response, user_id, sent, contact_id, message_id, status FROM "message_event_responses"
WHERE
user_id = $1;
"#,
)
.bind(user_id)
.fetch_all(pool)
.await
{
Ok(rows) => {
let mut events: Vec<schedtxt_models::message::event::MessageEventResponse> =
Vec::new();
for row in rows {
match parse_row(&row).await {
Ok(event) => {
events.push(event);
}
Err(err) => {
return Err(err);
}
}
}
Ok(events)
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
async fn parse_row(
row: &sqlx::postgres::PgRow,
) -> Result<schedtxt_models::message::event::MessageEventResponse, sqlx::Error> {
println!("Parsing MER");
let id: uuid::Uuid = row.try_get("id")?;
let response: serde_json::Value = row.try_get("response")?;
let status: String = row.try_get("status")?;
let contact_id: uuid::Uuid = row.try_get("contact_id")?;
let message_id: uuid::Uuid = row.try_get("message_id")?;
let sent: time::OffsetDateTime = row.try_get("sent")?;
let scheduled_message_event_id: Option<uuid::Uuid> = row
.try_get("scheduled_message_event_id")
.unwrap_or_default();
let user_id: uuid::Uuid = row.try_get("user_id")?;
Ok(schedtxt_models::message::event::MessageEventResponse {
id,
response,
contact_id,
message_id,
status,
sent: Some(sent),
scheduled_message_event_id,
user_id,
})
}
+94
View File
@@ -0,0 +1,94 @@
pub mod event;
pub mod scheduling;
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
message: &schedtxt_models::message::Message,
user_id: &uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
match sqlx::query(
r#"
INSERT INTO "messages" (content, user_id)
VALUES($1, $2) RETURNING id;
"#,
)
.bind(&message.content)
.bind(user_id)
.fetch_one(pool)
.await
{
Ok(row) => {
let id: uuid::Uuid = row.try_get("id")?;
Ok(id)
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<schedtxt_models::message::Message, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, content, user_id FROM "messages"
WHERE
id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
{
Ok(row) => {
let id: uuid::Uuid = row.try_get("id")?;
let content: String = row.try_get("content")?;
let user_id: uuid::Uuid = row.try_get("user_id")?;
Ok(schedtxt_models::message::Message {
id: Some(id),
content,
user_id: Some(user_id),
})
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_user_id(
pool: &sqlx::PgPool,
user_id: &uuid::Uuid,
) -> Result<Vec<schedtxt_models::message::Message>, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, content, user_id FROM "messages"
WHERE
user_id = $1;
"#,
)
.bind(user_id)
.fetch_all(pool)
.await
{
Ok(rows) => {
let mut messages: Vec<schedtxt_models::message::Message> = Vec::new();
for row in rows {
let id: uuid::Uuid = row.try_get("id")?;
let content: String = row.try_get("content")?;
let user_id: uuid::Uuid = row.try_get("user_id")?;
let message = schedtxt_models::message::Message {
id: Some(id),
content,
user_id: Some(user_id),
};
messages.push(message);
}
Ok(messages)
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
+279
View File
@@ -0,0 +1,279 @@
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
scheduled_message: &schedtxt_models::message::scheduling::ScheduledMessage,
user_id: &uuid::Uuid,
) -> Result<(uuid::Uuid, time::OffsetDateTime), sqlx::Error> {
match sqlx::query(
r#"
INSERT INTO "scheduled_messages" (scheduled, status, user_id)
VALUES($1, $2, $3) RETURNING id, created;
"#,
)
.bind(scheduled_message.scheduled)
.bind(&scheduled_message.status)
.bind(user_id)
.fetch_one(pool)
.await
{
Ok(row) => {
let id: uuid::Uuid = row.try_get("id")?;
let created: time::OffsetDateTime = row.try_get("created")?;
Ok((id, created))
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<schedtxt_models::message::scheduling::ScheduledMessage, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, scheduled, created, status, user_id FROM "scheduled_messages"
WHERE
id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
{
Ok(row) => match parse_row(&row).await {
Ok(scheduled_message) => Ok(scheduled_message),
Err(err) => Err(err),
},
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_user_id(
pool: &sqlx::PgPool,
user_id: &uuid::Uuid,
) -> Result<Vec<schedtxt_models::message::scheduling::ScheduledMessage>, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, scheduled, created, status, user_id FROM "scheduled_messages"
WHERE
user_id = $1;
"#,
)
.bind(user_id)
.fetch_all(pool)
.await
{
Ok(rows) => {
let mut messages: Vec<schedtxt_models::message::scheduling::ScheduledMessage> =
Vec::new();
for row in rows {
match parse_row(&row).await {
Ok(scheduled_message) => {
messages.push(scheduled_message);
}
Err(err) => {
return Err(err);
}
}
}
Ok(messages)
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn fetch(
pool: &sqlx::PgPool,
) -> Result<schedtxt_models::message::scheduling::ScheduledMessage, sqlx::Error> {
match sqlx::query(
r#"
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
"#,
)
.bind(schedtxt_models::message::scheduling::PROCESSING)
.bind(schedtxt_models::message::scheduling::READY)
.fetch_one(pool)
.await
{
Ok(row) => match parse_row(&row).await {
Ok(scheduled_message) => Ok(scheduled_message),
Err(err) => Err(err),
},
Err(err) => Err(err),
}
}
pub async fn update_status(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
status: &str,
) -> Result<(), sqlx::Error> {
match sqlx::query(
r#"
UPDATE "scheduled_messages" SET status = $1 WHERE id = $2;
"#,
)
.bind(status)
.bind(id)
.execute(pool)
.await
{
Ok(_row) => Ok(()),
Err(err) => Err(err),
}
}
async fn parse_row(
row: &sqlx::postgres::PgRow,
) -> Result<schedtxt_models::message::scheduling::ScheduledMessage, sqlx::Error> {
let id: uuid::Uuid = row.try_get("id")?;
let scheduled: time::OffsetDateTime = row.try_get("scheduled")?;
let status: String = row.try_get("status")?;
let created: time::OffsetDateTime = row.try_get("created")?;
let user_id: uuid::Uuid = row.try_get("user_id")?;
Ok(schedtxt_models::message::scheduling::ScheduledMessage {
id,
scheduled: Some(scheduled),
created: Some(created),
status,
user_id,
})
}
pub mod sched_msg_event {
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
scheduled_message_event: &schedtxt_models::message::scheduling::ScheduledMessageEvent,
) -> Result<(uuid::Uuid, time::OffsetDateTime), sqlx::Error> {
match sqlx::query(
r#"
INSERT INTO "scheduled_message_events" (contact_id, message_id, scheduled_message_id)
VALUES($1, $2, $3) RETURNING id, created;
"#,
)
.bind(scheduled_message_event.contact_id)
.bind(scheduled_message_event.message_id)
.bind(scheduled_message_event.scheduled_message_id)
.fetch_one(pool)
.await
{
Ok(row) => {
let id: uuid::Uuid = row.try_get("id")?;
let created: time::OffsetDateTime = row.try_get("created")?;
Ok((id, created))
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get(
pool: &sqlx::PgPool,
id: &uuid::Uuid,
) -> Result<schedtxt_models::message::scheduling::ScheduledMessageEvent, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, contact_id, message_id, scheduled_message_id, created FROM "scheduled_message_events"
WHERE
id = $1;
"#,
)
.bind(id)
.fetch_one(pool)
.await
{
Ok(row) => match parse_row(&row).await {
Ok(scheduled_message_event) => Ok(scheduled_message_event),
Err(err) => Err(err),
},
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn get_with_scheduled_message_id(
pool: &sqlx::PgPool,
scheduled_message_id: &uuid::Uuid,
) -> Result<Vec<schedtxt_models::message::scheduling::ScheduledMessageEvent>, sqlx::Error> {
match sqlx::query(
r#"
SELECT id, contact_id, message_id, scheduled_message_id, created FROM "scheduled_message_events"
WHERE
scheduled_message_id = $1;
"#,
)
.bind(scheduled_message_id)
.fetch_all(pool)
.await
{
Ok(rows) => {
let mut scheduled_message_events: Vec<schedtxt_models::message::scheduling::ScheduledMessageEvent> =
Vec::new();
for row in rows {
match parse_row(&row).await {
Ok(scheduled_message_event) => {
scheduled_message_events.push(scheduled_message_event);
}
Err(err) => {
return Err(err);
}
}
}
Ok(scheduled_message_events)
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
pub async fn delete(
pool: &sqlx::PgPool,
scheduled_message_event: &schedtxt_models::message::scheduling::ScheduledMessageEvent,
) -> Result<(), sqlx::Error> {
match sqlx::query(
r#"
DELETE FROM "scheduled_message_events"
WHERE id = $1
"#,
)
.bind(scheduled_message_event.id)
.execute(pool)
.await
{
Ok(_) => Ok(()),
Err(err) => Err(err),
}
}
async fn parse_row(
row: &sqlx::postgres::PgRow,
) -> Result<schedtxt_models::message::scheduling::ScheduledMessageEvent, sqlx::Error> {
let id: uuid::Uuid = row.try_get("id")?;
let contact_id: uuid::Uuid = row.try_get("contact_id")?;
let message_id: uuid::Uuid = row.try_get("message_id")?;
let scheduled_message_id: uuid::Uuid = row.try_get("scheduled_message_id")?;
let created: time::OffsetDateTime = row.try_get("created")?;
Ok(
schedtxt_models::message::scheduling::ScheduledMessageEvent {
id,
contact_id,
message_id,
scheduled_message_id,
created: Some(created),
},
)
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod contact;
pub mod message;
+1645
View File
File diff suppressed because it is too large Load Diff