101 Commits
Author SHA1 Message Date
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
43 changed files with 8335 additions and 257 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=textsender_db
DB_MAIN_USER=textsender
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://textsender.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=textsender_db
DB_MAIN_USER=textsender
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://textsender.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: textsender_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.96.1
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
chmod 600 ~/.ssh/textsender-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender-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.96.1
- run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
chmod 600 ~/.ssh/textsender-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender-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.96.1
- run: rustup component add clippy
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
chmod 600 ~/.ssh/textsender-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender-models_deploy_key
cargo clippy -- -D warnings
+91
View File
@@ -0,0 +1,91 @@
name: Rust Build
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.96.1
# --- 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/textsender-models_deploy_key
chmod 600 ~/.ssh/textsender-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender-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.96.1
- run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
chmod 600 ~/.ssh/textsender-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender-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: Rust 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.96.1
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
chmod 600 ~/.ssh/textsender-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender-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.96.1
# --- 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/textsender-models_deploy_key
chmod 600 ~/.ssh/textsender-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender-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.96.1
- run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
chmod 600 ~/.ssh/textsender-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender-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.96.1
- run: rustup component add clippy
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
chmod 600 ~/.ssh/textsender-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender-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.96.1
- uses: Swatinem/rust-cache@v2
- run: |
mkdir -p ~/.ssh
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key
chmod 600 ~/.ssh/textsender-models_deploy_key
ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add -v ~/.ssh/textsender-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
+3641
View File
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "textsender_api"
version = "0.2.3"
edition = "2024"
rust-version = "1.96.1"
[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.3", 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"] }
textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender_models.git", tag = "v0.5.1" }
swoosh = { git = "ssh://git@git.kundeng.us/phoenix/swoosh.git", tag = "v0.5.2" }
[dev-dependencies]
tower = { version = "0.5.3", features = ["full"] }
josekit = { version = "0.10.3" }
url = { version = "2.5.8" }
+70
View File
@@ -0,0 +1,70 @@
# Stage 1: Build the application
FROM rust:1.96.1 as builder
# Set the working directory inside the container
WORKDIR /usr/src/app
# Install build dependencies if needed (e.g., git for cloning)
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/*
# Create .ssh/ directory for internal dependencies
RUN mkdir -p -m 0700 ~/.ssh && \
echo "Host git.kundeng.us" >> ~/.ssh/config && \
echo " User git" >> ~/.ssh/config && \
chmod 600 ~/.ssh/config
# << --- ADD HOST KEY HERE --- >>
RUN ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts
# Copy Cargo manifests
COPY Cargo.toml Cargo.lock ./
# Build *only* dependencies to leverage Docker cache
# This dummy build caches dependencies as a separate layer
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/textsender_api* # Clean up dummy build artifacts (replace textsender_api)
# Copy the actual source code
COPY src ./src
# If you have other directories like `templates` or `static`, copy them too
COPY .env ./.env
COPY migrations ./migrations
# << --- SSH MOUNT ADDED HERE --- >>
# Build *only* dependencies to leverage Docker cache
# This dummy build caches dependencies as a separate layer
# Mount the SSH agent socket for this command
RUN --mount=type=ssh \
cargo build --release --quiet
# Stage 2: Create the final, smaller runtime image
# Use a minimal base image like debian-slim or even distroless for security/size
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 the compiled binary from the builder stage
# Replace 'textsender_api' with the actual name of your binary (usually the crate name)
COPY --from=builder /usr/src/app/target/release/textsender_api .
# Copy other necessary files like .env (if used for runtime config) or static assets
# It's generally better to configure via environment variables in Docker though
COPY --from=builder /usr/src/app/.env .
COPY --from=builder /usr/src/app/migrations ./migrations
# Expose the port your Axum app listens on (e.g., 3000 or 8000)
EXPOSE 9081
# Set the command to run your application
# Ensure this matches the binary name copied above
CMD ["./textsender_api"]
-9
View File
@@ -1,9 +0,0 @@
# TextSender-API
A software system to process Text messaging queueing
## Getting started
TODO
```BASH
```
-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))
}
+129
View File
@@ -0,0 +1,129 @@
version: '3.8' # Use a recent version
services:
# --- Web API ---
api:
build: # Tells docker-compose to build the Dockerfile in the current directory
context: .
ssh: ["default"] # Uses host's SSH agent
container_name: textsender_api # Optional: Give the container a specific name
ports:
# Map host port 8000 to container port 3000 (adjust as needed)
- "9081:9081"
env_file:
- .env
depends_on:
main_db:
condition: service_healthy
networks:
- textsender_api-network
restart: unless-stopped # Optional: Restart policy
# --- Auth API ---
auth_api:
build:
# Might want to change the naming convention at some point for the repo names. textsender-models, textsender_auth, textesender-api, etc
context: ../textsender_auth # IMPORTANT: Relative path to the local checkout of your web API repo (containing the Dockerfile)
ssh: ["default"] # Uses host's SSH agent
dockerfile: Dockerfile # Optional: Specify if your Dockerfile has a non-standard name
container_name: auth_api
restart: unless-stopped
ports:
- "9080:9080"
# environment:
# Environment variables your API needs, e.g., database connection
# Add other necessary environment variables
env_file:
- ../textsender_auth/.env
depends_on:
auth_db:
condition: service_healthy
networks:
- textsender_api-network
# Optional: Mount local code for development (live reload)
# volumes:
# - ./path/to/your/web-api-repo:/app
# --- 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:
- textsender_api-network
# PostgreSQL Database Service
# --- textsender_api web api db ---
main_db:
image: postgres:18.4-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: textsender_api_db # Optional: Give the container a specific name
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${DB_MAIN_USER:-textsender_api}
POSTGRES_PASSWORD: ${DB_MAIN_PASSWORD:-password}
POSTGRES_DB: ${DB_MAIN_NAME:-textsender_api_db}
volumes:
# Persist database data using a named volume
- postgres_data:/var/lib/postgresql
ports:
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
- "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 # Optional: Restart policy
networks:
- textsender_api-network
# --- textsender_api web auth api db ---
auth_db:
image: postgres:18.4-alpine # Use an official Postgres image (Alpine variant is smaller)
container_name: textsender_api_auth_db # Optional: Give the container a specific name
environment:
# These MUST match the user, password, and database name in the DATABASE_URL above
POSTGRES_USER: ${DB_AUTH_USER:-textsender_auth}
POSTGRES_PASSWORD: ${DB_AUTH_PASSWORD:-password}
POSTGRES_DB: ${DB_AUTH_NAME:-textsender_auth_db}
volumes:
# Persist database data using a named volume
- postgres_data_auth:/var/lib/postgresql
ports:
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
- "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 # Optional: Restart policy
networks:
- textsender_api-network
# Define the named volume for data persistence
volumes:
postgres_data:
driver: local # Use the default local driver
postgres_data_auth:
driver: local # Use the default local driver
# Define the network (optional, but good practice)
networks:
textsender_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 = textsender_models::envy::environment::get_secret_main_key().value;
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.set_audience(&["textsender"]); // Must match exactly what's in the token
let _claims = decode::<textsender_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)
}
+312
View File
@@ -0,0 +1,312 @@
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<textsender_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>,
}
/*
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct GetContactResponse {
pub message: String,
pub data: Vec<textsender_models::contact::Contact>,
}
*/
}
pub mod endpoint {
// use axum::{Json, response::IntoResponse};
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 = textsender_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<textsender_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<textsender_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 textsender_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<
textsender_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 = textsender_models::message::event::MessageEventResponse {
user_id: payload.user_id,
// TODO: Make this more accurate
sent: Some(time::OffsetDateTime::now_utc()),
status: String::from(textsender_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<textsender_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 = textsender_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<textsender_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 = textsender_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 == textsender_models::message::scheduling::PENDING
|| self.status == textsender_models::message::scheduling::READY
|| self.status == textsender_models::message::scheduling::PROCESSING
|| self.status == textsender_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<textsender_models::message::scheduling::ScheduledMessage>,
}
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct GetScheduledMessageResponse {
pub message: String,
pub data: Vec<textsender_models::message::scheduling::ScheduledMessage>,
}
#[derive(Debug, Default, Deserialize, Serialize, ToSchema)]
pub struct CreateScheduleMessageEventResponse {
pub message: String,
pub data: Vec<textsender_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 != textsender_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 =
textsender_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 = textsender_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(textsender_models::envy::keys::APP_ENV).as_deref() {
Ok("production") => {
// In production, allow only your specific, trusted origins
let allowed_origins_env =
textsender_models::envy::environment::get_allowed_origins();
match textsender_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 = "textsender 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 = textsender_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(textsender_api::config::host::get_full()).await {
Ok(listener) => {
// build our application with routes
let app = textsender_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: &textsender_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<textsender_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(textsender_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<textsender_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(textsender_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<textsender_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<textsender_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 = textsender_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: &textsender_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<textsender_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<textsender_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<textsender_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<textsender_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(textsender_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: &textsender_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<textsender_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(textsender_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<textsender_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<textsender_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 = textsender_models::message::Message {
id: Some(id),
content,
user_id: Some(user_id),
};
messages.push(message);
}
Ok(messages)
}
Err(_) => Err(sqlx::Error::RowNotFound),
}
}
+280
View File
@@ -0,0 +1,280 @@
use sqlx::Row;
pub async fn insert(
pool: &sqlx::PgPool,
scheduled_message: &textsender_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<textsender_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<textsender_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<textsender_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<textsender_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(textsender_models::message::scheduling::PROCESSING)
.bind(textsender_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<textsender_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(textsender_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: &textsender_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<textsender_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<textsender_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<textsender_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: &textsender_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<textsender_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(
textsender_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;
+1653
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"
)