104 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
phoenixandphoenix 0cd71c5506 tsk-34: Fetch next ready scheduled message (#38)
Closes #34

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

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

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

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

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

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

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

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

Reviewed-on: phoenix/textsender-api#23
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-10 16:25:34 +00:00
phoenixandphoenix 5f9bfa8fe2 tsk-9: Schedule queued message endpoint (#22)
Closes #9

Reviewed-on: phoenix/textsender-api#22
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-09 04:32:30 +00:00
phoenixandphoenix 0eba955ddf tsk-16: Permitting identical messages by a user (#21)
Closes #16

Reviewed-on: phoenix/textsender-api#21
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-07 17:29:40 +00:00
phoenixandphoenix 607059e7ce tsk-12: Added endpoint to get message (#18)
Closes #12

Reviewed-on: phoenix/textsender-api#18
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-06 20:42:24 +00:00
phoenixandphoenix acf414c876 tsk-11: Added endpoint to get Contact (#17)
Closes #11

Reviewed-on: phoenix/textsender-api#17
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-06 20:09:52 +00:00
phoenixandphoenix 507eb9eb09 tsk-8: Create Message endpoint (#15)
Closes #8

Reviewed-on: phoenix/textsender-api#15
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-05 19:37:49 +00:00
phoenixandphoenix 2045465111 tsk-6: Added docker support (#14)
Closes #6

Reviewed-on: phoenix/textsender-api#14
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-05 18:22:55 +00:00
phoenixandphoenix d332cde84f tsk-3: Add Token support (#10)
Closes #3

Reviewed-on: phoenix/textsender-api#10
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-04 17:51:35 +00:00
phoenixandphoenix 96fc87ea5e tsk-5: Add contact (#7)
Closes #5

Reviewed-on: phoenix/textsender-api#7
Co-authored-by: phoenix <kundeng00@pm.me>
Co-committed-by: phoenix <kundeng00@pm.me>
2025-11-03 22:27:34 +00:00
phoenix 3db003fc47 Updated readme 2025-10-29 19:55:02 -04:00
phoenix e3a23aae6d Fix build issue
Commented out some code for now
2025-10-29 19:52:47 -04:00
phoenix cf518fdcfb Added Makefile 2025-10-29 19:52:25 -04:00
phoenix 592c51c950 Moved version package 2025-10-29 19:51:25 -04:00
44 changed files with 8354 additions and 252 deletions
+9
View File
@@ -0,0 +1,9 @@
# Ignore git directory
.git/
.gitea/
# Ignore environment files (configure via docker-compose instead)
.env*
target/
+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=main_db
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
+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
-6
View File
@@ -1,6 +0,0 @@
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
-42
View File
@@ -1,42 +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@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: '1.24.5' # You can specify a specific version or 'stable'
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
- name: Run gofmt (optional)
# Uncomment to check code formatting with gofmt
run: |
if [ -n "$(gofmt -l.)" ]; then
echo "Go code is not formatted. Please run 'gofmt -w.' to fix it."
exit 1
fi
- name: Run golint (optional)
# Uncomment to check code style with golint
run: |
if [ -n "$(golint./...)" ]; then
echo "Go code has style issues. Please run 'golint./...' to see them."
exit 1
fi
+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
+2 -3
View File
@@ -1,5 +1,4 @@
/textsender-api
/vendor/
.env .env
.env.local .env.local
.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" }
+50
View File
@@ -0,0 +1,50 @@
FROM rust:1.97 as builder
# Set the working directory inside the container
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config libssl3 \
ca-certificates \
openssh-client git \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p -m 0700 ~/.ssh && \
echo "Host git.kundeng.us" >> ~/.ssh/config && \
echo " User git" >> ~/.ssh/config && \
chmod 600 ~/.ssh/config
RUN ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts
COPY Cargo.toml Cargo.lock ./
RUN --mount=type=ssh mkdir src && \
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 the actual source code
COPY src ./src
COPY .env ./.env
COPY migrations ./migrations
RUN --mount=type=ssh \
cargo build --release --quiet
FROM debian:trixie-slim
# 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/*
# Set the working directory
WORKDIR /usr/local/bin
COPY --from=builder /usr/src/app/target/release/schedtxt_api .
COPY --from=builder /usr/src/app/.env .
COPY --from=builder /usr/src/app/migrations ./migrations
EXPOSE 9081
# 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.
+41 -4
View File
@@ -1,9 +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.
TODO ```
```BASH .
..
- 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
``` ```
-17
View File
@@ -1,17 +0,0 @@
package main
import "fmt"
import "log"
import "net/http"
import "git.kundeng.us/phoenix/textsender-api/internal/config"
import "git.kundeng.us/phoenix/textsender-api/internal/handler"
func main() {
fmt.Println(config.APP_NAME)
http.HandleFunc(handler.MESSAGE_DRAFT_ENDPOINT, handler.DraftMessageHandler)
log.Println("Starting server", config.PORT)
log.Fatal(http.ListenAndServe(config.PortString(), nil))
}
+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
-5
View File
@@ -1,5 +0,0 @@
module git.kundeng.us/phoenix/textsender-api
go 1.25.3
require github.com/google/uuid v1.6.0
-2
View File
@@ -1,2 +0,0 @@
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=
-13
View File
@@ -1,13 +0,0 @@
package config
import "fmt"
const PORT = 8080
const APP_NAME = "textsender-api"
func PortString() string {
portMessage := fmt.Sprintf(":%d", PORT)
return portMessage
}
-82
View File
@@ -1,82 +0,0 @@
package handler
import (
"fmt"
"net/http"
"github.com/google/uuid"
)
type RequestAddContact struct {
PhoneNumber string `json:"phone_number"`
UserId uuid.UUID`json:"user_id"`
}
type LoginResponse struct {
Message string `json:"message"`
Data []model.Data`json:"data"`
}
type ContactHandler struct {
}
func NewContactHandler(store model.Store) *ContactHandler {
return &ContactHandler{Store: store}
}
func (l *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()
var statusCode int
/*
var resp LoginResponse
ctx := r.Context()
if exists, err := l.UserStore.UserExists(ctx, req.Username); err != nil {
fmt.Printf("Error: %v", err)
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
if !exists {
statusCode = http.StatusBadRequest
resp.Message = "Failure in user check"
} else {
if user, err := l.UserStore.GetUserByUsername(ctx, req.Username); err != nil {
statusCode = http.StatusInternalServerError
resp.Message = err.Error()
} else {
hashing := utility.HashMash{Password: req.Password}
if hashing.CheckPasswordHash(req.Password, user.Password) {
var tokGen utility.TokenGenerator
secretKey := config.GetSecretKey()
tokGen.SetSecretKey(secretKey)
if token, err := tokGen.GenerateToken(*user); err != nil {
fmt.Println(err.Error())
statusCode = http.StatusInternalServerError
resp.Message = "Error generating token"
} else {
statusCode = http.StatusOK
resp.Data = append(resp.Data, *token)
resp.Message = "Successful"
}
} else {
statusCode = http.StatusNotFound
resp.Message = "User not found"
}
}
}
}
*/
RespondWithJson(w, statusCode, &resp)
}
-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)
}
-4
View File
@@ -1,4 +0,0 @@
package handler
const MESSAGE_DRAFT_ENDPOINT = "/api/v1/message/draft"
const ADD_CONTACT_ENDPOINT = "/api/v1/contact";
-25
View File
@@ -1,25 +0,0 @@
package handler
import "encoding/json"
import "log"
import "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)
}
}
-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"`
}
+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'))
);
+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
-7
View File
@@ -1,7 +0,0 @@
package version
var (
Version = "dev"
Commit = "none"
Date = "unknown"
)