├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── ci-local.sh ├── docs ├── broadcast.md ├── images │ └── mvp.jpeg └── run-ci-locally.md └── src ├── bin ├── client.rs └── server │ ├── connection.rs │ ├── group.rs │ ├── group_table.rs │ └── main.rs ├── lib.rs └── utils.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | RUSTFLAGS: -D warnings 8 | 9 | jobs: 10 | 11 | ci: 12 | name: Build and test 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Cache dependencies 20 | uses: actions/cache@v4 21 | with: 22 | path: | 23 | ~/.cargo/bin 24 | ~/.cargo/registry/index/ 25 | ~/.cargo/registry/cache/ 26 | ~/.cargo/git/db/ 27 | target/ 28 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 31 | ${{ runner.os }}-cargo 32 | 33 | - name: Set up Rust 34 | uses: actions-rust-lang/setup-rust-toolchain@v1 35 | with: 36 | toolchain: stable 37 | rustflags: 38 | 39 | - name: Install Nextest test runner 40 | uses: taiki-e/install-action@nextest 41 | 42 | - name: Check Formatting 43 | run: cargo fmt --all -- --check 44 | 45 | - name: Build Project 46 | run: cargo build --workspace --all-features 47 | 48 | - name: Run Tests 49 | run: cargo nextest run --workspace --all-targets --all-features --no-fail-fast || echo "No tests found, skipping..." 50 | 51 | - name: Lints checks 52 | run: | 53 | cargo clippy --workspace --all-targets --all-features -- -D warnings 54 | 55 | - name: Check API documentation 56 | run: cargo doc --workspace --all-features --no-deps 57 | 58 | - name: Install cargo-audit 59 | uses: taiki-e/install-action@v2 60 | with: 61 | tool: cargo-audit 62 | 63 | - name: Run Security Audit 64 | run: cargo audit 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | Thank you for your interest in contributing to our async chatting app project! Below are the guidelines to help you get started with contributing effectively. 4 | 5 | ## Submit Pull Requests 6 | 7 | - All changes should be proposed via GitHub pull requests. 8 | 9 | - A project maintainer will review your PR, provide feedback, and either approve or request modifications. 10 | 11 | - Even experienced contributors should follow this process. 12 | 13 | ## Claim Issues First 14 | 15 | Before working on an issue, comment on it to let others know you're tackling it.This prevents duplicate work and helps coordinate efforts. 16 | 17 | ## Reporting New Issues 18 | 19 | Found a bug or have a feature idea? 20 | 21 | - Search existing issues to avoid duplicates. 22 | 23 | - Use templates: Fill out the Bug Report or Feature Request template. 24 | 25 | - Provide details: 26 | 27 | - Bug: Steps to reproduce, expected vs. actual behavior. 28 | 29 | - Feature: Use case and proposed solution. 30 | 31 | 32 | 33 | ## Get Started 34 | Fork the repository on GitHub 35 | 36 | Clone your fork locally: 37 | 38 | ```sh 39 | git clone https://github.com/your-username/async-chat-app.git 40 | cd async-chat-app 41 | ``` 42 | 43 | Set up upstream remote: 44 | ```sh 45 | git remote add upstream https://github.com/original-owner/async-chat-app.git 46 | ``` 47 | 48 | 49 | ## Branch Naming 50 | 51 | Use descriptive branch names prefixed with: 52 | 53 | - feature/issue-1-local-chat 54 | 55 | - bugfix/issue-3-linting-workflow 56 | 57 | - docs/issue-7-contribution-guidelines 58 | 59 | - refactor/ issue-number-changes 60 | 61 | - test/issue-number-changes 62 | 63 | ## Commit Messages 64 | 65 | - Follow Conventional Commits style 66 | 67 | - Feat(user): integrate user management into server connection handling 68 | 69 | - Use the present tense ("Add feature" not "Added feature") 70 | 71 | - Keep messages concise but descriptive 72 | 73 | ## Pull Requests 74 | 75 | - Keep PRs focused on a single feature/bugfix 76 | 77 | - Reference any related issues 78 | 79 | - Include a clear description of changes 80 | 81 | - Update documentation if needed 82 | 83 | - Ensure all tests pass 84 | 85 | ## Pull Request Checklist 86 | 87 | Before submitting your pull request, please ensure the following: 88 | 89 | - Branch from main: Create your feature/bugfix branch from the latest main branch. If updates occur in main while your PR is pending, rebase your changes to avoid merge conflicts. 90 | 91 | - Keep commits small & functional: Each commit should be self-contained (compiling and passing tests) while being as granular as possible. 92 | 93 | - Sign-off commits: Include a Developer Certificate of Origin (DCO) sign-off using git commit -s to certify your contribution under the project’s license terms. 94 | 95 | - Request reviews proactively: If your PR needs attention, tag relevant reviewers (@username) in a comment or reach out in the project’s chat (e.g., Discord channel). 96 | 97 | - Add tests: Include unit/integration tests for new features or bug fixes. For backend changes, test API endpoints; for frontend, add UI/component tests as needed. Refer to the testing guide (link your project’s guide here) for specifics. 98 | 99 | Note: PRs that don’t merge cleanly with main may require a rebase before approval. 100 | 101 | ## Code Style 102 | ### General 103 | 104 | - Follow existing patterns in the codebase 105 | 106 | - Keep functions small and focused 107 | 108 | - Use descriptive variable names 109 | 110 | 111 | 112 | ## Testing 113 | 114 | Write tests for new features 115 | 116 | Update tests when fixing bugs 117 | 118 | Run all tests before submitting PR: 119 | ```bash 120 | cargo test 121 | ``` 122 | 123 | ## Communication 124 | 125 | - Use GitHub issues for feature requests and bug reports 126 | 127 | - Be respectful and inclusive in all communications 128 | 129 | - Ask questions if anything is unclear 130 | 131 | ## Getting Help 132 | 133 | If you need help at any point: 134 | 135 | - Check the project's [README](./README.md) 136 | 137 | - Look through existing issues 138 | 139 | - Reach out to maintainers via GitHub discussions 140 | 141 | We appreciate your contributions and look forward to collaborating with you! -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.97" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" 25 | 26 | [[package]] 27 | name = "async-channel" 28 | version = "1.9.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" 31 | dependencies = [ 32 | "concurrent-queue", 33 | "event-listener 2.5.3", 34 | "futures-core", 35 | ] 36 | 37 | [[package]] 38 | name = "async-channel" 39 | version = "2.3.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" 42 | dependencies = [ 43 | "concurrent-queue", 44 | "event-listener-strategy", 45 | "futures-core", 46 | "pin-project-lite", 47 | ] 48 | 49 | [[package]] 50 | name = "async-chat" 51 | version = "0.1.0" 52 | dependencies = [ 53 | "anyhow", 54 | "async-std", 55 | "serde", 56 | "serde_json", 57 | "tokio", 58 | ] 59 | 60 | [[package]] 61 | name = "async-executor" 62 | version = "1.13.1" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" 65 | dependencies = [ 66 | "async-task", 67 | "concurrent-queue", 68 | "fastrand", 69 | "futures-lite", 70 | "slab", 71 | ] 72 | 73 | [[package]] 74 | name = "async-global-executor" 75 | version = "2.4.1" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" 78 | dependencies = [ 79 | "async-channel 2.3.1", 80 | "async-executor", 81 | "async-io", 82 | "async-lock", 83 | "blocking", 84 | "futures-lite", 85 | "once_cell", 86 | ] 87 | 88 | [[package]] 89 | name = "async-io" 90 | version = "2.4.0" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" 93 | dependencies = [ 94 | "async-lock", 95 | "cfg-if", 96 | "concurrent-queue", 97 | "futures-io", 98 | "futures-lite", 99 | "parking", 100 | "polling", 101 | "rustix", 102 | "slab", 103 | "tracing", 104 | "windows-sys", 105 | ] 106 | 107 | [[package]] 108 | name = "async-lock" 109 | version = "3.4.0" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" 112 | dependencies = [ 113 | "event-listener 5.4.0", 114 | "event-listener-strategy", 115 | "pin-project-lite", 116 | ] 117 | 118 | [[package]] 119 | name = "async-process" 120 | version = "2.3.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" 123 | dependencies = [ 124 | "async-channel 2.3.1", 125 | "async-io", 126 | "async-lock", 127 | "async-signal", 128 | "async-task", 129 | "blocking", 130 | "cfg-if", 131 | "event-listener 5.4.0", 132 | "futures-lite", 133 | "rustix", 134 | "tracing", 135 | ] 136 | 137 | [[package]] 138 | name = "async-signal" 139 | version = "0.2.10" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" 142 | dependencies = [ 143 | "async-io", 144 | "async-lock", 145 | "atomic-waker", 146 | "cfg-if", 147 | "futures-core", 148 | "futures-io", 149 | "rustix", 150 | "signal-hook-registry", 151 | "slab", 152 | "windows-sys", 153 | ] 154 | 155 | [[package]] 156 | name = "async-std" 157 | version = "1.13.1" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" 160 | dependencies = [ 161 | "async-channel 1.9.0", 162 | "async-global-executor", 163 | "async-io", 164 | "async-lock", 165 | "async-process", 166 | "crossbeam-utils", 167 | "futures-channel", 168 | "futures-core", 169 | "futures-io", 170 | "futures-lite", 171 | "gloo-timers", 172 | "kv-log-macro", 173 | "log", 174 | "memchr", 175 | "once_cell", 176 | "pin-project-lite", 177 | "pin-utils", 178 | "slab", 179 | "wasm-bindgen-futures", 180 | ] 181 | 182 | [[package]] 183 | name = "async-task" 184 | version = "4.7.1" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 187 | 188 | [[package]] 189 | name = "atomic-waker" 190 | version = "1.1.2" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 193 | 194 | [[package]] 195 | name = "autocfg" 196 | version = "1.4.0" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 199 | 200 | [[package]] 201 | name = "backtrace" 202 | version = "0.3.74" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 205 | dependencies = [ 206 | "addr2line", 207 | "cfg-if", 208 | "libc", 209 | "miniz_oxide", 210 | "object", 211 | "rustc-demangle", 212 | "windows-targets", 213 | ] 214 | 215 | [[package]] 216 | name = "bitflags" 217 | version = "2.9.0" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 220 | 221 | [[package]] 222 | name = "blocking" 223 | version = "1.6.1" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" 226 | dependencies = [ 227 | "async-channel 2.3.1", 228 | "async-task", 229 | "futures-io", 230 | "futures-lite", 231 | "piper", 232 | ] 233 | 234 | [[package]] 235 | name = "bumpalo" 236 | version = "3.17.0" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 239 | 240 | [[package]] 241 | name = "cfg-if" 242 | version = "1.0.0" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 245 | 246 | [[package]] 247 | name = "concurrent-queue" 248 | version = "2.5.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 251 | dependencies = [ 252 | "crossbeam-utils", 253 | ] 254 | 255 | [[package]] 256 | name = "crossbeam-utils" 257 | version = "0.8.21" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 260 | 261 | [[package]] 262 | name = "errno" 263 | version = "0.3.10" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 266 | dependencies = [ 267 | "libc", 268 | "windows-sys", 269 | ] 270 | 271 | [[package]] 272 | name = "event-listener" 273 | version = "2.5.3" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 276 | 277 | [[package]] 278 | name = "event-listener" 279 | version = "5.4.0" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" 282 | dependencies = [ 283 | "concurrent-queue", 284 | "parking", 285 | "pin-project-lite", 286 | ] 287 | 288 | [[package]] 289 | name = "event-listener-strategy" 290 | version = "0.5.4" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" 293 | dependencies = [ 294 | "event-listener 5.4.0", 295 | "pin-project-lite", 296 | ] 297 | 298 | [[package]] 299 | name = "fastrand" 300 | version = "2.3.0" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 303 | 304 | [[package]] 305 | name = "futures-channel" 306 | version = "0.3.31" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 309 | dependencies = [ 310 | "futures-core", 311 | ] 312 | 313 | [[package]] 314 | name = "futures-core" 315 | version = "0.3.31" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 318 | 319 | [[package]] 320 | name = "futures-io" 321 | version = "0.3.31" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 324 | 325 | [[package]] 326 | name = "futures-lite" 327 | version = "2.6.0" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" 330 | dependencies = [ 331 | "fastrand", 332 | "futures-core", 333 | "futures-io", 334 | "parking", 335 | "pin-project-lite", 336 | ] 337 | 338 | [[package]] 339 | name = "gimli" 340 | version = "0.31.1" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 343 | 344 | [[package]] 345 | name = "gloo-timers" 346 | version = "0.3.0" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" 349 | dependencies = [ 350 | "futures-channel", 351 | "futures-core", 352 | "js-sys", 353 | "wasm-bindgen", 354 | ] 355 | 356 | [[package]] 357 | name = "hermit-abi" 358 | version = "0.4.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" 361 | 362 | [[package]] 363 | name = "itoa" 364 | version = "1.0.15" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 367 | 368 | [[package]] 369 | name = "js-sys" 370 | version = "0.3.77" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 373 | dependencies = [ 374 | "once_cell", 375 | "wasm-bindgen", 376 | ] 377 | 378 | [[package]] 379 | name = "kv-log-macro" 380 | version = "1.0.7" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" 383 | dependencies = [ 384 | "log", 385 | ] 386 | 387 | [[package]] 388 | name = "libc" 389 | version = "0.2.171" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 392 | 393 | [[package]] 394 | name = "linux-raw-sys" 395 | version = "0.4.15" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 398 | 399 | [[package]] 400 | name = "log" 401 | version = "0.4.27" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 404 | dependencies = [ 405 | "value-bag", 406 | ] 407 | 408 | [[package]] 409 | name = "memchr" 410 | version = "2.7.4" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 413 | 414 | [[package]] 415 | name = "miniz_oxide" 416 | version = "0.8.5" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" 419 | dependencies = [ 420 | "adler2", 421 | ] 422 | 423 | [[package]] 424 | name = "object" 425 | version = "0.36.7" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 428 | dependencies = [ 429 | "memchr", 430 | ] 431 | 432 | [[package]] 433 | name = "once_cell" 434 | version = "1.21.3" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 437 | 438 | [[package]] 439 | name = "parking" 440 | version = "2.2.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" 443 | 444 | [[package]] 445 | name = "pin-project-lite" 446 | version = "0.2.16" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 449 | 450 | [[package]] 451 | name = "pin-utils" 452 | version = "0.1.0" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 455 | 456 | [[package]] 457 | name = "piper" 458 | version = "0.2.4" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 461 | dependencies = [ 462 | "atomic-waker", 463 | "fastrand", 464 | "futures-io", 465 | ] 466 | 467 | [[package]] 468 | name = "polling" 469 | version = "3.7.4" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" 472 | dependencies = [ 473 | "cfg-if", 474 | "concurrent-queue", 475 | "hermit-abi", 476 | "pin-project-lite", 477 | "rustix", 478 | "tracing", 479 | "windows-sys", 480 | ] 481 | 482 | [[package]] 483 | name = "proc-macro2" 484 | version = "1.0.94" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 487 | dependencies = [ 488 | "unicode-ident", 489 | ] 490 | 491 | [[package]] 492 | name = "quote" 493 | version = "1.0.40" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 496 | dependencies = [ 497 | "proc-macro2", 498 | ] 499 | 500 | [[package]] 501 | name = "rustc-demangle" 502 | version = "0.1.24" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 505 | 506 | [[package]] 507 | name = "rustix" 508 | version = "0.38.44" 509 | source = "registry+https://github.com/rust-lang/crates.io-index" 510 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 511 | dependencies = [ 512 | "bitflags", 513 | "errno", 514 | "libc", 515 | "linux-raw-sys", 516 | "windows-sys", 517 | ] 518 | 519 | [[package]] 520 | name = "rustversion" 521 | version = "1.0.20" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 524 | 525 | [[package]] 526 | name = "ryu" 527 | version = "1.0.20" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 530 | 531 | [[package]] 532 | name = "serde" 533 | version = "1.0.219" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 536 | dependencies = [ 537 | "serde_derive", 538 | ] 539 | 540 | [[package]] 541 | name = "serde_derive" 542 | version = "1.0.219" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 545 | dependencies = [ 546 | "proc-macro2", 547 | "quote", 548 | "syn", 549 | ] 550 | 551 | [[package]] 552 | name = "serde_json" 553 | version = "1.0.140" 554 | source = "registry+https://github.com/rust-lang/crates.io-index" 555 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 556 | dependencies = [ 557 | "itoa", 558 | "memchr", 559 | "ryu", 560 | "serde", 561 | ] 562 | 563 | [[package]] 564 | name = "signal-hook-registry" 565 | version = "1.4.2" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 568 | dependencies = [ 569 | "libc", 570 | ] 571 | 572 | [[package]] 573 | name = "slab" 574 | version = "0.4.9" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 577 | dependencies = [ 578 | "autocfg", 579 | ] 580 | 581 | [[package]] 582 | name = "syn" 583 | version = "2.0.100" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" 586 | dependencies = [ 587 | "proc-macro2", 588 | "quote", 589 | "unicode-ident", 590 | ] 591 | 592 | [[package]] 593 | name = "tokio" 594 | version = "1.44.1" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" 597 | dependencies = [ 598 | "backtrace", 599 | "pin-project-lite", 600 | ] 601 | 602 | [[package]] 603 | name = "tracing" 604 | version = "0.1.41" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 607 | dependencies = [ 608 | "pin-project-lite", 609 | "tracing-core", 610 | ] 611 | 612 | [[package]] 613 | name = "tracing-core" 614 | version = "0.1.33" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 617 | 618 | [[package]] 619 | name = "unicode-ident" 620 | version = "1.0.18" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 623 | 624 | [[package]] 625 | name = "value-bag" 626 | version = "1.11.1" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" 629 | 630 | [[package]] 631 | name = "wasm-bindgen" 632 | version = "0.2.100" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 635 | dependencies = [ 636 | "cfg-if", 637 | "once_cell", 638 | "rustversion", 639 | "wasm-bindgen-macro", 640 | ] 641 | 642 | [[package]] 643 | name = "wasm-bindgen-backend" 644 | version = "0.2.100" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 647 | dependencies = [ 648 | "bumpalo", 649 | "log", 650 | "proc-macro2", 651 | "quote", 652 | "syn", 653 | "wasm-bindgen-shared", 654 | ] 655 | 656 | [[package]] 657 | name = "wasm-bindgen-futures" 658 | version = "0.4.50" 659 | source = "registry+https://github.com/rust-lang/crates.io-index" 660 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 661 | dependencies = [ 662 | "cfg-if", 663 | "js-sys", 664 | "once_cell", 665 | "wasm-bindgen", 666 | "web-sys", 667 | ] 668 | 669 | [[package]] 670 | name = "wasm-bindgen-macro" 671 | version = "0.2.100" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 674 | dependencies = [ 675 | "quote", 676 | "wasm-bindgen-macro-support", 677 | ] 678 | 679 | [[package]] 680 | name = "wasm-bindgen-macro-support" 681 | version = "0.2.100" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 684 | dependencies = [ 685 | "proc-macro2", 686 | "quote", 687 | "syn", 688 | "wasm-bindgen-backend", 689 | "wasm-bindgen-shared", 690 | ] 691 | 692 | [[package]] 693 | name = "wasm-bindgen-shared" 694 | version = "0.2.100" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 697 | dependencies = [ 698 | "unicode-ident", 699 | ] 700 | 701 | [[package]] 702 | name = "web-sys" 703 | version = "0.3.77" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 706 | dependencies = [ 707 | "js-sys", 708 | "wasm-bindgen", 709 | ] 710 | 711 | [[package]] 712 | name = "windows-sys" 713 | version = "0.59.0" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 716 | dependencies = [ 717 | "windows-targets", 718 | ] 719 | 720 | [[package]] 721 | name = "windows-targets" 722 | version = "0.52.6" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 725 | dependencies = [ 726 | "windows_aarch64_gnullvm", 727 | "windows_aarch64_msvc", 728 | "windows_i686_gnu", 729 | "windows_i686_gnullvm", 730 | "windows_i686_msvc", 731 | "windows_x86_64_gnu", 732 | "windows_x86_64_gnullvm", 733 | "windows_x86_64_msvc", 734 | ] 735 | 736 | [[package]] 737 | name = "windows_aarch64_gnullvm" 738 | version = "0.52.6" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 741 | 742 | [[package]] 743 | name = "windows_aarch64_msvc" 744 | version = "0.52.6" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 747 | 748 | [[package]] 749 | name = "windows_i686_gnu" 750 | version = "0.52.6" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 753 | 754 | [[package]] 755 | name = "windows_i686_gnullvm" 756 | version = "0.52.6" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 759 | 760 | [[package]] 761 | name = "windows_i686_msvc" 762 | version = "0.52.6" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 765 | 766 | [[package]] 767 | name = "windows_x86_64_gnu" 768 | version = "0.52.6" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 771 | 772 | [[package]] 773 | name = "windows_x86_64_gnullvm" 774 | version = "0.52.6" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 777 | 778 | [[package]] 779 | name = "windows_x86_64_msvc" 780 | version = "0.52.6" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 783 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-chat" 3 | version = "0.1.0" 4 | edition = "2024" 5 | authors = ["Christian yemele "] 6 | 7 | [dependencies] 8 | async-std = { version = "1.7", features = ["unstable"] } 9 | tokio = { version = "1.0", features = ["sync"] } 10 | serde = { version = "1.0", features = ["derive", "rc"] } 11 | serde_json = "1.0" 12 | anyhow = "1.0.97" 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Async Chat 2 | 3 | A real-time asynchronous chat library built in Rust that enables WhatsApp-like group communication functionality. 4 | 5 | ## Overview 6 | 7 | Async Chat is a robust chat system that allows multiple clients to communicate with each other through a central server. The system is built using Rust's async capabilities, providing efficient and scalable group-based communication. 8 | 9 | Below is a high-level overview of the system: 10 | ![Async Rust Chat](docs/images/mvp.jpeg) 11 | 12 | ## Existing Features 13 | 14 | - Asynchronous communication using Rust's async/await 15 | - Group-based chat system 16 | - Multiple client support 17 | - Real-time message delivery 18 | 19 | ## Planned Features 20 | - Secure groups with passwords 21 | - Group creation and management 22 | - Secure message handling 23 | - WASM (WebAssembly) support 24 | 25 | ## Prerequisites 26 | 27 | - Cargo package manager 28 | 29 | ## Dependencies 30 | 31 | - async-std (1.7) - Async runtime with unstable features 32 | - tokio (1.0) - Async runtime with synchronization features 33 | - serde (1.0) - Serialization framework 34 | - serde_json (1.0) - JSON serialization support 35 | - anyhow (1.0.97) - Error handling 36 | 37 | ## Installation 38 | 39 | Clone the repository using any of the methods below: 40 | 41 | - **Using SSH (recommended for developers):** 42 | ```bash 43 | git clone git@github.com:Rust-Cameroon/async-chat.git 44 | ``` 45 | 46 | - **Using HTTPS (easier for beginners):** 47 | ```bash 48 | git clone https://github.com/Rust-Cameroon/async-chat.git 49 | ``` 50 | 51 | ## Usage 52 | 53 | 1. Start the server: 54 | ```bash 55 | cargo run --release --bin server -- localhost:8000 56 | ``` 57 | 58 | 2. Start a client: 59 | ```bash 60 | cargo run --release --bin client -- localhost:8000 61 | ``` 62 | 63 | ### Basic Workflow 64 | 65 | 1. Start server 66 | 2. Clients connect to the server 67 | 3. Create and join a group on the server 68 | 4. Start sending messages within the group 69 | 70 | 71 | ## Contributing 72 | 73 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. 74 | -------------------------------------------------------------------------------- /ci-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e # Exit on error 4 | set -o pipefail # Fail pipeline if any command fails 5 | 6 | echo "Running Local CI Pipeline..." 7 | 8 | # Set environment variables 9 | export CARGO_TERM_COLOR=always 10 | export RUSTFLAGS="-D warnings" 11 | 12 | # Check if Cargo is installed 13 | if ! command -v cargo &> /dev/null; then 14 | echo "Cargo is not installed. Please install Rust." 15 | exit 1 16 | fi 17 | 18 | # Ensure cargo-audit is installed 19 | if ! command -v cargo-audit &> /dev/null; then 20 | echo "Installing cargo-audit..." 21 | cargo install cargo-audit 22 | fi 23 | 24 | # Ensure cargo-nextest is installed 25 | if ! command -v cargo-nextest &> /dev/null; then 26 | echo "Installing cargo-nextest..." 27 | cargo install cargo-nextest --locked 28 | fi 29 | 30 | # Run all CI steps locally 31 | echo "Checking Formatting..." 32 | cargo fmt --all -- --check || (echo "Formatting issues found, please run 'cargo fmt' to auto-fix." && exit 1) 33 | 34 | echo "Building Project..." 35 | cargo build --workspace --all-features 36 | 37 | echo "Running Tests..." 38 | cargo nextest run --workspace --all-targets --all-features --no-fail-fast || echo "No tests found, skipping..." 39 | 40 | echo "Running Lints Checks..." 41 | cargo clippy --workspace --all-targets --all-features -- -D warnings 42 | 43 | echo "Checking API Documentation..." 44 | cargo doc --workspace --all-features --no-deps 45 | 46 | echo "Running Security Audit..." 47 | cargo audit 48 | 49 | echo "CI pipeline completed successfully!" 50 | -------------------------------------------------------------------------------- /docs/broadcast.md: -------------------------------------------------------------------------------- 1 | # Broadcast channels In Async Chat Server 2 | 3 | ## Introduction 4 | 5 | Broadcasting is a means of communication whereby information is transferred from a single or multiple producers to one or more consumers or receivers. Here , Broadcast channels are the channels through which broadcasting occurs. 6 | 7 | 8 | ## Why broadcasting 9 | 10 | - ### Point to Point 11 | 12 | Although point-to-point communication allows multiple users to send messages, it only supports a single receiver. On the other hand, broadcasting works with SPMC (single producer, multiple consumers) channels, allowing multiple consumers to receive messages simultaneously 13 | 14 | - ### Unicast 15 | 16 | Here, the messages are sent from one person to another. This is not very efficient because if someone wants to send the same message to 10 people, they would need to send the message one by one, which is not ideal. Broadcasting would be preferred here, as it would make message sending easier and more efficient. 17 | 18 | - ### Multicast 19 | Multicasting is very difficult and complex to built than broadcasting .. Even though both send messages from one producer to multiple consumers, there are many aspects to handle when using multicasting that introduce complexity, while broadcasting is simpler and less complex. 20 | 21 | ## Advantages 22 | 23 | Some of the advantages of broadcasting are 24 | 25 | - ### Efficiency 26 | A broadcast channel is not only simple to implement but also highly efficient. It enables the transfer of messages to multiple receivers simultaneously, making it ideal for chat servers with a large number of users. 27 | 28 | - ### Data Isolation 29 | When using a broadcast channel, each user receives their own copy of the data, preventing shared access that could lead to data races. 30 | 31 | - ### Concurrency Management 32 | Even though broadcast channels work with MPSC (Multiple Producer, Single Consumer), they allow multiple users to send messages by cloning the user. This reduces potential issues that may arise if two users attempt to send messages simultaneously. 33 | 34 | - ### Offline Message Retrieval 35 | Using broadcasting allows the chat server to store built-in messages, enabling users to retrieve them if they were offline. 36 | 37 | 38 | ## Conclusion 39 | In conclusion, using a broadcasting channel is certainly the best choice for this chat server, as it provides efficiency, fast message retrieval, and safety. -------------------------------------------------------------------------------- /docs/images/mvp.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rust-Cameroon/async-chat/c7f01440fd594d22b290d2ac0e18eb06a1c894ca/docs/images/mvp.jpeg -------------------------------------------------------------------------------- /docs/run-ci-locally.md: -------------------------------------------------------------------------------- 1 | # Running the Local CI Pipeline Script 2 | 3 | Follow these steps to run the CI pipeline locally using the `ci-local.sh` script: 4 | 5 | ## Prerequisites 6 | Ensure you have the following installed: 7 | - [Rust](https://www.rust-lang.org/tools/install) (with `cargo` command available) 8 | - [cargo-audit](https://github.com/RustSec/cargo-audit) (for security checks) 9 | 10 | ## Steps to Run the Script 11 | 12 | 1. **Clone the Repository** (if not already cloned): 13 | ```bash 14 | git clone 15 | cd async-chat 16 | ``` 17 | 2. **Make the Script Executable** (if not already executable): 18 | ```bash 19 | chmod +x ci-local.sh 20 | ``` 21 | 3. **Run the CI Script Locally:** 22 | ```bash 23 | ./ci-local.sh 24 | ``` 25 | This will execute the following steps: 26 | 27 | - Check if formatting issues exist (`cargo fmt --check`) 28 | 29 | - Build the project (`cargo build`) 30 | 31 | - Run tests (`cargo nextest run`) 32 | 33 | - Perform lint checks (`cargo clippy`) 34 | 35 | - Check API documentation (`cargo doc`) 36 | 37 | - Run a security audit (`cargo audit`) 38 | 39 | If any issues are detected (e.g., formatting errors), you can fix them by running: 40 | ```bash 41 | cargo fmt --all 42 | ``` 43 | After fixing the issues, re-run the script to ensure everything passes. -------------------------------------------------------------------------------- /src/bin/client.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused_variables, unused_mut)] // Suppresses warnings 2 | 3 | use async_chat::{FromServer, utils}; 4 | use async_std::{io::BufReader, net, prelude::FutureExt, stream::StreamExt, task}; 5 | 6 | /// Client binary for connecting to the async chat server. 7 | /// 8 | /// Expects one argument: the server address and port to connect to. 9 | /// Example usage: `client 127.0.0.1:8080` 10 | fn main() -> anyhow::Result<()> { 11 | let address = std::env::args().nth(1).expect("Usage: client ADDRESS:PORT"); 12 | 13 | task::block_on(async { 14 | let socket = net::TcpStream::connect(address).await?; 15 | socket.set_nodelay(true)?; // Disable Nagle's algorithm for lower latency. 16 | 17 | // Race two futures: sending commands vs. receiving server. 18 | let to_server = send_commands(socket.clone()); 19 | let from_server = handle_replies(socket); 20 | 21 | from_server.race(to_server).await?; 22 | Ok(()) 23 | }) 24 | } 25 | 26 | /// Reads user input (planned via `clap`) and sends commands to the server. 27 | async fn send_commands(_to_server: net::TcpStream) -> anyhow::Result<()> { 28 | // TODO: Implement use clap to parse command line arguments and print help message 29 | todo!() 30 | } 31 | /// Handles responses from the server and prints them to stdout as they arrive. 32 | async fn handle_replies(from_server: net::TcpStream) -> anyhow::Result<()> { 33 | let buffered = BufReader::new(from_server); 34 | let mut reply_stream = utils::receive_as_json(buffered); 35 | 36 | while let Some(reply) = reply_stream.next().await { 37 | let reply = reply?; 38 | match reply { 39 | FromServer::Message { 40 | group_name, 41 | message, 42 | } => { 43 | println!("message posted to {}: {}", group_name, message); 44 | } 45 | FromServer::Error(error) => { 46 | eprintln!("Error: {}", error); 47 | } 48 | } 49 | } 50 | 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /src/bin/server/connection.rs: -------------------------------------------------------------------------------- 1 | use crate::group_table::GroupTable; 2 | use async_chat::utils::{self}; 3 | use async_chat::{FromClient, FromServer}; 4 | use async_std::io::BufReader; 5 | use async_std::net::TcpStream; 6 | use async_std::prelude::*; 7 | use async_std::sync::Arc; 8 | use async_std::sync::Mutex; 9 | 10 | /// Represents a thread-safe outbound connection to a client. 11 | /// This struct wraps a `TcpStream` in a `Mutex` to provide a safe and exclusive way to send data to the client. 12 | pub struct Outbound(Mutex); 13 | impl Outbound { 14 | /// Creates a new `Outbound` connection. 15 | /// 16 | /// # Arguments 17 | /// 18 | /// * `to_client` - The TCP stream to write to. 19 | pub fn new(to_client: TcpStream) -> Outbound { 20 | Outbound(Mutex::new(to_client)) 21 | } 22 | /// Sends a message to the connected client in JSON format. 23 | /// 24 | /// # Arguments 25 | /// 26 | /// * `packet` - The message to send, wrapped in the `FromServer` enum. 27 | /// 28 | /// # Errors 29 | /// 30 | /// Returns an error if writing or flushing to the stream fails. 31 | pub async fn send(&self, packet: FromServer) -> anyhow::Result<()> { 32 | let mut guard = self.0.lock().await; 33 | utils::send_as_json(&mut *guard, &packet).await?; 34 | guard.flush().await?; 35 | Ok(()) 36 | } 37 | } 38 | 39 | /// Serves a single client connection by reading messages and interacting with group state. 40 | /// 41 | /// # Arguments 42 | /// 43 | /// * `socket` - The TCP connection to the client. 44 | /// * `groups` - A shared reference to the server's group table. 45 | /// 46 | /// # Errors 47 | /// 48 | /// Returns an error if: 49 | /// - Reading from the socket fails 50 | /// - Sending a message fails 51 | /// - A user tries to post to a group that does not exist 52 | pub async fn serve(socket: TcpStream, groups: Arc) -> anyhow::Result<()> { 53 | // wrapping our connection in outbound so as to have exclusive access to it in the groups and avoid interference 54 | let outbound = Arc::new(Outbound::new(socket.clone())); 55 | let buffered = BufReader::new(socket); 56 | // receive data from clients 57 | let mut from_client = utils::receive_as_json(buffered); 58 | while let Some(request_result) = from_client.next().await { 59 | let request = request_result?; 60 | let result = match request { 61 | FromClient::Join { group_name } => { 62 | let group = groups.get_or_create(group_name); 63 | group.join(outbound.clone()); 64 | Ok(()) 65 | } 66 | FromClient::Post { 67 | group_name, 68 | message, 69 | } => match groups.get(&group_name) { 70 | Some(group) => { 71 | group.post(message); 72 | Ok(()) 73 | } 74 | None => Err(format!("Group '{}' does not exist", group_name)), 75 | }, 76 | }; 77 | // If an error occurred, send an error message back to the client 78 | if let Err(message) = result { 79 | let report = FromServer::Error(message); 80 | // send error back to client 81 | outbound.send(report).await?; 82 | } 83 | } 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /src/bin/server/group.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] // Suppresses warnings about unused code 2 | 3 | use crate::connection::Outbound; 4 | use async_std::task; 5 | use std::sync::Arc; 6 | use tokio::sync::broadcast; 7 | 8 | /// A named group that broadcasts messages to all connected subscribers. 9 | pub struct Group { 10 | name: Arc, 11 | sender: broadcast::Sender>, 12 | } 13 | 14 | impl Group { 15 | /// Creates a new `Group` with a given name. 16 | /// 17 | /// # Arguments 18 | /// 19 | /// * `name` - The name of the group. 20 | pub fn new(name: Arc) -> Group { 21 | let (sender, _receiver) = broadcast::channel(1000); // buffer size of 1000 messages 22 | Group { name, sender } 23 | } 24 | /// Adds a client connection to the group and starts sending messages to it. 25 | /// 26 | /// # Arguments 27 | /// 28 | /// * `outbound` - The client connection to receive messages. 29 | /// 30 | /// This function spawns a background task to handle receiving messages from the 31 | /// broadcast channel and forwarding them to the client. A task is used so that 32 | /// the message receiving loop can run asynchronously without blocking the caller. 33 | pub fn join(&self, outbound: Arc) { 34 | let receiver = self.sender.subscribe(); 35 | task::spawn(handle_subscriber(self.name.clone(), receiver, outbound)); 36 | } 37 | /// Posts a message to the group, broadcasting it to all subscribers. 38 | /// 39 | /// # Arguments 40 | /// 41 | /// * `message` - The message to broadcast. 42 | pub fn post(&self, message: Arc) { 43 | let _ = self.sender.send(message); // Ignoring the result to suppress warning 44 | } 45 | } 46 | 47 | /// Handles the lifecycle of a subscriber: receiving messages and sending them over their connection. 48 | /// 49 | /// This is a stub — should be implemented to read from the `receiver` and forward messages to `outbound`. 50 | async fn handle_subscriber( 51 | _group_name: Arc, 52 | _receiver: broadcast::Receiver>, 53 | _outbound: Arc, 54 | ) { 55 | todo!() 56 | } 57 | -------------------------------------------------------------------------------- /src/bin/server/group_table.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::new_without_default)] // Suppresses Clippy warning 2 | 3 | use crate::group::Group; 4 | use std::collections::HashMap; 5 | use std::sync::{Arc, Mutex}; 6 | 7 | /// A thread-safe table that stores all active chat groups by name. 8 | /// 9 | /// Internally wraps a `HashMap, Arc>` in a `Mutex` for safe concurrent access. 10 | pub struct GroupTable(Mutex, Arc>>); 11 | 12 | impl GroupTable { 13 | /// Creates a new, empty `GroupTable`. 14 | pub fn new() -> GroupTable { 15 | GroupTable(Mutex::new(HashMap::new())) 16 | } 17 | 18 | /// Retrieves a group by name, if it exists. 19 | /// 20 | /// # Arguments 21 | /// 22 | /// * `name` - The name of the group to retrieve. 23 | /// 24 | /// # Returns 25 | /// 26 | /// An `Option` containing the group, or `None` if it doesn't exist. 27 | pub fn get(&self, name: &String) -> Option> { 28 | self.0.lock().unwrap().get(name).cloned() 29 | } 30 | 31 | pub fn get_or_create(&self, name: Arc) -> Arc { 32 | self.0 33 | .lock() 34 | .unwrap() 35 | .entry(name.clone()) 36 | .or_insert_with(|| Arc::new(Group::new(name))) 37 | .clone() 38 | } 39 | } 40 | 41 | // Implement Default to satisfy Clippy's `new_without_default` lint 42 | impl Default for GroupTable { 43 | fn default() -> Self { 44 | Self::new() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/bin/server/main.rs: -------------------------------------------------------------------------------- 1 | pub mod connection; 2 | pub mod group; 3 | pub mod group_table; 4 | 5 | use connection::serve; 6 | 7 | use async_std::net::TcpListener; 8 | use async_std::prelude::*; 9 | use async_std::task; 10 | use std::sync::Arc; 11 | 12 | /// The main entry point for the async-chat server. 13 | /// 14 | /// Accepts incoming TCP connections and spawns a task to handle each. 15 | /// Expects one argument: the address to bind to (e.g., `127.0.0.1:8080`) 16 | fn main() -> anyhow::Result<()> { 17 | let address = std::env::args().nth(1).expect( 18 | "Usage: server 19 | ADDRESS", 20 | ); 21 | // A thread-safe table that stores all active chat groups by name. 22 | let chat_group_table = Arc::new(group_table::GroupTable::new()); 23 | async_std::task::block_on(async { 24 | let listener = TcpListener::bind(address).await?; 25 | let mut new_connections = listener.incoming(); 26 | // Accept incoming connections and spawn an asynchronous task to handle each 27 | while let Some(socket_result) = new_connections.next().await { 28 | let socket = socket_result?; 29 | let groups = chat_group_table.clone(); 30 | task::spawn(async { 31 | log_error(serve(socket, groups).await); 32 | }); 33 | } 34 | Ok(()) 35 | }) 36 | } 37 | 38 | /// Logs errors from client handler tasks. 39 | fn log_error(result: anyhow::Result<()>) { 40 | if let Err(error) = result { 41 | eprintln!("Error: {}", error); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # async-chat 2 | //! 3 | //! A simple async group chat system implemented in Rust, using `async-std` for concurrency. 4 | //! This crate defines the message formats and utility functions used by both the client and server. 5 | 6 | use std::sync::Arc; 7 | 8 | use serde::{Deserialize, Serialize}; 9 | pub mod utils; 10 | 11 | /// Messages that clients can send to the server. 12 | #[derive(Debug, Deserialize, Serialize, PartialEq)] 13 | pub enum FromClient { 14 | /// Join a group by name. 15 | Join { group_name: Arc }, 16 | /// Post a message to a group. 17 | Post { 18 | group_name: Arc, 19 | message: Arc, 20 | }, 21 | } 22 | /// Messages that the server sends back to clients. 23 | #[derive(Debug, Deserialize, Serialize)] 24 | pub enum FromServer { 25 | /// A message has been posted to a group. 26 | Message { 27 | group_name: Arc, 28 | message: Arc, 29 | }, 30 | /// The server encountered an error. 31 | Error(String), 32 | } 33 | 34 | #[cfg(test)] 35 | mod test { 36 | use crate::FromClient; 37 | 38 | #[test] 39 | fn test_fromclient_json() { 40 | use std::sync::Arc; 41 | let from_client = FromClient::Post { 42 | group_name: Arc::new("Dogs".to_string()), 43 | message: Arc::new("Samoyeds rock!".to_string()), 44 | }; 45 | let json = serde_json::to_string(&from_client).unwrap(); 46 | assert_eq!( 47 | json, 48 | r#"{"Post":{"group_name":"Dogs","message":"Samoyeds rock!"}}"# 49 | ); 50 | assert_eq!( 51 | serde_json::from_str::(&json).unwrap(), 52 | from_client 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use async_std::prelude::*; 2 | use serde::de::DeserializeOwned; 3 | 4 | /// Sends a serializable packet as a JSON-encoded line over a writable stream. 5 | /// 6 | /// # Arguments 7 | /// * `outbound` - The writable stream to send through. 8 | /// * `packet` - The serializable data to be sent. 9 | pub async fn send_as_json(outbound: &mut S, packet: &P) -> anyhow::Result<()> 10 | where 11 | S: async_std::io::Write + Unpin, 12 | P: serde::Serialize, 13 | { 14 | let mut json = serde_json::to_string(&packet)?; 15 | json.push('\n'); 16 | outbound.write_all(json.as_bytes()).await?; 17 | Ok(()) 18 | } 19 | 20 | /// Returns a stream of deserialized packets from a buffered input stream. 21 | /// 22 | /// # Arguments 23 | /// * `inbound` - A stream of lines containing JSON messages. 24 | /// 25 | /// # Returns 26 | /// A stream of parsed packets of type `P`. 27 | pub fn receive_as_json(inbound: S) -> impl Stream> 28 | where 29 | S: async_std::io::BufRead + Unpin, 30 | P: DeserializeOwned, 31 | { 32 | inbound.lines().map(|lines_result| -> anyhow::Result

{ 33 | let line = lines_result?; 34 | let parsed = serde_json::from_str::

(&line)?; 35 | Ok(parsed) 36 | }) 37 | } 38 | --------------------------------------------------------------------------------