diff --git a/.github/workflows/docker-cleanup.yaml b/.github/workflows/docker-cleanup.yaml new file mode 100644 index 00000000..83061949 --- /dev/null +++ b/.github/workflows/docker-cleanup.yaml @@ -0,0 +1,46 @@ +name: Docker PR Image Cleanup + +on: + pull_request: + types: + - closed + +permissions: + contents: read + packages: write + +concurrency: + group: docker-cleanup-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + cleanup: + runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.head.repo.fork }} + strategy: + fail-fast: false + matrix: + package: + - base + - api + - events + - file-server + - proxy + - gifbox + - crond + - pushd + - voice-ingress + steps: + - env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ORG: stoatchat + PACKAGE: ${{ matrix.package }} + TAG: pr-${{ github.event.pull_request.number }} + run: | + set -euo pipefail + gh api --paginate \ + "/orgs/${ORG}/packages/container/${PACKAGE}/versions" \ + --jq ".[] | select(.metadata.container.tags | index(\"${TAG}\")) | .id" \ + | while read -r id; do + gh api -X DELETE "/orgs/${ORG}/packages/container/${PACKAGE}/versions/${id}" + done diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 2be1f438..75aefeb6 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -5,8 +5,6 @@ on: tags: - "*" pull_request: - paths: - - "Dockerfile" workflow_dispatch: permissions: @@ -19,9 +17,9 @@ concurrency: jobs: base: - name: Test base image build + name: Test base image build (fork) runs-on: arc-runner-set - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork steps: # Configure build environment - name: Checkout @@ -42,7 +40,7 @@ jobs: publish: runs-on: arc-runner-set - if: github.event_name != 'pull_request' + if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }} name: Publish Docker images steps: # Configure build environment @@ -59,6 +57,15 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Determine base image tag + id: base + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + echo "tag=pr-${{ github.event.number }}" >> "$GITHUB_OUTPUT" + else + echo "tag=latest" >> "$GITHUB_OUTPUT" + fi + # Build the image - name: Build base image uses: docker/build-push-action@v4 @@ -66,7 +73,9 @@ jobs: context: . push: true platforms: linux/amd64,linux/arm64 - tags: ghcr.io/${{ github.repository_owner }}/base:latest + tags: ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} + cache-from: type=gha,scope=buildx-base-multi-arch + cache-to: type=gha,scope=buildx-base-multi-arch,mode=max # stoatchat/api - name: Docker meta @@ -84,7 +93,7 @@ jobs: file: crates/delta/Dockerfile tags: ${{ steps.meta-delta.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-delta.outputs.labels }} # stoatchat/events @@ -103,7 +112,7 @@ jobs: file: crates/bonfire/Dockerfile tags: ${{ steps.meta-bonfire.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-bonfire.outputs.labels }} # stoatchat/file-server @@ -122,7 +131,7 @@ jobs: file: crates/services/autumn/Dockerfile tags: ${{ steps.meta-autumn.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-autumn.outputs.labels }} # stoatchat/proxy @@ -141,7 +150,7 @@ jobs: file: crates/services/january/Dockerfile tags: ${{ steps.meta-january.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-january.outputs.labels }} # stoatchat/gifbox @@ -160,7 +169,7 @@ jobs: file: crates/services/gifbox/Dockerfile tags: ${{ steps.meta-gifbox.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-gifbox.outputs.labels }} # stoatchat/crond @@ -179,7 +188,7 @@ jobs: file: crates/daemons/crond/Dockerfile tags: ${{ steps.meta-crond.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-crond.outputs.labels }} # stoatchat/pushd @@ -198,7 +207,7 @@ jobs: file: crates/daemons/pushd/Dockerfile tags: ${{ steps.meta-pushd.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-pushd.outputs.labels }} # stoatchat/voice-ingress @@ -217,5 +226,5 @@ jobs: file: crates/daemons/voice-ingress/Dockerfile tags: ${{ steps.meta-voice-ingress.outputs.tags }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:latest + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }} labels: ${{ steps.meta-voice-ingress.outputs.labels }} diff --git a/.github/workflows/publish-crates.yml b/.github/workflows/publish-crates.yml index ce02d841..5e35d358 100644 --- a/.github/workflows/publish-crates.yml +++ b/.github/workflows/publish-crates.yml @@ -21,4 +21,6 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} run: mise publish --workspace diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml index 08e210a3..3b7a78d2 100644 --- a/.github/workflows/rust.yaml +++ b/.github/workflows/rust.yaml @@ -37,6 +37,7 @@ jobs: - name: Reference Test env: TEST_DB: REFERENCE + continue-on-error: ${{ github.ref_name == 'main' }} run: | mise test @@ -44,6 +45,7 @@ jobs: env: TEST_DB: MONGODB MONGODB: mongodb://localhost + continue-on-error: ${{ github.ref_name == 'main' }} run: | mise test diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8950f712..29781928 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.12.1" + ".": "0.13.7" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cc7f7328..9912f5b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,97 @@ # Changelog +## [0.13.7](https://github.com/stoatchat/stoatchat/compare/v0.13.6...v0.13.7) (2026-05-21) + + +### Bug Fixes + +* sanitize emoji input to handle variation selectors ([#774](https://github.com/stoatchat/stoatchat/issues/774)) ([2d308e0](https://github.com/stoatchat/stoatchat/commit/2d308e03d58c19f27b5b4d65dc2a15ef20b56190)) +* update mention count badge for channel acks ([#769](https://github.com/stoatchat/stoatchat/issues/769)) ([0d9ae50](https://github.com/stoatchat/stoatchat/commit/0d9ae508d9d2199f0e408b8ca634d20489be6f61)) + +## [0.13.6](https://github.com/stoatchat/stoatchat/compare/v0.13.5...v0.13.6) (2026-05-18) + + +### Features + +* Update FCM payload for android notifications ([#766](https://github.com/stoatchat/stoatchat/issues/766)) ([acbc087](https://github.com/stoatchat/stoatchat/commit/acbc087982e9aeb05cabc5ab4c9b1291f67490ad)) +* user slowmode events ([#760](https://github.com/stoatchat/stoatchat/issues/760)) ([af0d8aa](https://github.com/stoatchat/stoatchat/commit/af0d8aad14dc68d88159d0e1c714077d362e21e4)) + + +### Bug Fixes + +* include `minio` region as tests need it ([#761](https://github.com/stoatchat/stoatchat/issues/761)) ([298742d](https://github.com/stoatchat/stoatchat/commit/298742dbad4eafae356f976c56b9db23904b0c3a)) +* set env var for publishing crates ([#768](https://github.com/stoatchat/stoatchat/issues/768)) ([018afaf](https://github.com/stoatchat/stoatchat/commit/018afaf38f6330d92dad2a68b640c0cb3f6b639a)) +* Use proper headers to determine IP when not behind cloudflare ([#764](https://github.com/stoatchat/stoatchat/issues/764)) ([494c8b7](https://github.com/stoatchat/stoatchat/commit/494c8b7cabaae2a51039a7a5b559d5e2e5279554)) +* voice ingress crashing due to new Result in AMQP::new_auto() ([#765](https://github.com/stoatchat/stoatchat/issues/765)) ([2871632](https://github.com/stoatchat/stoatchat/commit/2871632382395cb20cbe0047c542d3ac31ff3f03)) + + +### Miscellaneous Chores + +* switch to lapin ([#767](https://github.com/stoatchat/stoatchat/issues/767)) ([5b19853](https://github.com/stoatchat/stoatchat/commit/5b1985381ae829a92c80a19e91a414cd9dc4de93)) + +## [0.13.5](https://github.com/stoatchat/stoatchat/compare/v0.13.4...v0.13.5) (2026-05-17) + + +### Bug Fixes + +* dont panic on hash missing when deleting files ([#755](https://github.com/stoatchat/stoatchat/issues/755)) ([c902077](https://github.com/stoatchat/stoatchat/commit/c902077cf51076fee11712eb732dc8a8f786fc4b)) + +## [0.13.4](https://github.com/stoatchat/stoatchat/compare/v0.13.3...v0.13.4) (2026-05-16) + + +### Bug Fixes + +* add TLS feature to livekit-api crate ([#753](https://github.com/stoatchat/stoatchat/issues/753)) ([6cfee1f](https://github.com/stoatchat/stoatchat/commit/6cfee1f601c1e084df7c8f1e7a5e8a560d1dd514)) + +## [0.13.3](https://github.com/stoatchat/stoatchat/compare/v0.13.2...v0.13.3) (2026-05-15) + + +### Bug Fixes + +* don't automatically set up rabbitmq in delta ([#749](https://github.com/stoatchat/stoatchat/issues/749)) ([7647cfc](https://github.com/stoatchat/stoatchat/commit/7647cfc8d93aba99f5faef13eb3d970097540d76)) +* don't declare queues which seem to cause the backend to crash in prod ([7647cfc](https://github.com/stoatchat/stoatchat/commit/7647cfc8d93aba99f5faef13eb3d970097540d76)) + +## [0.13.2](https://github.com/stoatchat/stoatchat/compare/v0.13.1...v0.13.2) (2026-05-11) + + +### Bug Fixes + +* update default exchange to `revolt.default` ([#746](https://github.com/stoatchat/stoatchat/issues/746)) ([fcb8091](https://github.com/stoatchat/stoatchat/commit/fcb8091cd7a00d7f26c798daa33aae4b923b2a8b)) + +## [0.13.1](https://github.com/stoatchat/stoatchat/compare/v0.13.0...v0.13.1) (2026-05-10) + + +### Bug Fixes + +* amqprs startup bug ([#744](https://github.com/stoatchat/stoatchat/issues/744)) ([1100eaf](https://github.com/stoatchat/stoatchat/commit/1100eaf46f849f2509ae01ac497556ca33bde778)) + +## [0.13.0](https://github.com/stoatchat/stoatchat/compare/v0.12.1...v0.13.0) (2026-05-08) + + +### Features + +* add embed support for YouTube Shorts ([#734](https://github.com/stoatchat/stoatchat/issues/734)) ([d46c7f7](https://github.com/stoatchat/stoatchat/commit/d46c7f7f3c04524c0639c3e0a122626f8e0b3bf7)) +* add emoji rename endpoint ([#714](https://github.com/stoatchat/stoatchat/issues/714)) ([23ad135](https://github.com/stoatchat/stoatchat/commit/23ad1359834bb7d07a460b8678d6a6ebffc73eb0)) +* add legal links to root payload ([#733](https://github.com/stoatchat/stoatchat/issues/733)) ([21d8201](https://github.com/stoatchat/stoatchat/commit/21d82018cf84ab0fdd10613d254b9562aea8eea3)) +* add role icon support ([#724](https://github.com/stoatchat/stoatchat/issues/724)) ([841985d](https://github.com/stoatchat/stoatchat/commit/841985d3b994df1c6eefab2fc7ecbd77ab22c493)) +* Add webhook endpoints for editing and deleting messages ([#682](https://github.com/stoatchat/stoatchat/issues/682)) ([6f3441c](https://github.com/stoatchat/stoatchat/commit/6f3441cf4acac2a8e6e1bf07a279a153b80f7956)) +* automatically sanitise usernames on create/update ([#689](https://github.com/stoatchat/stoatchat/issues/689)) ([e937697](https://github.com/stoatchat/stoatchat/commit/e93769786c7669485a659ee471630740d3cea702)) +* blacklist private ip ranges and add january domain blocklist ([#731](https://github.com/stoatchat/stoatchat/issues/731)) ([6b41db9](https://github.com/stoatchat/stoatchat/commit/6b41db984bb491b2e58324309cc70d8c14e0b814)) +* Rewrite acks ([#741](https://github.com/stoatchat/stoatchat/issues/741)) ([ab5bd47](https://github.com/stoatchat/stoatchat/commit/ab5bd47a39ee889de0b5ae6e7b560620853daead)) + + +### Bug Fixes + +* add new_user_hours to configuration limits ([#729](https://github.com/stoatchat/stoatchat/issues/729)) ([279f5d5](https://github.com/stoatchat/stoatchat/commit/279f5d5fd7af2df55902c706859ec07f569cdb1e)) +* add reconnection policy to Redis subscriber to prevent ghost state ([#708](https://github.com/stoatchat/stoatchat/issues/708)) ([057f2bb](https://github.com/stoatchat/stoatchat/commit/057f2bb8b359f8b942741a30ff54eeb8fbe3e0b1)) +* docker compose file had personal url in it ([#742](https://github.com/stoatchat/stoatchat/issues/742)) ([0719985](https://github.com/stoatchat/stoatchat/commit/0719985ac5636590f91e6f9ec4b68f3eded70c13)) +* don't strip ICC from exif ([#735](https://github.com/stoatchat/stoatchat/issues/735)) ([d76a711](https://github.com/stoatchat/stoatchat/commit/d76a71141f3e508f6308ba52fa28eaeb56fb3438)) +* dont send notification in fcm ([#721](https://github.com/stoatchat/stoatchat/issues/721)) ([89171e9](https://github.com/stoatchat/stoatchat/commit/89171e9bd0f15711157e78c6eec0fe7b480de93a)) +* encode filenames in redirects ([#737](https://github.com/stoatchat/stoatchat/issues/737)) ([9fd7128](https://github.com/stoatchat/stoatchat/commit/9fd7128f800badbd184baf943d4f799e601201e4)) +* january ip redirects & domain resolver ([#738](https://github.com/stoatchat/stoatchat/issues/738)) ([356491e](https://github.com/stoatchat/stoatchat/commit/356491e934b274f9e895df883dd63ef0b3123510)) +* update message length validation to remove upper limit ([#723](https://github.com/stoatchat/stoatchat/issues/723)) ([ed4fd5e](https://github.com/stoatchat/stoatchat/commit/ed4fd5ebfe6d0ea534a0898da4afdc1f4e2cd6c5)) +* use correct response for NoEffect errors ([#732](https://github.com/stoatchat/stoatchat/issues/732)) ([5378cd2](https://github.com/stoatchat/stoatchat/commit/5378cd22b4c7d85f44c31a6af0dda00941b80d5c)) + ## [0.12.1](https://github.com/stoatchat/stoatchat/compare/v0.12.0...v0.12.1) (2026-04-10) diff --git a/Cargo.lock b/Cargo.lock index ce648f79..bcd1c5ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -125,28 +125,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "amqp_serde" -version = "0.4.3" +name = "amq-protocol" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f450f572a1ec4cdb4af7af09cbd0c7c3e1b9da2bfc7414c059a780993a8e16" +checksum = "c5b2e8843d88d935a75cbdb1c5b9ad80987da6e6472c2703914e41dc14560990" dependencies = [ - "bytes 1.11.1", + "amq-protocol-tcp", + "amq-protocol-types", + "amq-protocol-uri", + "cookie-factory", + "nom 8.0.0", "serde", - "serde_bytes", ] [[package]] -name = "amqprs" -version = "1.8.0" +name = "amq-protocol-tcp" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1b4afcbd862e16c272b7625b6b057930b052d63c720bc90f6afab0d9abe8a8" +checksum = "998fb81655e11de5a336bb609042c11633678fc01f90b0772fb9a7886b6cc4c2" dependencies = [ - "amqp_serde", - "async-trait", - "bytes 1.11.1", + "amq-protocol-uri", + "async-rs", + "cfg-if", + "tcp-stream", + "tracing", +] + +[[package]] +name = "amq-protocol-types" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee5b3a9e458bd2e452536995c8cf861b01c17283f55c4bbe0a1b9626b8253add" +dependencies = [ + "cookie-factory", + "nom 8.0.0", "serde", - "serde_bytes_ng", - "tokio 1.51.0", + "serde_json", +] + +[[package]] +name = "amq-protocol-uri" +version = "10.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca3316970d20cdcca9123f4e8feb7a2c1c8fdca572a9692fc10002db35407aa" +dependencies = [ + "amq-protocol-types", + "percent-encoding", + "url", ] [[package]] @@ -211,6 +236,45 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "syn 2.0.117", + "synstructure 0.13.2", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "async-attributes" version = "1.1.2" @@ -244,6 +308,19 @@ dependencies = [ "pin-project-lite 0.2.17", ] +[[package]] +name = "async-compat" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite 0.2.17", + "tokio 1.51.0", +] + [[package]] name = "async-executor" version = "1.14.0" @@ -275,6 +352,20 @@ dependencies = [ "tokio 1.51.0", ] +[[package]] +name = "async-global-executor" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f937e26114b93193065fd44f507aa2e9169ad0cdabbb996920b1fe1ddea7ba" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-lock 3.4.2", + "blocking", + "futures-lite", + "tokio 1.51.0", +] + [[package]] name = "async-io" version = "2.6.0" @@ -288,7 +379,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -328,7 +419,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.1", "futures-lite", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -342,6 +433,22 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "async-rs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e32bd31386d41d0c06bd79b0397ec96e544d69d9dbd6db0236c7ceefe1ad61b" +dependencies = [ + "async-compat", + "async-global-executor 3.1.0", + "async-trait", + "futures-core", + "futures-io", + "hickory-resolver 0.26.1", + "tokio 1.51.0", + "tokio-stream", +] + [[package]] name = "async-signal" version = "0.2.13" @@ -354,7 +461,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -368,7 +475,7 @@ checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ "async-attributes", "async-channel 1.9.0", - "async-global-executor", + "async-global-executor 2.4.1", "async-io", "async-lock 3.4.2", "async-process", @@ -438,7 +545,7 @@ dependencies = [ "futures-util", "log", "pin-project-lite 0.2.17", - "tungstenite", + "tungstenite 0.17.3", ] [[package]] @@ -1130,6 +1237,15 @@ dependencies = [ "ubyte", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand 2.4.0", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -1289,6 +1405,15 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array 0.14.7", +] + [[package]] name = "block2" version = "0.6.2" @@ -1441,6 +1566,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.59" @@ -1482,6 +1616,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -1514,6 +1659,18 @@ dependencies = [ "cc", ] +[[package]] +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", + "x509-cert", +] + [[package]] name = "coarsetime" version = "0.1.37" @@ -1699,6 +1856,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.3.0" @@ -1907,7 +2073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -2128,7 +2294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" dependencies = [ "const-oid 0.6.2", - "der_derive", + "der_derive 0.4.1", ] [[package]] @@ -2149,10 +2315,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid 0.9.6", + "der_derive 0.7.3", + "flagset", "pem-rfc7468 0.7.0", "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der_derive" version = "0.4.1" @@ -2165,6 +2347,17 @@ dependencies = [ "synstructure 0.12.6", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "syn 2.0.117", +] + [[package]] name = "deranged" version = "0.5.8" @@ -2231,6 +2424,15 @@ dependencies = [ "unicode-xid 0.2.6", ] +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + [[package]] name = "devise" version = "0.4.2" @@ -2758,6 +2960,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + [[package]] name = "flate2" version = "1.1.9" @@ -2777,6 +2985,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2824,7 +3043,28 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" dependencies = [ - "foreign-types-shared", + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -2833,6 +3073,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -2973,6 +3219,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "futures-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" +dependencies = [ + "futures-io", + "rustls 0.23.37", + "rustls-pki-types", +] + [[package]] name = "futures-sink" version = "0.3.32" @@ -3096,6 +3353,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -3362,6 +3620,30 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "hickory-proto 0.26.1", + "idna 1.1.0", + "ipnet", + "jni 0.22.4", + "rand 0.10.1", + "thiserror 2.0.18", + "tinyvec", + "tokio 1.51.0", + "tracing", + "url", +] + [[package]] name = "hickory-proto" version = "0.25.2" @@ -3387,6 +3669,26 @@ dependencies = [ "url", ] +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna 1.1.0", + "ipnet", + "jni 0.22.4", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + [[package]] name = "hickory-resolver" version = "0.25.2" @@ -3395,7 +3697,7 @@ checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" dependencies = [ "cfg-if", "futures-util", - "hickory-proto", + "hickory-proto 0.25.2", "ipconfig", "moka", "once_cell", @@ -3408,6 +3710,32 @@ dependencies = [ "tracing", ] +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto 0.26.1", + "ipconfig", + "ipnet", + "jni 0.22.4", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.1", + "resolv-conf", + "smallvec", + "system-configuration 0.7.0", + "thiserror 2.0.18", + "tokio 1.51.0", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -3450,6 +3778,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "hostname" version = "0.3.1" @@ -3968,6 +4305,7 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ + "block-padding", "generic-array 0.14.7", ] @@ -4009,6 +4347,9 @@ name = "ipnet" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] [[package]] name = "iri-string" @@ -4120,6 +4461,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -4451,6 +4822,24 @@ dependencies = [ "log", ] +[[package]] +name = "lapin" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478790661081f7434e111a31953acc1cf23654cf2a2d815d0e12cef9a2aed720" +dependencies = [ + "amq-protocol", + "async-rs", + "async-trait", + "atomic-waker", + "backon", + "cfg-if", + "flume", + "futures-core", + "futures-io", + "tracing", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -4460,6 +4849,29 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "lcms2" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75877b724685dd49310bdbadbf973fc69b1d01992a6d4a861b928fc3943f87b" +dependencies = [ + "bytemuck", + "foreign-types 0.5.0", + "lcms2-sys", +] + +[[package]] +name = "lcms2-sys" +version = "4.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2604b23848ca80b2add60f0fb2270fd980e622c25029b6597fa01cfd5f8d5f" +dependencies = [ + "cc", + "dunce", + "libc", + "pkg-config", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -4576,6 +4988,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4605,11 +5023,14 @@ dependencies = [ "prost", "rand 0.9.2", "reqwest 0.12.28", + "rustls-native-certs 0.6.3", "scopeguard", "serde", "serde_json", "sha2", "thiserror 2.0.18", + "tokio-rustls 0.24.1", + "tokio-tungstenite", "url", ] @@ -4993,8 +5414,8 @@ dependencies = [ "futures-io", "futures-util", "hex", - "hickory-proto", - "hickory-resolver", + "hickory-proto 0.25.2", + "hickory-resolver 0.25.2", "hmac", "macro_magic", "md-5", @@ -5103,6 +5524,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -5456,6 +5883,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -5466,6 +5902,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "opaque-debug" version = "0.3.1" @@ -5480,7 +5922,7 @@ checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.11.0", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -5554,6 +5996,29 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "p12-keystore" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb9bf5222606eb712d3bb30e01bc9420545b00859970897e70c682353a034f2" +dependencies = [ + "base64 0.22.1", + "cbc", + "cms", + "der 0.7.10", + "des", + "hex", + "hmac", + "pkcs12", + "pkcs5", + "rand 0.10.1", + "rc2", + "sha1", + "sha2", + "thiserror 2.0.18", + "x509-parser", +] + [[package]] name = "p256" version = "0.11.1" @@ -5680,6 +6145,107 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest", + "hmac", +] + +[[package]] +name = "pdk-classy" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3e632c61a7f8ad1a77c4f52d9a85a89c577881f44e89b33e28163af38e515" +dependencies = [ + "bincode", + "futures", + "getrandom 0.2.17", + "http 0.2.12", + "log", + "pdk-proxy-wasm-stub", + "protobuf", + "proxy-wasm", + "serde", + "serde_derive", + "thiserror 1.0.69", +] + +[[package]] +name = "pdk-core" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60f2b1d1c8876b54d03d35a18fba1e5172ed8d7f1c379001c11b62c909fdf654" +dependencies = [ + "anyhow", + "log", + "pdk-classy", + "pdk-macros", + "pdk-script", + "protobuf", + "protobuf-codegen", + "rmp-serde", + "serde", + "serde_derive", + "serde_json", + "sha2", + "url", +] + +[[package]] +name = "pdk-ip-filter-lib" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab00d1dfe7b232fcb5424c3af23721993b439a51960f1f3152760228f492ec" +dependencies = [ + "anyhow", + "ipnet", + "pdk-core", + "thiserror 1.0.69", +] + +[[package]] +name = "pdk-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da346bb3e02aad6b6bf31e6e38798c0f3cdf8bd0319fe97d44addc5a0ea5de92" +dependencies = [ + "proc-macro2", + "quote 1.0.45", + "syn 1.0.109", +] + +[[package]] +name = "pdk-pel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c60996708c43b91581ea9e6fa324af7eae5a9b8146dd96cacf2868e9260b654" +dependencies = [ + "base64 0.22.1", + "getrandom 0.2.17", + "serde_json", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "pdk-proxy-wasm-stub" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469571b12631b71dce33890917bec59f58c53f9d3b05b748d5aa873c950e1702" + +[[package]] +name = "pdk-script" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8842ea2908eb9b94ed7daa522cc75fcd5fe3738a20d7ecde67cb7829dbf129e" +dependencies = [ + "log", + "num-traits", + "oorandom", + "pdk-classy", + "pdk-pel", + "roxmltree", + "serde", + "serde_json", + "thiserror 1.0.69", + "url", ] [[package]] @@ -5971,6 +6537,36 @@ dependencies = [ "spki 0.7.3", ] +[[package]] +name = "pkcs12" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b3df3d3cc1015f12d70235e35b6b79befc5fa7a9b95b951eab1dd07c9efc2" +dependencies = [ + "cms", + "const-oid 0.9.6", + "der 0.7.10", + "digest", + "spki 0.7.3", + "x509-cert", + "zeroize", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der 0.7.10", + "pbkdf2", + "scrypt", + "sha2", + "spki 0.7.3", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -6033,7 +6629,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.5.2", "pin-project-lite 0.2.17", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -6044,7 +6640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -6085,6 +6681,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prefix-trie" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f561214012d3fc240a1f9c817cc4d57f5310910d066069c1b093f766bb5966" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "pretty_env_logger" version = "0.4.0" @@ -6278,6 +6885,67 @@ dependencies = [ "prost", ] +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap 2.13.1", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "proxy-wasm" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d35d9e2bc5104e2e954b149aa1d5f9fa3bb27f73b45b2706020fed101db685" +dependencies = [ + "hashbrown 0.16.1", + "log", +] + [[package]] name = "pxfm" version = "0.1.28" @@ -6427,6 +7095,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -6465,6 +7144,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rav1e" version = "0.8.1" @@ -6535,6 +7220,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rc2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" +dependencies = [ + "cipher", +] + [[package]] name = "redis" version = "0.23.3" @@ -6703,16 +7397,22 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.9.0", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite 0.2.17", + "quinn", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper 1.0.2", "tokio 1.51.0", + "tokio-rustls 0.26.4", "tower", "tower-http 0.6.8", "tower-service", @@ -6747,7 +7447,7 @@ dependencies = [ "quinn", "rustls 0.23.37", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.6.2", "serde", "serde_json", "serde_urlencoded", @@ -6788,7 +7488,7 @@ dependencies = [ [[package]] name = "revolt-autumn" -version = "0.12.1" +version = "0.13.7" dependencies = [ "axum", "axum-macros", @@ -6800,6 +7500,7 @@ dependencies = [ "jxl-oxide", "kamadak-exif", "lazy_static", + "lcms2", "moka", "nanoid", "revolt-config", @@ -6820,6 +7521,7 @@ dependencies = [ "tracing", "tracing-subscriber", "ulid 1.2.1", + "url-escape", "utoipa", "utoipa-scalar", "webp", @@ -6827,7 +7529,7 @@ dependencies = [ [[package]] name = "revolt-bonfire" -version = "0.12.1" +version = "0.13.7" dependencies = [ "async-channel 2.5.0", "async-std", @@ -6858,7 +7560,7 @@ dependencies = [ [[package]] name = "revolt-coalesced" -version = "0.12.1" +version = "0.13.7" dependencies = [ "indexmap 2.13.1", "lru", @@ -6867,7 +7569,7 @@ dependencies = [ [[package]] name = "revolt-config" -version = "0.12.1" +version = "0.13.7" dependencies = [ "async-std", "cached", @@ -6884,21 +7586,28 @@ dependencies = [ [[package]] name = "revolt-crond" -version = "0.12.1" +version = "0.13.7" dependencies = [ + "futures-lite", + "iso8601-timestamp", + "lapin", "log", + "redis-kiss", "revolt-config", "revolt-database", "revolt-files", + "revolt-permissions", "revolt-result", + "revolt_optional_struct", + "serde", + "serde_json", "tokio 1.51.0", ] [[package]] name = "revolt-database" -version = "0.12.1" +version = "0.13.7" dependencies = [ - "amqprs", "async-lock 2.8.0", "async-recursion", "async-std", @@ -6913,6 +7622,7 @@ dependencies = [ "indexmap 2.13.1", "isahc", "iso8601-timestamp", + "lapin", "linkify", "livekit-api", "livekit-protocol", @@ -6925,6 +7635,7 @@ dependencies = [ "rand 0.8.5", "redis-kiss", "regex", + "revolt-coalesced", "revolt-config", "revolt-models", "revolt-parser", @@ -6946,9 +7657,8 @@ dependencies = [ [[package]] name = "revolt-delta" -version = "0.12.1" +version = "0.13.7" dependencies = [ - "amqprs", "async-channel 2.5.0", "async-std", "authifier", @@ -6958,6 +7668,7 @@ dependencies = [ "futures", "impl_ops", "iso8601-timestamp", + "lapin", "lettre", "linkify", "livekit-api", @@ -6995,7 +7706,7 @@ dependencies = [ [[package]] name = "revolt-files" -version = "0.12.1" +version = "0.13.7" dependencies = [ "aes-gcm", "anyhow", @@ -7023,7 +7734,7 @@ dependencies = [ [[package]] name = "revolt-gifbox" -version = "0.12.1" +version = "0.13.7" dependencies = [ "axum", "axum-extra", @@ -7046,7 +7757,7 @@ dependencies = [ [[package]] name = "revolt-january" -version = "0.12.1" +version = "0.13.7" dependencies = [ "async-recursion", "axum", @@ -7055,6 +7766,7 @@ dependencies = [ "lazy_static", "mime", "moka", + "pdk-ip-filter-lib", "regex", "reqwest 0.13.2", "revolt-config", @@ -7068,13 +7780,14 @@ dependencies = [ "tokio 1.51.0", "tracing", "tracing-subscriber", + "url", "utoipa", "utoipa-scalar", ] [[package]] name = "revolt-models" -version = "0.12.1" +version = "0.13.7" dependencies = [ "indexmap 2.13.1", "iso8601-timestamp", @@ -7093,14 +7806,14 @@ dependencies = [ [[package]] name = "revolt-parser" -version = "0.12.1" +version = "0.13.7" dependencies = [ "logos", ] [[package]] name = "revolt-permissions" -version = "0.12.1" +version = "0.13.7" dependencies = [ "async-std", "async-trait", @@ -7115,7 +7828,7 @@ dependencies = [ [[package]] name = "revolt-presence" -version = "0.12.1" +version = "0.13.7" dependencies = [ "async-std", "log", @@ -7127,9 +7840,8 @@ dependencies = [ [[package]] name = "revolt-pushd" -version = "0.12.1" +version = "0.13.7" dependencies = [ - "amqprs", "anyhow", "async-trait", "authifier", @@ -7137,6 +7849,7 @@ dependencies = [ "fcm_v1", "isahc", "iso8601-timestamp", + "lapin", "log", "pretty_env_logger", "redis-kiss", @@ -7158,7 +7871,7 @@ dependencies = [ [[package]] name = "revolt-ratelimits" -version = "0.12.1" +version = "0.13.7" dependencies = [ "async-trait", "authifier", @@ -7175,7 +7888,7 @@ dependencies = [ [[package]] name = "revolt-result" -version = "0.12.1" +version = "0.13.7" dependencies = [ "axum", "log", @@ -7191,9 +7904,8 @@ dependencies = [ [[package]] name = "revolt-voice-ingress" -version = "0.12.1" +version = "0.13.7" dependencies = [ - "amqprs", "async-std", "chrono", "futures", @@ -7608,6 +8320,28 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" @@ -7617,7 +8351,7 @@ dependencies = [ "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -7663,6 +8397,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-connector" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26bcb6901a3319d57589047c0da93a0f3228f13abf8dd949deef024749cb5e2" +dependencies = [ + "futures-io", + "futures-rustls", + "log", + "rustls 0.23.37", + "rustls-pki-types", + "rustls-platform-verifier 0.7.0", + "rustls-webpki 0.103.10", +] + [[package]] name = "rustls-native-certs" version = "0.6.3" @@ -7723,7 +8472,28 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.21.1", + "log", + "once_cell", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.10", + "security-framework 3.7.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", "log", "once_cell", "rustls 0.23.37", @@ -7805,6 +8575,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -7877,6 +8656,17 @@ dependencies = [ "tendril", ] +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -8171,15 +8961,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_bytes_ng" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb0ebce8684e2253f964e8b6ce51f0ccc6666bbb448fb4a6788088bda6544b6" -dependencies = [ - "serde", -] - [[package]] name = "serde_core" version = "1.0.228" @@ -8295,7 +9076,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -8306,7 +9087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -8323,7 +9104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -8378,6 +9159,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -8481,6 +9272,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spin" @@ -8772,6 +9566,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tcp-stream" +version = "0.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7219422d3348cddeaf9073772997c452085c33e22d0b08cbd19652e1b16da5" +dependencies = [ + "async-rs", + "cfg-if", + "futures-io", + "p12-keystore", + "rustls-connector", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -8781,7 +9588,7 @@ dependencies = [ "fastrand 2.4.0", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -9058,6 +9865,21 @@ dependencies = [ "tokio 1.51.0", ] +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio 1.51.0", + "tokio-rustls 0.24.1", + "tungstenite 0.20.1", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -9313,6 +10135,26 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes 1.11.1", + "data-encoding", + "http 0.2.12", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.21.12", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + [[package]] name = "typed-builder" version = "0.22.0" @@ -9979,6 +10821,18 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "widestring" version = "1.2.1" @@ -10523,6 +11377,34 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xmlparser" version = "0.13.6" diff --git a/Cargo.toml b/Cargo.toml index f10bfb26..6d1335cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ kamadak-exif = "0.5.4" webp = "0.3.0" image = "0.25.2" # avif encode requires dav1d system library: features = ["avif-native"] thumbhash = "0.1.0" +lcms2 = "6.1.1" # for color profile processing # File processing revolt_clamav-client = "0.1.5" @@ -158,7 +159,7 @@ opentelemetry-appender-tracing = "0.31.1" authifier = "1.0.16" # RabbitMQ -amqprs = "1.7.0" +lapin = "4.7.1" # Voice livekit-api = "0.4.4" @@ -184,18 +185,19 @@ url = "2.2.2" impl_ops = "0.1.1" lazy_static = "1.5.0" mime = "0.3.17" +futures-lite = "2.6.1" # Build Dependencies vergen = "7.5.0" # Local packages -revolt-coalesced = { version = "0.12.0", path = "crates/core/coalesced" } -revolt-config = { version = "0.12.0", path = "crates/core/config" } -revolt-database = { version = "0.12.0", path = "crates/core/database" } -revolt-files = { version = "0.12.0", path = "crates/core/files" } -revolt-models = { version = "0.12.0", path = "crates/core/models" } -revolt-parser = { version = "0.12.0", path = "crates/core/parser" } -revolt-permissions = { version = "0.12.0", path = "crates/core/permissions" } -revolt-presence = { version = "0.12.0", path = "crates/core/presence" } -revolt-ratelimits = { version = "0.12.0", path = "crates/core/ratelimits" } -revolt-result = { version = "0.12.0", path = "crates/core/result" } +revolt-coalesced = { version = "0.13.7", path = "crates/core/coalesced" } +revolt-config = { version = "0.13.7", path = "crates/core/config" } +revolt-database = { version = "0.13.7", path = "crates/core/database" } +revolt-files = { version = "0.13.7", path = "crates/core/files" } +revolt-models = { version = "0.13.7", path = "crates/core/models" } +revolt-parser = { version = "0.13.7", path = "crates/core/parser" } +revolt-permissions = { version = "0.13.7", path = "crates/core/permissions" } +revolt-presence = { version = "0.13.7", path = "crates/core/presence" } +revolt-ratelimits = { version = "0.13.7", path = "crates/core/ratelimits" } +revolt-result = { version = "0.13.7", path = "crates/core/result" } diff --git a/compose.yml b/compose.yml index a01c965e..b7e73689 100644 --- a/compose.yml +++ b/compose.yml @@ -8,10 +8,20 @@ services: # MongoDB database: image: mongo + command: mongod --replSet rs0 ports: - "27017:27017" volumes: - ./.data/db:/data/db + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: echo "try { rs.status() } catch (err) { rs.initiate({_id:'rs0',members:[{_id:0,host:'127.0.0.1:27017'}]}) }" | mongosh --port 27017 --quiet + interval: 5s + timeout: 30s + start_period: 0s + start_interval: 1s + retries: 30 ulimits: nofile: soft: 65536 @@ -19,11 +29,12 @@ services: # MinIO minio: - image: minio/minio - command: server /data + image: firstfinger/minio:latest + #command: server /data environment: MINIO_ROOT_USER: minioautumn MINIO_ROOT_PASSWORD: minioautumn + MINIO_REGION: minio volumes: - ./.data/minio:/data ports: diff --git a/crates/bonfire/Cargo.toml b/crates/bonfire/Cargo.toml index 1e1cc8ca..b2687266 100644 --- a/crates/bonfire/Cargo.toml +++ b/crates/bonfire/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-bonfire" -version = "0.12.1" +version = "0.13.7" license = "AGPL-3.0-or-later" edition = "2021" publish = false diff --git a/crates/bonfire/src/events/impl.rs b/crates/bonfire/src/events/impl.rs index 12113537..96a0f2ab 100644 --- a/crates/bonfire/src/events/impl.rs +++ b/crates/bonfire/src/events/impl.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use futures::future::join_all; +use redis_kiss::AsyncCommands; use revolt_database::{ events::client::{EventV1, ReadyPayloadFields}, util::permissions::DatabasePermissionQuery, diff --git a/crates/bonfire/src/websocket.rs b/crates/bonfire/src/websocket.rs index b5611bd0..ee551289 100644 --- a/crates/bonfire/src/websocket.rs +++ b/crates/bonfire/src/websocket.rs @@ -13,7 +13,7 @@ use futures::{ stream::{SplitSink, SplitStream}, FutureExt, SinkExt, StreamExt, TryStreamExt, }; -use redis_kiss::{PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI}; +use redis_kiss::{get_connection, AsyncCommands, PayloadType, REDIS_PAYLOAD_TYPE, REDIS_URI}; use revolt_config::report_internal_error; use revolt_database::{ events::{client::EventV1, server::ClientMessage}, @@ -32,6 +32,7 @@ use sentry::Level; use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback}; use crate::events::state::{State, SubscriptionStateChange}; +use revolt_models::v0; type WsReader = SplitStream>; type WsWriter = SplitSink, async_tungstenite::tungstenite::Message>; @@ -128,6 +129,14 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr) return; } + let slowmodes = fetch_user_slowmodes(&user_id).await.unwrap_or_default(); + if !slowmodes.is_empty() { + let event = EventV1::UserSlowmodes { slowmodes }; + if report_internal_error!(write.send(config.encode(&event)).await).is_err() { + return; + } + } + // Create presence session. let (first_session, session_id) = create_session(&user_id, 0).await; @@ -527,4 +536,43 @@ async fn worker( } } } -} \ No newline at end of file +} + +async fn fetch_user_slowmodes(user_id: &str) -> Option> { + let mut conn = get_connection().await.ok()?.into_inner(); + let idx_key = format!("slowmode_idx:{}", user_id); + + let channel_ids: Vec = conn.smembers(&idx_key).await.unwrap_or_default(); + if channel_ids.is_empty() { + return Some(vec![]); + } + + // Bulk fetch all TTLs in one round trip + let mut pipe = redis_kiss::redis::pipe(); + for channel_id in &channel_ids { + pipe.ttl(format!("slowmode:{}:{}", user_id, channel_id)); + } + let ttls: Vec = pipe.query_async(&mut conn).await.unwrap_or_default(); + + // Partition into alive/expired in one pass + let mut slowmodes = vec![]; + let mut expired = vec![]; + for (channel_id, ttl) in channel_ids.iter().zip(ttls.iter()) { + if *ttl > 0 { + slowmodes.push(v0::ChannelSlowmode { + channel_id: channel_id.clone(), + duration: *ttl as u64, + retry_after: *ttl as u64, + }); + } else { + expired.push(channel_id.as_str()); + } + } + + // Bulk remove all expired members in one SREM call + if !expired.is_empty() { + conn.srem::<_, _, ()>(&idx_key, expired).await.ok(); + } + + Some(slowmodes) +} diff --git a/crates/core/coalesced/Cargo.toml b/crates/core/coalesced/Cargo.toml index e6277bd0..1c8102ad 100644 --- a/crates/core/coalesced/Cargo.toml +++ b/crates/core/coalesced/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-coalesced" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Paul Makles ", "Zomatree "] diff --git a/crates/core/config/Cargo.toml b/crates/core/config/Cargo.toml index 7b33b5e9..e9e329c7 100644 --- a/crates/core/config/Cargo.toml +++ b/crates/core/config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-config" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index 59dd217b..c6c618c0 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -30,6 +30,10 @@ host = "rabbit" port = 5672 username = "rabbituser" password = "rabbitpass" +default_exchange = "revolt.default" + +[rabbit.queues] +acks = "internal.ack" [api] @@ -135,6 +139,8 @@ pkcs8 = "" key_id = "" team_id = "" +[january] +blocked_domains = [] [files] # Encryption key for stored files @@ -321,6 +327,12 @@ emojis = 500_000 # default: 5 process_message_delay_limit = 5 +[features.legal_links] +# URLs for legal documents +terms_of_service = "" +privacy_policy = "" +guidelines = "" + [sentry] # Configuration for Sentry error reporting api = "" diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index ceb8152e..cdb52d36 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -122,12 +122,19 @@ pub struct Database { pub redis_pubsub: Option, } +#[derive(Deserialize, Debug, Clone)] +pub struct RabbitQueues { + pub acks: String, +} + #[derive(Deserialize, Debug, Clone)] pub struct Rabbit { pub host: String, pub port: u16, pub username: String, pub password: String, + pub default_exchange: String, + pub queues: RabbitQueues, } #[derive(Deserialize, Debug, Clone)] @@ -303,6 +310,11 @@ impl Pushd { } } +#[derive(Deserialize, Debug, Clone)] +pub struct January { + pub blocked_domains: Vec, +} + #[derive(Deserialize, Debug, Clone)] pub struct FilesLimit { pub min_file_size: usize, @@ -378,6 +390,16 @@ pub struct FeaturesLimitsCollection { pub roles: HashMap, } +#[derive(Deserialize, Debug, Clone)] +pub struct LegalLinks { + /// Terms of Service URL + pub terms_of_service: String, + /// Privacy Policy URL + pub privacy_policy: String, + /// Guidelines URL + pub guidelines: String, +} + #[derive(Deserialize, Debug, Clone)] pub struct FeaturesAdvanced { #[serde(default)] @@ -395,6 +417,7 @@ impl Default for FeaturesAdvanced { #[derive(Deserialize, Debug, Clone)] pub struct Features { pub limits: FeaturesLimitsCollection, + pub legal_links: LegalLinks, pub webhooks_enabled: bool, pub mass_mentions_send_notifications: bool, pub mass_mentions_enabled: bool, @@ -424,6 +447,7 @@ pub struct Settings { pub hosts: Hosts, pub api: Api, pub pushd: Pushd, + pub january: January, pub files: Files, pub features: Features, pub sentry: Sentry, diff --git a/crates/core/database/Cargo.toml b/crates/core/database/Cargo.toml index ba5b7d1b..f820c899 100644 --- a/crates/core/database/Cargo.toml +++ b/crates/core/database/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-database" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] @@ -38,6 +38,7 @@ revolt-models = { workspace = true, features = ["validator"] } revolt-presence = { workspace = true } revolt-permissions = { workspace = true, features = ["serde", "bson"] } revolt-parser = { workspace = true } +revolt-coalesced = { workspace = true } # Utility log = { workspace = true } @@ -94,9 +95,9 @@ revolt_rocket_okapi = { workspace = true, optional = true } authifier = { workspace = true } # RabbitMQ -amqprs = { workspace = true } +lapin = { workspace = true, features = ["tokio"] } # Voice -livekit-api = { workspace = true, optional = true } +livekit-api = { workspace = true, features = ["rustls-tls-native-roots"], optional = true } livekit-protocol = { workspace = true, optional = true } livekit-runtime = { workspace = true, features = ["tokio"], optional = true } diff --git a/crates/core/database/src/amqp/amqp.rs b/crates/core/database/src/amqp/amqp.rs index b29105ed..b0e5e175 100644 --- a/crates/core/database/src/amqp/amqp.rs +++ b/crates/core/database/src/amqp/amqp.rs @@ -1,58 +1,77 @@ use std::collections::HashSet; +use std::sync::Arc; use crate::events::rabbit::*; use crate::User; -use amqprs::channel::{BasicPublishArguments, ExchangeDeclareArguments}; -use amqprs::connection::OpenConnectionArguments; -use amqprs::{channel::Channel, connection::Connection, error::Error as AMQPError}; -use amqprs::{BasicProperties, FieldTable}; +use lapin::{ + options::BasicPublishOptions, + protocol::basic::AMQPProperties, + types::{AMQPValue, FieldTable}, + Channel, Connection, ConnectionProperties, Error as AMQPError, +}; use revolt_models::v0::PushNotification; use revolt_presence::filter_online; +use revolt_result::Result; use serde_json::to_string; #[derive(Clone)] pub struct AMQP { + friend_request_accepted: Arc, + friend_request_received: Arc, + generic_message: Arc, + message_sent: Arc, + mass_mention_message_sent: Arc, + ack_notification_message: Arc, + dm_call_updated: Arc, + process_ack: Arc, #[allow(unused)] - connection: Connection, - channel: Channel, + connection: Arc, } impl AMQP { - pub fn new(connection: Connection, channel: Channel) -> AMQP { - AMQP { + pub async fn new(connection: Arc) -> Self { + Self { + friend_request_accepted: Self::create_channel(&connection).await, + friend_request_received: Self::create_channel(&connection).await, + generic_message: Self::create_channel(&connection).await, + message_sent: Self::create_channel(&connection).await, + mass_mention_message_sent: Self::create_channel(&connection).await, + ack_notification_message: Self::create_channel(&connection).await, + dm_call_updated: Self::create_channel(&connection).await, + process_ack: Self::create_channel(&connection).await, connection, - channel, } } - pub async fn new_auto() -> AMQP { + pub async fn new_auto() -> Self { let config = revolt_config::config().await; - let connection = Connection::open(&OpenConnectionArguments::new( - &config.rabbit.host, - config.rabbit.port, - &config.rabbit.username, - &config.rabbit.password, - )) - .await - .expect("Failed to connect to RabbitMQ"); - - let channel = connection - .open_channel(None) - .await - .expect("Failed to open RabbitMQ channel"); - - channel - .exchange_declare( - ExchangeDeclareArguments::new(&config.pushd.exchange, "direct") - .durable(true) - .finish(), + let connection = Arc::new( + Connection::connect( + &format!( + "amqp://{}:{}@{}:{}", + &config.rabbit.username, + &config.rabbit.password, + &config.rabbit.host, + &config.rabbit.port, + ), + ConnectionProperties::default(), ) .await - .expect("Failed to declare exchange"); + .expect("Failed to connect to RabbitMQ"), + ); - AMQP::new(connection, channel) + Self::new(connection).await + } + + async fn create_channel(connection: &Connection) -> Arc { + Arc::new( + connection + .create_channel() + .await + .expect("Failed to create channel"), + ) } pub async fn friend_request_accepted( @@ -72,19 +91,20 @@ impl AMQP { config.pushd.get_fr_accepted_routing_key(), payload ); - self.channel + + self.friend_request_accepted .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.pushd.exchange, - &config.pushd.get_fr_accepted_routing_key(), - ), + config.pushd.exchange.clone().into(), + config.pushd.get_fr_accepted_routing_key().into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } pub async fn friend_request_received( @@ -105,19 +125,19 @@ impl AMQP { payload ); - self.channel + self.friend_request_received .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.pushd.exchange, - &config.pushd.get_fr_received_routing_key(), - ), + config.pushd.exchange.clone().into(), + config.pushd.get_fr_received_routing_key().into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } pub async fn generic_message( @@ -142,19 +162,19 @@ impl AMQP { payload ); - self.channel + self.generic_message .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.pushd.exchange, - &config.pushd.get_generic_routing_key(), - ), + config.pushd.exchange.clone().into(), + config.pushd.get_generic_routing_key().into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } pub async fn message_sent( @@ -185,19 +205,19 @@ impl AMQP { payload ); - self.channel + self.message_sent .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.pushd.exchange, - &config.pushd.get_message_routing_key(), - ), + config.pushd.exchange.clone().into(), + config.pushd.get_message_routing_key().into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } pub async fn mass_mention_message_sent( @@ -220,19 +240,24 @@ impl AMQP { routing_key, payload ); - self.channel + self.mass_mention_message_sent .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new(&config.pushd.exchange, routing_key.as_str()), + config.pushd.exchange.clone().into(), + routing_key.into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } - pub async fn ack_message( + /// # Sends an ack to pushd to update badges on iPhones. + /// Not to be confused with the process_ack function, which handles sending all acks to crond for processing. + pub async fn ack_notification_message( &self, user_id: String, channel_id: String, @@ -252,23 +277,25 @@ impl AMQP { config.pushd.ack_queue, payload ); - let mut headers = FieldTable::new(); + let mut headers = FieldTable::default(); headers.insert( - "x-deduplication-header".try_into().unwrap(), - format!("{}-{}", &user_id, &channel_id).into(), + "x-deduplication-header".into(), + AMQPValue::LongString(format!("{}-{}", &user_id, &channel_id).into()), ); - self.channel + self.ack_notification_message .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - //.with_headers(headers) - .finish(), - payload.into(), - BasicPublishArguments::new(&config.pushd.exchange, &config.pushd.ack_queue), + config.pushd.exchange.clone().into(), + config.pushd.ack_queue.into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) } /// # DM Call Update @@ -302,18 +329,54 @@ impl AMQP { payload ); - self.channel + self.dm_call_updated .basic_publish( - BasicProperties::default() - .with_content_type("application/json") - .with_persistence(true) - .finish(), - payload.into(), - BasicPublishArguments::new( - &config.pushd.exchange, - &config.pushd.get_dm_call_routing_key(), - ), + config.pushd.exchange.clone().into(), + config.pushd.get_dm_call_routing_key().into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), ) - .await + .await?; + + Ok(()) + } + + /// # Send an ack to crond for processing + pub async fn process_ack( + &self, + user_id: &str, + channel_id: Option<&str>, + server_id: Option<&str>, + ) -> Result<(), AMQPError> { + let config = revolt_config::config().await; + + let payload = AckEventPayload { + user_id: user_id.to_string(), + channel_id: channel_id.map(|value| value.to_string()), + server_id: server_id.map(|value| value.to_string()), + }; + let payload = to_string(&payload).unwrap(); + + info!( + "Sending ack processor event on exchange {}, channel {}: {}", + config.rabbit.default_exchange, config.rabbit.queues.acks, payload + ); + + self.process_ack + .basic_publish( + config.rabbit.default_exchange.clone().into(), + config.rabbit.queues.acks.into(), + BasicPublishOptions::default(), + payload.as_bytes(), + AMQPProperties::default() + .with_content_type("application/json".into()) + .with_delivery_mode(2), + ) + .await?; + + Ok(()) } } diff --git a/crates/core/database/src/events/client.rs b/crates/core/database/src/events/client.rs index 4615273d..31864cee 100644 --- a/crates/core/database/src/events/client.rs +++ b/crates/core/database/src/events/client.rs @@ -3,7 +3,12 @@ use revolt_result::Error; use serde::{Deserialize, Serialize}; use revolt_models::v0::{ - AppendMessage, Channel, ChannelUnread, ChannelVoiceState, Emoji, FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, UserVoiceState, Webhook + AppendMessage, Channel, ChannelSlowmode, ChannelUnread, ChannelVoiceState, Emoji, + FieldsChannel, FieldsMember, FieldsMessage, FieldsRole, FieldsServer, FieldsUser, + FieldsWebhook, Member, MemberCompositeKey, Message, PartialChannel, PartialEmoji, + PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser, PartialUserVoiceState, + PartialWebhook, PolicyChange, RemovalIntention, Report, Server, User, UserSettings, + UserVoiceState, Webhook, }; use crate::Database; @@ -51,9 +56,13 @@ impl Default for ReadyPayloadFields { #[serde(tag = "type")] pub enum EventV1 { /// Multiple events - Bulk { v: Vec }, + Bulk { + v: Vec, + }, /// Error event - Error { data: Error }, + Error { + data: Error, + }, /// Successfully authenticated Authenticated, @@ -84,7 +93,9 @@ pub enum EventV1 { }, /// Ping response - Pong { data: Ping }, + Pong { + data: Ping, + }, /// New message Message(Message), @@ -105,7 +116,10 @@ pub enum EventV1 { }, /// Delete message - MessageDelete { id: String, channel: String }, + MessageDelete { + id: String, + channel: String, + }, /// New reaction to a message MessageReact { @@ -131,7 +145,10 @@ pub enum EventV1 { }, /// Bulk delete messages - BulkMessageDelete { channel: String, ids: Vec }, + BulkMessageDelete { + channel: String, + ids: Vec, + }, /// New server ServerCreate { @@ -139,7 +156,7 @@ pub enum EventV1 { server: Server, channels: Vec, emojis: Vec, - voice_states: Vec + voice_states: Vec, }, /// Update existing server @@ -151,7 +168,9 @@ pub enum EventV1 { }, /// Delete server - ServerDelete { id: String }, + ServerDelete { + id: String, + }, /// Update existing server member ServerMemberUpdate { @@ -187,10 +206,16 @@ pub enum EventV1 { }, /// Server role deleted - ServerRoleDelete { id: String, role_id: String }, + ServerRoleDelete { + id: String, + role_id: String, + }, /// Server roles ranks updated - ServerRoleRanksUpdate { id: String, ranks: Vec }, + ServerRoleRanksUpdate { + id: String, + ranks: Vec, + }, /// Update existing user UserUpdate { @@ -202,9 +227,15 @@ pub enum EventV1 { }, /// Relationship with another user changed - UserRelationship { id: String, user: User }, + UserRelationship { + id: String, + user: User, + }, /// Settings updated remotely - UserSettingsUpdate { id: String, update: UserSettings }, + UserSettingsUpdate { + id: String, + update: UserSettings, + }, /// User has been platform banned or deleted their account /// @@ -215,12 +246,23 @@ pub enum EventV1 { /// - Server Memberships /// /// User flags are specified to explain why a wipe is occurring though not all reasons will necessarily ever appear. - UserPlatformWipe { user_id: String, flags: i32 }, + UserPlatformWipe { + user_id: String, + flags: i32, + }, /// New emoji EmojiCreate(Emoji), + /// Update existing emoji + EmojiUpdate { + id: String, + data: PartialEmoji, + }, + /// Delete emoji - EmojiDelete { id: String }, + EmojiDelete { + id: String, + }, /// New report ReportCreate(Report), @@ -236,19 +278,33 @@ pub enum EventV1 { }, /// Delete channel - ChannelDelete { id: String }, + ChannelDelete { + id: String, + }, /// User joins a group - ChannelGroupJoin { id: String, user: String }, + ChannelGroupJoin { + id: String, + user: String, + }, /// User leaves a group - ChannelGroupLeave { id: String, user: String }, + ChannelGroupLeave { + id: String, + user: String, + }, /// User started typing in a channel - ChannelStartTyping { id: String, user: String }, + ChannelStartTyping { + id: String, + user: String, + }, /// User stopped typing in a channel - ChannelStopTyping { id: String, user: String }, + ChannelStopTyping { + id: String, + user: String, + }, /// User acknowledged message in channel ChannelAck { @@ -268,7 +324,9 @@ pub enum EventV1 { }, /// Delete webhook - WebhookDelete { id: String }, + WebhookDelete { + id: String, + }, /// Auth events Auth(AuthifierEvent), @@ -286,7 +344,7 @@ pub enum EventV1 { user: String, from: String, to: String, - state: UserVoiceState + state: UserVoiceState, }, UserVoiceStateUpdate { id: String, @@ -298,7 +356,11 @@ pub enum EventV1 { from: String, to: String, token: String, - } + }, + /// User's active slowmodes + UserSlowmodes { + slowmodes: Vec, + }, } impl EventV1 { diff --git a/crates/core/database/src/events/rabbit.rs b/crates/core/database/src/events/rabbit.rs index 6f5c9ab3..1673f4a4 100644 --- a/crates/core/database/src/events/rabbit.rs +++ b/crates/core/database/src/events/rabbit.rs @@ -78,3 +78,11 @@ pub struct AckPayload { pub channel_id: String, pub message_id: String, } + +/// This is not the same as the AckPayload above, as the state for this event is stored in redis to allow for state updates while the event is queued. +#[derive(Serialize, Deserialize, Debug)] +pub struct AckEventPayload { + pub user_id: String, + pub channel_id: Option, + pub server_id: Option, +} diff --git a/crates/core/database/src/models/channels/model.rs b/crates/core/database/src/models/channels/model.rs index 025b7934..74b56e11 100644 --- a/crates/core/database/src/models/channels/model.rs +++ b/crates/core/database/src/models/channels/model.rs @@ -1,6 +1,7 @@ #![allow(deprecated)] use std::{borrow::Cow, collections::HashMap}; +use redis_kiss::get_connection; use revolt_config::config; use revolt_models::v0::{self, MessageAuthor}; use revolt_permissions::OverrideField; @@ -212,7 +213,7 @@ impl Channel { role_permissions: HashMap::new(), nsfw: data.nsfw.unwrap_or(false), voice: data.voice.map(|voice| voice.into()), - slowmode: None + slowmode: None, }, v0::LegacyServerChannelType::Voice => Channel::TextChannel { id: id.clone(), @@ -225,7 +226,7 @@ impl Channel { role_permissions: HashMap::new(), nsfw: data.nsfw.unwrap_or(false), voice: Some(data.voice.unwrap_or_default().into()), - slowmode: None + slowmode: None, }, }; @@ -643,7 +644,7 @@ impl Channel { } /// Acknowledge a message - pub async fn ack(&self, user: &str, message: &str) -> Result<()> { + pub async fn ack(&self, user: &str, message: &str, amqp: &AMQP) -> Result<()> { EventV1::ChannelAck { id: self.id().to_string(), user: user.to_string(), @@ -652,17 +653,7 @@ impl Channel { .private(user.to_string()) .await; - #[cfg(feature = "tasks")] - crate::tasks::ack::queue_ack( - self.id().to_string(), - user.to_string(), - crate::tasks::ack::AckEvent::AckMessage { - id: message.to_string(), - }, - ) - .await; - - Ok(()) + crate::util::acker::ack_channel(user, self.id(), message, amqp).await } /// Remove user from a group diff --git a/crates/core/database/src/models/emojis/model.rs b/crates/core/database/src/models/emojis/model.rs index 8294d7c7..55d13e8c 100644 --- a/crates/core/database/src/models/emojis/model.rs +++ b/crates/core/database/src/models/emojis/model.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::str::FromStr; use once_cell::sync::Lazy; +use revolt_models::v0; use revolt_result::Result; use ulid::Ulid; @@ -11,7 +12,7 @@ use crate::Database; static PERMISSIBLE_EMOJIS: Lazy> = Lazy::new(|| { include_str!("unicode_emoji.txt") .split('\n') - .map(|x| x.into()) + .map(|x| x.replace('\u{FE0F}', "")) .collect() }); @@ -41,6 +42,12 @@ auto_derived!( Server { id: String }, Detached, } + + /// Partial representation of an emoji + pub struct PartialEmoji { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + } ); #[allow(clippy::disallowed_methods)] @@ -75,13 +82,34 @@ impl Emoji { db.detach_emoji(&self).await } + /// Update an emoji + pub async fn update(&mut self, db: &Database, partial: PartialEmoji) -> Result<()> { + if let Some(name) = partial.name.clone() { + self.name = name; + } + + db.update_emoji(&self.id, &partial).await?; + + EventV1::EmojiUpdate { + id: self.id.clone(), + data: v0::PartialEmoji { + name: partial.name.clone(), + }, + } + .p(self.parent().to_string()) + .await; + + Ok(()) + } + /// Check whether we can use a given emoji pub async fn can_use(db: &Database, emoji: &str) -> Result { if Ulid::from_str(emoji).is_ok() { db.fetch_emoji(emoji).await?; Ok(true) } else { - Ok(PERMISSIBLE_EMOJIS.contains(emoji)) + let sanitized_emoji = emoji.replace('\u{FE0F}', ""); + Ok(PERMISSIBLE_EMOJIS.contains(&sanitized_emoji)) } } } diff --git a/crates/core/database/src/models/emojis/ops.rs b/crates/core/database/src/models/emojis/ops.rs index 26e23aca..d28f30d4 100644 --- a/crates/core/database/src/models/emojis/ops.rs +++ b/crates/core/database/src/models/emojis/ops.rs @@ -1,6 +1,6 @@ use revolt_result::Result; -use crate::Emoji; +use crate::{Emoji, PartialEmoji}; #[cfg(feature = "mongodb")] mod mongodb; @@ -20,6 +20,9 @@ pub trait AbstractEmojis: Sync + Send { /// Fetch emoji by their parent ids async fn fetch_emoji_by_parent_ids(&self, parent_ids: &[String]) -> Result>; + /// Update emoji with new information + async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()>; + /// Detach an emoji by its id async fn detach_emoji(&self, emoji: &Emoji) -> Result<()>; } diff --git a/crates/core/database/src/models/emojis/ops/mongodb.rs b/crates/core/database/src/models/emojis/ops/mongodb.rs index 6dd3137b..ad7b557c 100644 --- a/crates/core/database/src/models/emojis/ops/mongodb.rs +++ b/crates/core/database/src/models/emojis/ops/mongodb.rs @@ -1,7 +1,7 @@ use bson::Document; use revolt_result::Result; -use crate::Emoji; +use crate::{Emoji, PartialEmoji}; use crate::MongoDb; use super::AbstractEmojis; @@ -46,6 +46,11 @@ impl AbstractEmojis for MongoDb { ) } + /// Update emoji with new information + async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()> { + query!(self, update_one_by_id, COL, emoji_id, partial, vec![], None).map(|_| ()) + } + /// Detach an emoji by its id async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> { self.col::(COL) diff --git a/crates/core/database/src/models/emojis/ops/reference.rs b/crates/core/database/src/models/emojis/ops/reference.rs index 2f0c2a2d..2f9be435 100644 --- a/crates/core/database/src/models/emojis/ops/reference.rs +++ b/crates/core/database/src/models/emojis/ops/reference.rs @@ -1,6 +1,6 @@ use revolt_result::Result; -use crate::Emoji; +use crate::{Emoji, PartialEmoji}; use crate::EmojiParent; use crate::ReferenceDb; @@ -54,6 +54,19 @@ impl AbstractEmojis for ReferenceDb { .collect()) } + /// Update emoji with new information + async fn update_emoji(&self, emoji_id: &str, partial: &PartialEmoji) -> Result<()> { + let mut emojis = self.emojis.lock().await; + if let Some(emoji) = emojis.get_mut(emoji_id) { + if let Some(name) = partial.name.clone() { + emoji.name = name; + } + Ok(()) + } else { + Err(create_error!(NotFound)) + } + } + /// Detach an emoji by its id async fn detach_emoji(&self, emoji: &Emoji) -> Result<()> { let mut emojis = self.emojis.lock().await; diff --git a/crates/core/database/src/models/emojis/unicode_emoji.txt b/crates/core/database/src/models/emojis/unicode_emoji.txt index bc8036e3..06e8bf74 100644 --- a/crates/core/database/src/models/emojis/unicode_emoji.txt +++ b/crates/core/database/src/models/emojis/unicode_emoji.txt @@ -1,1421 +1,84 @@ -💯 -🔢 -😀 -😃 -😄 -😁 -😆 -😆 -😅 -🤣 -😂 -🙂 -🙃 -😉 -😊 -😇 -🥰 -😍 -🤩 -😘 -😗 -☺️ -😚 -😙 -🥲 -😋 -😛 -😜 -🤪 -😝 -🤑 -🤗 -🤭 -🤫 -🤔 -🤐 -🤨 -😐 -😑 -😶 -😏 -😒 -🙄 -😬 -🤥 -😌 -😔 -😪 -🤤 -😴 -😷 -🤒 -🤕 -🤢 -🤮 -🤧 -🥵 -🥶 -🥴 -😵 -🤯 -🤠 -🥳 -🥸 -😎 -🤓 -🧐 -😕 -😟 -🙁 -☹️ -😮 -😯 -😲 -😳 -🥺 -😦 -😧 -😨 -😰 -😥 -😢 -😭 -😱 -😖 -😣 -😞 -😓 -😩 -😫 -🥱 -😤 -😡 -😡 -😠 -🤬 -😈 -👿 -💀 -☠️ -💩 -💩 -💩 -🤡 -👹 -👺 -👻 -👽 -👾 -🤖 -😺 -😸 -😹 -😻 -😼 -😽 -🙀 -😿 -😾 -🙈 -🙉 -🙊 -💋 -💌 -💘 -💝 -💖 -💗 -💓 -💞 -💕 -💟 -❣️ -💔 -❤️ -🧡 -💛 -💚 -💙 -💜 -🤎 -🖤 -🤍 -💢 -💥 -💥 -💫 -💦 -💨 -🕳️ -💣 -💬 -👁️‍🗨️ -🗨️ -🗯️ -💭 -💤 -👋 -🤚 -🖐️ -✋ -✋ -🖖 -👌 -🤌 -🤏 -✌️ -🤞 -🤟 -🤘 -🤙 -👈 -👉 -👆 -🖕 -🖕 -👇 -☝️ -👍 -👍 -👎 -👎 -✊ -✊ -👊 -👊 -👊 -🤛 -🤜 -👏 -🙌 -👐 -🤲 -🤝 -🙏 -✍️ -💅 -🤳 -💪 -🦾 -🦿 -🦵 -🦶 -👂 -🦻 -👃 -🧠 -🫀 -🫁 -🦷 -🦴 -👀 -👁️ -👅 -👄 -👶 -🧒 -👦 -👧 -🧑 -👱 -👨 -🧔 -👨‍🦰 -👨‍🦱 -👨‍🦳 -👨‍🦲 -👩 -👩‍🦰 -🧑‍🦰 -👩‍🦱 -🧑‍🦱 -👩‍🦳 -🧑‍🦳 -👩‍🦲 -🧑‍🦲 -👱‍♀️ -👱‍♀️ -👱‍♂️ -🧓 -👴 -👵 -🙍 -🙍‍♂️ -🙍‍♀️ -🙎 -🙎‍♂️ -🙎‍♀️ -🙅 -🙅‍♂️ -🙅‍♂️ -🙅‍♀️ -🙅‍♀️ -🙆 -🙆‍♂️ -🙆‍♀️ -💁 -💁 -💁‍♂️ -💁‍♂️ -💁‍♀️ -💁‍♀️ -🙋 -🙋‍♂️ -🙋‍♀️ -🧏 -🧏‍♂️ -🧏‍♀️ -🙇 -🙇‍♂️ -🙇‍♀️ -🤦 -🤦‍♂️ -🤦‍♀️ -🤷 -🤷‍♂️ -🤷‍♀️ -🧑‍⚕️ -👨‍⚕️ -👩‍⚕️ -🧑‍🎓 -👨‍🎓 -👩‍🎓 -🧑‍🏫 -👨‍🏫 -👩‍🏫 -🧑‍⚖️ -👨‍⚖️ -👩‍⚖️ -🧑‍🌾 -👨‍🌾 -👩‍🌾 -🧑‍🍳 -👨‍🍳 -👩‍🍳 -🧑‍🔧 -👨‍🔧 -👩‍🔧 -🧑‍🏭 -👨‍🏭 -👩‍🏭 -🧑‍💼 -👨‍💼 -👩‍💼 -🧑‍🔬 -👨‍🔬 -👩‍🔬 -🧑‍💻 -👨‍💻 -👩‍💻 -🧑‍🎤 -👨‍🎤 -👩‍🎤 -🧑‍🎨 -👨‍🎨 -👩‍🎨 -🧑‍✈️ -👨‍✈️ -👩‍✈️ -🧑‍🚀 -👨‍🚀 -👩‍🚀 -🧑‍🚒 -👨‍🚒 -👩‍🚒 -👮 -👮 -👮‍♂️ -👮‍♀️ -🕵️ -🕵️‍♂️ -🕵️‍♀️ -💂 -💂‍♂️ -💂‍♀️ -🥷 -👷 -👷‍♂️ -👷‍♀️ -🤴 -👸 -👳 -👳‍♂️ -👳‍♀️ -👲 -🧕 -🤵 -🤵‍♂️ -🤵‍♀️ -👰 -👰‍♂️ -👰‍♀️ -👰‍♀️ -🤰 -🤱 -👩‍🍼 -👨‍🍼 -🧑‍🍼 -👼 -🎅 -🤶 -🧑‍🎄 -🦸 -🦸‍♂️ -🦸‍♀️ -🦹 -🦹‍♂️ -🦹‍♀️ -🧙 -🧙‍♂️ -🧙‍♀️ -🧚 -🧚‍♂️ -🧚‍♀️ -🧛 -🧛‍♂️ -🧛‍♀️ -🧜 -🧜‍♂️ -🧜‍♀️ -🧝 -🧝‍♂️ -🧝‍♀️ -🧞 -🧞‍♂️ -🧞‍♀️ -🧟 -🧟‍♂️ -🧟‍♀️ -💆 -💆‍♂️ -💆‍♀️ -💇 -💇‍♂️ -💇‍♀️ -🚶 -🚶‍♂️ -🚶‍♀️ -🧍 -🧍‍♂️ -🧍‍♀️ -🧎 -🧎‍♂️ -🧎‍♀️ -🧑‍🦯 -👨‍🦯 -👩‍🦯 -🧑‍🦼 -👨‍🦼 -👩‍🦼 -🧑‍🦽 -👨‍🦽 -👩‍🦽 -🏃 -🏃 -🏃‍♂️ -🏃‍♀️ -💃 -💃 -🕺 -🕴️ -👯 -👯‍♂️ -👯‍♀️ -🧖 -🧖‍♂️ -🧖‍♀️ -🧗 -🧗‍♂️ -🧗‍♀️ -🤺 -🏇 -⛷️ -🏂 -🏌️ -🏌️‍♂️ -🏌️‍♀️ -🏄 -🏄‍♂️ -🏄‍♀️ -🚣 -🚣‍♂️ -🚣‍♀️ -🏊 -🏊‍♂️ -🏊‍♀️ -⛹️ -⛹️‍♂️ -⛹️‍♂️ -⛹️‍♀️ -⛹️‍♀️ -🏋️ -🏋️‍♂️ -🏋️‍♀️ -🚴 -🚴‍♂️ -🚴‍♀️ -🚵 -🚵‍♂️ -🚵‍♀️ -🤸 -🤸‍♂️ -🤸‍♀️ -🤼 -🤼‍♂️ -🤼‍♀️ -🤽 -🤽‍♂️ -🤽‍♀️ -🤾 -🤾‍♂️ -🤾‍♀️ -🤹 -🤹‍♂️ -🤹‍♀️ -🧘 -🧘‍♂️ -🧘‍♀️ -🛀 -🛌 -🧑‍🤝‍🧑 -👭 -👫 -👬 -💏 -👩‍❤️‍💋‍👨 -👨‍❤️‍💋‍👨 -👩‍❤️‍💋‍👩 -💑 -👩‍❤️‍👨 -👨‍❤️‍👨 -👩‍❤️‍👩 -👪 -👨‍👩‍👦 -👨‍👩‍👧 -👨‍👩‍👧‍👦 -👨‍👩‍👦‍👦 -👨‍👩‍👧‍👧 -👨‍👨‍👦 -👨‍👨‍👧 -👨‍👨‍👧‍👦 -👨‍👨‍👦‍👦 -👨‍👨‍👧‍👧 -👩‍👩‍👦 -👩‍👩‍👧 -👩‍👩‍👧‍👦 -👩‍👩‍👦‍👦 -👩‍👩‍👧‍👧 -👨‍👦 -👨‍👦‍👦 -👨‍👧 -👨‍👧‍👦 -👨‍👧‍👧 -👩‍👦 -👩‍👦‍👦 -👩‍👧 -👩‍👧‍👦 -👩‍👧‍👧 -🗣️ -👤 -👥 -🫂 -👣 -🐵 -🐒 -🦍 -🦧 -🐶 -🐕 -🦮 -🐕‍🦺 -🐩 -🐺 -🦊 -🦝 -🐱 -🐈 -🐈‍⬛ -🦁 -🐯 -🐅 -🐆 -🐴 -🐎 -🦄 -🦓 -🦌 -🦬 -🐮 -🐂 -🐃 -🐄 -🐷 -🐖 -🐗 -🐽 -🐏 -🐑 -🐐 -🐪 -🐫 -🦙 -🦒 -🐘 -🦣 -🦏 -🦛 -🐭 -🐁 -🐀 -🐹 -🐰 -🐇 -🐿️ -🦫 -🦔 -🦇 -🐻 -🐻‍❄️ -🐨 -🐼 -🦥 -🦦 -🦨 -🦘 -🦡 -🐾 -🐾 -🦃 -🐔 -🐓 -🐣 -🐤 -🐥 -🐦 -🐧 -🕊️ -🦅 -🦆 -🦢 -🦉 -🦤 -🪶 -🦩 -🦚 -🦜 -🐸 -🐊 -🐢 -🦎 -🐍 -🐲 -🐉 -🦕 -🦖 -🐳 -🐋 -🐬 -🐬 -🦭 -🐟 -🐠 -🐡 -🦈 -🐙 -🐚 -🐌 -🦋 -🐛 -🐜 -🐝 -🐝 -🪲 -🐞 -🦗 -🪳 -🕷️ -🕸️ -🦂 -🦟 -🪰 -🪱 -🦠 -💐 -🌸 -💮 -🏵️ -🌹 -🥀 -🌺 -🌻 -🌼 -🌷 -🌱 -🪴 -🌲 -🌳 -🌴 -🌵 -🌾 -🌿 -☘️ -🍀 -🍁 -🍂 -🍃 -🍇 -🍈 -🍉 -🍊 -🍊 -🍊 -🍋 -🍌 -🍌 -🍍 -🥭 -🍎 -🍏 -🍐 -🍑 -🍒 -🍓 -🫐 -🥝 -🍅 -🫒 -🥥 -🥑 -🍆 -🥔 -🥕 -🌽 -🌶️ -🫑 -🥒 -🥬 -🥦 -🧄 -🧅 -🍄 -🥜 -🌰 -🍞 -🥐 -🥖 -🫓 -🥨 -🥯 -🥞 -🧇 -🧀 -🍖 -🍗 -🥩 -🥓 -🍔 -🍟 -🍕 -🌭 -🥪 -🌮 -🌯 -🫔 -🥙 -🧆 -🥚 -🍳 -🥘 -🍲 -🫕 -🥣 -🥗 -🍿 -🧈 -🧂 -🥫 -🍱 -🍘 -🍙 -🍚 -🍛 -🍜 -🍝 -🍠 -🍢 -🍣 -🍤 -🍥 -🥮 -🍡 -🥟 -🥠 -🥡 -🦀 -🦞 -🦐 -🦑 -🦪 -🍦 -🍧 -🍨 -🍩 -🍪 -🎂 -🍰 -🧁 -🥧 -🍫 -🍬 -🍭 -🍮 -🍯 -🍼 -🥛 -☕ -🫖 -🍵 -🍶 -🍾 -🍷 -🍸 -🍹 -🍺 -🍻 -🥂 -🥃 -🥤 -🧋 -🧃 -🧉 -🧊 -🥢 -🍽️ -🍴 -🥄 -🔪 -🔪 -🏺 -🌍 -🌎 -🌏 -🌐 -🗺️ -🗾 -🧭 -🏔️ -⛰️ -🌋 -🗻 -🏕️ -🏖️ -🏜️ -🏝️ -🏞️ -🏟️ -🏛️ -🏗️ -🧱 -🪨 -🪵 -🛖 -🏘️ -🏚️ -🏠 -🏡 -🏢 -🏣 -🏤 -🏥 -🏦 -🏨 -🏩 -🏪 -🏫 -🏬 -🏭 -🏯 -🏰 -💒 -🗼 -🗽 -⛪ -🕌 -🛕 -🕍 -⛩️ -🕋 -⛲ -⛺ -🌁 -🌃 -🏙️ -🌄 -🌅 -🌆 -🌇 -🌉 -♨️ -🎠 -🎡 -🎢 -💈 -🎪 -🚂 -🚃 -🚄 -🚅 -🚆 -🚇 -🚈 -🚉 -🚊 -🚝 -🚞 -🚋 -🚌 -🚍 -🚎 -🚎 -🚐 -🚑 -🚒 -🚓 -🚔 -🚕 -🚖 -🚗 -🚗 -🚘 -🚙 -🛻 -🚚 -🚛 -🚜 -🏎️ -🏍️ -🛵 -🦽 -🦼 -🛺 -🚲 -🛴 -🛹 -🛼 -🚏 -🛣️ -🛤️ -🛢️ -⛽ -🚨 -🚥 -🚦 -🛑 -🚧 -⚓ -⛵ -⛵ -🛶 -🚤 -🛳️ -⛴️ -🛥️ -🚢 -✈️ -🛩️ -🛫 -🛬 -🪂 -💺 -🚁 -🚟 -🚠 -🚡 -🛰️ -🚀 -🛸 -🛎️ -🧳 -⌛ -⏳ + +*️⃣ +0️⃣ +1️⃣ +2️⃣ +3️⃣ +4️⃣ +5️⃣ +6️⃣ +7️⃣ +8️⃣ +9️⃣ +©️ +®️ +‼️ +⁉️ +™️ +ℹ️ +↔️ +↕️ +↖️ +↗️ +↘️ +↙️ +↩️ +↪️ ⌚ +⌛ +⌨️ +⏏️ +⏩ +⏪ +⏫ +⏬ +⏭️ +⏮️ +⏯️ ⏰ ⏱️ ⏲️ -🕰️ -🕛 -🕧 -🕐 -🕜 -🕑 -🕝 -🕒 -🕞 -🕓 -🕟 -🕔 -🕠 -🕕 -🕡 -🕖 -🕢 -🕗 -🕣 -🕘 -🕤 -🕙 -🕥 -🕚 -🕦 -🌑 -🌒 -🌓 -🌔 -🌔 -🌕 -🌖 -🌗 -🌘 -🌙 -🌚 -🌛 -🌜 -🌡️ +⏳ +⏸️ +⏹️ +⏺️ +Ⓜ️ +▪️ +▫️ +▶️ +◀️ +◻️ +◼️ +◽ +◾ ☀️ -🌝 -🌞 -🪐 -⭐ -🌟 -🌠 -🌌 ☁️ -⛅ -⛈️ -🌤️ -🌥️ -🌦️ -🌧️ -🌨️ -🌩️ -🌪️ -🌫️ -🌬️ -🌀 -🌈 -🌂 ☂️ -☔ -⛱️ -⚡ -❄️ ☃️ -⛄ ☄️ -🔥 -💧 -🌊 -🎃 -🎄 -🎆 -🎇 -🧨 -✨ -🎈 -🎉 -🎊 -🎋 -🎍 -🎎 -🎏 -🎐 -🎑 -🧧 -🎀 -🎁 -🎗️ -🎟️ -🎫 -🎖️ -🏆 -🏅 -🥇 -🥈 -🥉 -⚽ -⚾ -🥎 -🏀 -🏐 -🏈 -🏉 -🎾 -🥏 -🎳 -🏏 -🏑 -🏒 -🥍 -🏓 -🏸 -🥊 -🥋 -🥅 -⛳ -⛸️ -🎣 -🤿 -🎽 -🎿 -🛷 -🥌 -🎯 -🪀 -🪁 -🎱 -🔮 -🪄 -🧿 -🎮 -🕹️ -🎰 -🎲 -🧩 -🧸 -🪅 -🪆 -♠️ -♥️ -♦️ -♣️ -♟️ -🃏 -🀄 -🎴 -🎭 -🖼️ -🎨 -🧵 -🪡 -🧶 -🪢 -👓 -🕶️ -🥽 -🥼 -🦺 -👔 -👕 -👕 -👖 -🧣 -🧤 -🧥 -🧦 -👗 -👘 -🥻 -🩱 -🩲 -🩳 -👙 -👚 -👛 -👜 -👝 -🛍️ -🎒 -🩴 -👞 -👞 -👟 -🥾 -🥿 -👠 -👡 -🩰 -👢 -👑 -👒 -🎩 -🎓 -🧢 -🪖 -⛑️ -📿 -💄 -💍 -💎 -🔇 -🔈 -🔉 -🔊 -📢 -📣 -📯 -🔔 -🔕 -🎼 -🎵 -🎶 -🎙️ -🎚️ -🎛️ -🎤 -🎧 -📻 -🎷 -🪗 -🎸 -🎹 -🎺 -🎻 -🪕 -🥁 -🪘 -📱 -📲 ☎️ -☎️ -📞 -📟 -📠 -🔋 -🔌 -💻 -🖥️ -🖨️ -⌨️ -🖱️ -🖲️ -💽 -💾 -💿 -📀 -🧮 -🎥 -🎞️ -📽️ -🎬 -📺 -📷 -📸 -📹 -📼 -🔍 -🔎 -🕯️ -💡 -🔦 -🏮 -🏮 -🪔 -📔 -📕 -📖 -📖 -📗 -📘 -📙 -📚 -📓 -📒 -📃 -📜 -📄 -📰 -🗞️ -📑 -🔖 -🏷️ -💰 -🪙 -💴 -💵 -💶 -💷 -💸 -💳 -🧾 -💹 -✉️ -📧 -📧 -📨 -📩 -📤 -📥 -📦 -📫 -📪 -📬 -📭 -📮 -🗳️ -✏️ -✒️ -🖋️ -🖊️ -🖌️ -🖍️ -📝 -📝 -💼 -📁 -📂 -🗂️ -📅 -📆 -🗒️ -🗓️ -📇 -📈 -📉 -📊 -📋 -📌 -📍 -📎 -🖇️ -📏 -📐 -✂️ -🗃️ -🗄️ -🗑️ -🔒 -🔓 -🔏 -🔐 -🔑 -🗝️ -🔨 -🪓 -⛏️ -⚒️ -🛠️ -🗡️ -⚔️ -🔫 -🪃 -🏹 -🛡️ -🪚 -🔧 -🪛 -🔩 -⚙️ -🗜️ -⚖️ -🦯 -🔗 -⛓️ -🪝 -🧰 -🧲 -🪜 -⚗️ -🧪 -🧫 -🧬 -🔬 -🔭 -📡 -💉 -🩸 -💊 -🩹 -🩺 -🚪 -🛗 -🪞 -🪟 -🛏️ -🛋️ -🪑 -🚽 -🪠 -🚿 -🛁 -🪤 -🪒 -🧴 -🧷 -🧹 -🧺 -🧻 -🪣 -🧼 -🪥 -🧽 -🧯 -🛒 -🚬 -⚰️ -🪦 -⚱️ -🗿 -🪧 -🏧 -🚮 -🚰 -♿ -🚹 -🚺 -🚻 -🚼 -🚾 -🛂 -🛃 -🛄 -🛅 -⚠️ -🚸 -⛔ -🚫 -🚳 -🚭 -🚯 -🚱 -🚷 -📵 -🔞 +☑️ +☔ +☕ +☘️ +☝️ +☝🏻 +☝🏼 +☝🏽 +☝🏾 +☝🏿 +☠️ ☢️ ☣️ -⬆️ -↗️ -➡️ -↘️ -⬇️ -↙️ -⬅️ -↖️ -↕️ -↔️ -↩️ -↪️ -⤴️ -⤵️ -🔃 -🔄 -🔙 -🔚 -🔛 -🔜 -🔝 -🛐 -⚛️ -🕉️ -✡️ -☸️ -☯️ -✝️ ☦️ ☪️ ☮️ -🕎 -🔯 +☯️ +☸️ +☹️ +☺️ +♀️ +♂️ ♈ ♉ ♊ @@ -1428,163 +91,157 @@ ♑ ♒ ♓ -⛎ -🔀 -🔁 -🔂 -▶️ -⏩ -⏭️ -⏯️ -◀️ -⏪ -⏮️ -🔼 -⏫ -🔽 -⏬ -⏸️ -⏹️ -⏺️ -⏏️ -🎦 -🔅 -🔆 -📶 -📳 -📴 -♀️ -♂️ -⚧️ -✖️ -➕ -➖ -➗ +♟️ +♠️ +♣️ +♥️ +♦️ +♨️ +♻️ ♾️ -‼️ -⁉️ +♿ +⚒️ +⚓ +⚔️ +⚕️ +⚖️ +⚗️ +⚙️ +⚛️ +⚜️ +⚠️ +⚡ +⚧️ +⚪ +⚫ +⚰️ +⚱️ +⚽ +⚾ +⛄ +⛅ +⛈️ +⛎ +⛏️ +⛑️ +⛓️ +⛓️‍💥 +⛔ +⛩️ +⛪ +⛰️ +⛱️ +⛲ +⛳ +⛴️ +⛵ +⛷️ +⛸️ +⛹️ +⛹️‍♀️ +⛹️‍♂️ +⛹🏻 +⛹🏻‍♀️ +⛹🏻‍♂️ +⛹🏼 +⛹🏼‍♀️ +⛹🏼‍♂️ +⛹🏽 +⛹🏽‍♀️ +⛹🏽‍♂️ +⛹🏾 +⛹🏾‍♀️ +⛹🏾‍♂️ +⛹🏿 +⛹🏿‍♀️ +⛹🏿‍♂️ +⛺ +⛽ +✂️ +✅ +✈️ +✉️ +✊ +✊🏻 +✊🏼 +✊🏽 +✊🏾 +✊🏿 +✋ +✋🏻 +✋🏼 +✋🏽 +✋🏾 +✋🏿 +✌️ +✌🏻 +✌🏼 +✌🏽 +✌🏾 +✌🏿 +✍️ +✍🏻 +✍🏼 +✍🏽 +✍🏾 +✍🏿 +✏️ +✒️ +✔️ +✖️ +✝️ +✡️ +✨ +✳️ +✴️ +❄️ +❇️ +❌ +❎ ❓ ❔ ❕ ❗ -❗ -〰️ -💱 -💲 -⚕️ -♻️ -⚜️ -🔱 -📛 -🔰 -⭕ -✅ -☑️ -✔️ -❌ -❎ +❣️ +❤️ +❤️‍🔥 +❤️‍🩹 +➕ +➖ +➗ +➡️ ➰ ➿ +⤴️ +⤵️ +⬅️ +⬆️ +⬇️ +⬛ +⬜ +⭐ +⭕ +〰️ 〽️ -✳️ -✴️ -❇️ -©️ -®️ -™️ -#️⃣ -*️⃣ -0️⃣ -1️⃣ -2️⃣ -3️⃣ -4️⃣ -5️⃣ -6️⃣ -7️⃣ -8️⃣ -9️⃣ -🔟 -🔠 -🔡 -🔣 -🔤 +㊗️ +㊙️ +🀄 +🃏 🅰️ -🆎 🅱️ +🅾️ +🅿️ +🆎 🆑 🆒 🆓 -ℹ️ 🆔 -Ⓜ️ 🆕 🆖 -🅾️ 🆗 -🅿️ 🆘 🆙 🆚 -🈁 -🈂️ -🈷️ -🈶 -🈯 -🉐 -🈹 -🈚 -🈲 -🉑 -🈸 -🈴 -🈳 -㊗️ -㊙️ -🈺 -🈵 -🔴 -🟠 -🟡 -🟢 -🔵 -🟣 -🟤 -⚫ -⚪ -🟥 -🟧 -🟨 -🟩 -🟦 -🟪 -🟫 -⬛ -⬜ -◼️ -◻️ -◾ -◽ -▪️ -▫️ -🔶 -🔷 -🔸 -🔹 -🔺 -🔻 -💠 -🔘 -🔳 -🔲 -🏁 -🚩 -🎌 -🏴 -🏳️ -🏳️‍🌈 -🏳️‍⚧️ -🏴‍☠️ 🇦🇨 🇦🇩 🇦🇪 @@ -1636,6 +293,7 @@ 🇨🇳 🇨🇴 🇨🇵 +🇨🇶 🇨🇷 🇨🇺 🇨🇻 @@ -1659,7 +317,6 @@ 🇪🇸 🇪🇹 🇪🇺 -🇪🇺 🇫🇮 🇫🇯 🇫🇰 @@ -1668,7 +325,6 @@ 🇫🇷 🇬🇦 🇬🇧 -🇬🇧 🇬🇩 🇬🇪 🇬🇫 @@ -1845,6 +501,3444 @@ 🇿🇦 🇿🇲 🇿🇼 +🈁 +🈂️ +🈚 +🈯 +🈲 +🈳 +🈴 +🈵 +🈶 +🈷️ +🈸 +🈹 +🈺 +🉐 +🉑 +🌀 +🌁 +🌂 +🌃 +🌄 +🌅 +🌆 +🌇 +🌈 +🌉 +🌊 +🌋 +🌌 +🌍 +🌎 +🌏 +🌐 +🌑 +🌒 +🌓 +🌔 +🌕 +🌖 +🌗 +🌘 +🌙 +🌚 +🌛 +🌜 +🌝 +🌞 +🌟 +🌠 +🌡️ +🌤️ +🌥️ +🌦️ +🌧️ +🌨️ +🌩️ +🌪️ +🌫️ +🌬️ +🌭 +🌮 +🌯 +🌰 +🌱 +🌲 +🌳 +🌴 +🌵 +🌶️ +🌷 +🌸 +🌹 +🌺 +🌻 +🌼 +🌽 +🌾 +🌿 +🍀 +🍁 +🍂 +🍃 +🍄 +🍄‍🟫 +🍅 +🍆 +🍇 +🍈 +🍉 +🍊 +🍋 +🍋‍🟩 +🍌 +🍍 +🍎 +🍏 +🍐 +🍑 +🍒 +🍓 +🍔 +🍕 +🍖 +🍗 +🍘 +🍙 +🍚 +🍛 +🍜 +🍝 +🍞 +🍟 +🍠 +🍡 +🍢 +🍣 +🍤 +🍥 +🍦 +🍧 +🍨 +🍩 +🍪 +🍫 +🍬 +🍭 +🍮 +🍯 +🍰 +🍱 +🍲 +🍳 +🍴 +🍵 +🍶 +🍷 +🍸 +🍹 +🍺 +🍻 +🍼 +🍽️ +🍾 +🍿 +🎀 +🎁 +🎂 +🎃 +🎄 +🎅 +🎅🏻 +🎅🏼 +🎅🏽 +🎅🏾 +🎅🏿 +🎆 +🎇 +🎈 +🎉 +🎊 +🎋 +🎌 +🎍 +🎎 +🎏 +🎐 +🎑 +🎒 +🎓 +🎖️ +🎗️ +🎙️ +🎚️ +🎛️ +🎞️ +🎟️ +🎠 +🎡 +🎢 +🎣 +🎤 +🎥 +🎦 +🎧 +🎨 +🎩 +🎪 +🎫 +🎬 +🎭 +🎮 +🎯 +🎰 +🎱 +🎲 +🎳 +🎴 +🎵 +🎶 +🎷 +🎸 +🎹 +🎺 +🎻 +🎼 +🎽 +🎾 +🎿 +🏀 +🏁 +🏂 +🏂🏻 +🏂🏼 +🏂🏽 +🏂🏾 +🏂🏿 +🏃 +🏃‍♀️ +🏃‍♀️‍➡️ +🏃‍♂️ +🏃‍♂️‍➡️ +🏃‍➡️ +🏃🏻 +🏃🏻‍♀️ +🏃🏻‍♀️‍➡️ +🏃🏻‍♂️ +🏃🏻‍♂️‍➡️ +🏃🏻‍➡️ +🏃🏼 +🏃🏼‍♀️ +🏃🏼‍♀️‍➡️ +🏃🏼‍♂️ +🏃🏼‍♂️‍➡️ +🏃🏼‍➡️ +🏃🏽 +🏃🏽‍♀️ +🏃🏽‍♀️‍➡️ +🏃🏽‍♂️ +🏃🏽‍♂️‍➡️ +🏃🏽‍➡️ +🏃🏾 +🏃🏾‍♀️ +🏃🏾‍♀️‍➡️ +🏃🏾‍♂️ +🏃🏾‍♂️‍➡️ +🏃🏾‍➡️ +🏃🏿 +🏃🏿‍♀️ +🏃🏿‍♀️‍➡️ +🏃🏿‍♂️ +🏃🏿‍♂️‍➡️ +🏃🏿‍➡️ +🏄 +🏄‍♀️ +🏄‍♂️ +🏄🏻 +🏄🏻‍♀️ +🏄🏻‍♂️ +🏄🏼 +🏄🏼‍♀️ +🏄🏼‍♂️ +🏄🏽 +🏄🏽‍♀️ +🏄🏽‍♂️ +🏄🏾 +🏄🏾‍♀️ +🏄🏾‍♂️ +🏄🏿 +🏄🏿‍♀️ +🏄🏿‍♂️ +🏅 +🏆 +🏇 +🏇🏻 +🏇🏼 +🏇🏽 +🏇🏾 +🏇🏿 +🏈 +🏉 +🏊 +🏊‍♀️ +🏊‍♂️ +🏊🏻 +🏊🏻‍♀️ +🏊🏻‍♂️ +🏊🏼 +🏊🏼‍♀️ +🏊🏼‍♂️ +🏊🏽 +🏊🏽‍♀️ +🏊🏽‍♂️ +🏊🏾 +🏊🏾‍♀️ +🏊🏾‍♂️ +🏊🏿 +🏊🏿‍♀️ +🏊🏿‍♂️ +🏋️ +🏋️‍♀️ +🏋️‍♂️ +🏋🏻 +🏋🏻‍♀️ +🏋🏻‍♂️ +🏋🏼 +🏋🏼‍♀️ +🏋🏼‍♂️ +🏋🏽 +🏋🏽‍♀️ +🏋🏽‍♂️ +🏋🏾 +🏋🏾‍♀️ +🏋🏾‍♂️ +🏋🏿 +🏋🏿‍♀️ +🏋🏿‍♂️ +🏌️ +🏌️‍♀️ +🏌️‍♂️ +🏌🏻 +🏌🏻‍♀️ +🏌🏻‍♂️ +🏌🏼 +🏌🏼‍♀️ +🏌🏼‍♂️ +🏌🏽 +🏌🏽‍♀️ +🏌🏽‍♂️ +🏌🏾 +🏌🏾‍♀️ +🏌🏾‍♂️ +🏌🏿 +🏌🏿‍♀️ +🏌🏿‍♂️ +🏍️ +🏎️ +🏏 +🏐 +🏑 +🏒 +🏓 +🏔️ +🏕️ +🏖️ +🏗️ +🏘️ +🏙️ +🏚️ +🏛️ +🏜️ +🏝️ +🏞️ +🏟️ +🏠 +🏡 +🏢 +🏣 +🏤 +🏥 +🏦 +🏧 +🏨 +🏩 +🏪 +🏫 +🏬 +🏭 +🏮 +🏯 +🏰 +🏳️ +🏳️‍⚧️ +🏳️‍🌈 +🏴 +🏴‍☠️ 🏴󠁧󠁢󠁥󠁮󠁧󠁿 🏴󠁧󠁢󠁳󠁣󠁴󠁿 -🏴󠁧󠁢󠁷󠁬󠁳󠁿 \ No newline at end of file +🏴󠁧󠁢󠁷󠁬󠁳󠁿 +🏵️ +🏷️ +🏸 +🏹 +🏺 +🐀 +🐁 +🐂 +🐃 +🐄 +🐅 +🐆 +🐇 +🐈 +🐈‍⬛ +🐉 +🐊 +🐋 +🐌 +🐍 +🐎 +🐏 +🐐 +🐑 +🐒 +🐓 +🐔 +🐕 +🐕‍🦺 +🐖 +🐗 +🐘 +🐙 +🐚 +🐛 +🐜 +🐝 +🐞 +🐟 +🐠 +🐡 +🐢 +🐣 +🐤 +🐥 +🐦 +🐦‍⬛ +🐦‍🔥 +🐧 +🐨 +🐩 +🐪 +🐫 +🐬 +🐭 +🐮 +🐯 +🐰 +🐱 +🐲 +🐳 +🐴 +🐵 +🐶 +🐷 +🐸 +🐹 +🐺 +🐻 +🐻‍❄️ +🐼 +🐽 +🐾 +🐿️ +👀 +👁️ +👁️‍🗨️ +👂 +👂🏻 +👂🏼 +👂🏽 +👂🏾 +👂🏿 +👃 +👃🏻 +👃🏼 +👃🏽 +👃🏾 +👃🏿 +👄 +👅 +👆 +👆🏻 +👆🏼 +👆🏽 +👆🏾 +👆🏿 +👇 +👇🏻 +👇🏼 +👇🏽 +👇🏾 +👇🏿 +👈 +👈🏻 +👈🏼 +👈🏽 +👈🏾 +👈🏿 +👉 +👉🏻 +👉🏼 +👉🏽 +👉🏾 +👉🏿 +👊 +👊🏻 +👊🏼 +👊🏽 +👊🏾 +👊🏿 +👋 +👋🏻 +👋🏼 +👋🏽 +👋🏾 +👋🏿 +👌 +👌🏻 +👌🏼 +👌🏽 +👌🏾 +👌🏿 +👍 +👍🏻 +👍🏼 +👍🏽 +👍🏾 +👍🏿 +👎 +👎🏻 +👎🏼 +👎🏽 +👎🏾 +👎🏿 +👏 +👏🏻 +👏🏼 +👏🏽 +👏🏾 +👏🏿 +👐 +👐🏻 +👐🏼 +👐🏽 +👐🏾 +👐🏿 +👑 +👒 +👓 +👔 +👕 +👖 +👗 +👘 +👙 +👚 +👛 +👜 +👝 +👞 +👟 +👠 +👡 +👢 +👣 +👤 +👥 +👦 +👦🏻 +👦🏼 +👦🏽 +👦🏾 +👦🏿 +👧 +👧🏻 +👧🏼 +👧🏽 +👧🏾 +👧🏿 +👨 +👨‍⚕️ +👨‍⚖️ +👨‍✈️ +👨‍❤️‍👨 +👨‍❤️‍💋‍👨 +👨‍🌾 +👨‍🍳 +👨‍🍼 +👨‍🎓 +👨‍🎤 +👨‍🎨 +👨‍🏫 +👨‍🏭 +👨‍👦 +👨‍👦‍👦 +👨‍👧 +👨‍👧‍👦 +👨‍👧‍👧 +👨‍👨‍👦 +👨‍👨‍👦‍👦 +👨‍👨‍👧 +👨‍👨‍👧‍👦 +👨‍👨‍👧‍👧 +👨‍👩‍👦 +👨‍👩‍👦‍👦 +👨‍👩‍👧 +👨‍👩‍👧‍👦 +👨‍👩‍👧‍👧 +👨‍💻 +👨‍💼 +👨‍🔧 +👨‍🔬 +👨‍🚀 +👨‍🚒 +👨‍🦯 +👨‍🦯‍➡️ +👨‍🦰 +👨‍🦱 +👨‍🦲 +👨‍🦳 +👨‍🦼 +👨‍🦼‍➡️ +👨‍🦽 +👨‍🦽‍➡️ +👨🏻 +👨🏻‍⚕️ +👨🏻‍⚖️ +👨🏻‍✈️ +👨🏻‍❤️‍👨🏻 +👨🏻‍❤️‍👨🏼 +👨🏻‍❤️‍👨🏽 +👨🏻‍❤️‍👨🏾 +👨🏻‍❤️‍👨🏿 +👨🏻‍❤️‍💋‍👨🏻 +👨🏻‍❤️‍💋‍👨🏼 +👨🏻‍❤️‍💋‍👨🏽 +👨🏻‍❤️‍💋‍👨🏾 +👨🏻‍❤️‍💋‍👨🏿 +👨🏻‍🌾 +👨🏻‍🍳 +👨🏻‍🍼 +👨🏻‍🎓 +👨🏻‍🎤 +👨🏻‍🎨 +👨🏻‍🏫 +👨🏻‍🏭 +👨🏻‍🐰‍👨🏼 +👨🏻‍🐰‍👨🏽 +👨🏻‍🐰‍👨🏾 +👨🏻‍🐰‍👨🏿 +👨🏻‍💻 +👨🏻‍💼 +👨🏻‍🔧 +👨🏻‍🔬 +👨🏻‍🚀 +👨🏻‍🚒 +👨🏻‍🤝‍👨🏼 +👨🏻‍🤝‍👨🏽 +👨🏻‍🤝‍👨🏾 +👨🏻‍🤝‍👨🏿 +👨🏻‍🦯 +👨🏻‍🦯‍➡️ +👨🏻‍🦰 +👨🏻‍🦱 +👨🏻‍🦲 +👨🏻‍🦳 +👨🏻‍🦼 +👨🏻‍🦼‍➡️ +👨🏻‍🦽 +👨🏻‍🦽‍➡️ +👨🏻‍🫯‍👨🏼 +👨🏻‍🫯‍👨🏽 +👨🏻‍🫯‍👨🏾 +👨🏻‍🫯‍👨🏿 +👨🏼 +👨🏼‍⚕️ +👨🏼‍⚖️ +👨🏼‍✈️ +👨🏼‍❤️‍👨🏻 +👨🏼‍❤️‍👨🏼 +👨🏼‍❤️‍👨🏽 +👨🏼‍❤️‍👨🏾 +👨🏼‍❤️‍👨🏿 +👨🏼‍❤️‍💋‍👨🏻 +👨🏼‍❤️‍💋‍👨🏼 +👨🏼‍❤️‍💋‍👨🏽 +👨🏼‍❤️‍💋‍👨🏾 +👨🏼‍❤️‍💋‍👨🏿 +👨🏼‍🌾 +👨🏼‍🍳 +👨🏼‍🍼 +👨🏼‍🎓 +👨🏼‍🎤 +👨🏼‍🎨 +👨🏼‍🏫 +👨🏼‍🏭 +👨🏼‍🐰‍👨🏻 +👨🏼‍🐰‍👨🏽 +👨🏼‍🐰‍👨🏾 +👨🏼‍🐰‍👨🏿 +👨🏼‍💻 +👨🏼‍💼 +👨🏼‍🔧 +👨🏼‍🔬 +👨🏼‍🚀 +👨🏼‍🚒 +👨🏼‍🤝‍👨🏻 +👨🏼‍🤝‍👨🏽 +👨🏼‍🤝‍👨🏾 +👨🏼‍🤝‍👨🏿 +👨🏼‍🦯 +👨🏼‍🦯‍➡️ +👨🏼‍🦰 +👨🏼‍🦱 +👨🏼‍🦲 +👨🏼‍🦳 +👨🏼‍🦼 +👨🏼‍🦼‍➡️ +👨🏼‍🦽 +👨🏼‍🦽‍➡️ +👨🏼‍🫯‍👨🏻 +👨🏼‍🫯‍👨🏽 +👨🏼‍🫯‍👨🏾 +👨🏼‍🫯‍👨🏿 +👨🏽 +👨🏽‍⚕️ +👨🏽‍⚖️ +👨🏽‍✈️ +👨🏽‍❤️‍👨🏻 +👨🏽‍❤️‍👨🏼 +👨🏽‍❤️‍👨🏽 +👨🏽‍❤️‍👨🏾 +👨🏽‍❤️‍👨🏿 +👨🏽‍❤️‍💋‍👨🏻 +👨🏽‍❤️‍💋‍👨🏼 +👨🏽‍❤️‍💋‍👨🏽 +👨🏽‍❤️‍💋‍👨🏾 +👨🏽‍❤️‍💋‍👨🏿 +👨🏽‍🌾 +👨🏽‍🍳 +👨🏽‍🍼 +👨🏽‍🎓 +👨🏽‍🎤 +👨🏽‍🎨 +👨🏽‍🏫 +👨🏽‍🏭 +👨🏽‍🐰‍👨🏻 +👨🏽‍🐰‍👨🏼 +👨🏽‍🐰‍👨🏾 +👨🏽‍🐰‍👨🏿 +👨🏽‍💻 +👨🏽‍💼 +👨🏽‍🔧 +👨🏽‍🔬 +👨🏽‍🚀 +👨🏽‍🚒 +👨🏽‍🤝‍👨🏻 +👨🏽‍🤝‍👨🏼 +👨🏽‍🤝‍👨🏾 +👨🏽‍🤝‍👨🏿 +👨🏽‍🦯 +👨🏽‍🦯‍➡️ +👨🏽‍🦰 +👨🏽‍🦱 +👨🏽‍🦲 +👨🏽‍🦳 +👨🏽‍🦼 +👨🏽‍🦼‍➡️ +👨🏽‍🦽 +👨🏽‍🦽‍➡️ +👨🏽‍🫯‍👨🏻 +👨🏽‍🫯‍👨🏼 +👨🏽‍🫯‍👨🏾 +👨🏽‍🫯‍👨🏿 +👨🏾 +👨🏾‍⚕️ +👨🏾‍⚖️ +👨🏾‍✈️ +👨🏾‍❤️‍👨🏻 +👨🏾‍❤️‍👨🏼 +👨🏾‍❤️‍👨🏽 +👨🏾‍❤️‍👨🏾 +👨🏾‍❤️‍👨🏿 +👨🏾‍❤️‍💋‍👨🏻 +👨🏾‍❤️‍💋‍👨🏼 +👨🏾‍❤️‍💋‍👨🏽 +👨🏾‍❤️‍💋‍👨🏾 +👨🏾‍❤️‍💋‍👨🏿 +👨🏾‍🌾 +👨🏾‍🍳 +👨🏾‍🍼 +👨🏾‍🎓 +👨🏾‍🎤 +👨🏾‍🎨 +👨🏾‍🏫 +👨🏾‍🏭 +👨🏾‍🐰‍👨🏻 +👨🏾‍🐰‍👨🏼 +👨🏾‍🐰‍👨🏽 +👨🏾‍🐰‍👨🏿 +👨🏾‍💻 +👨🏾‍💼 +👨🏾‍🔧 +👨🏾‍🔬 +👨🏾‍🚀 +👨🏾‍🚒 +👨🏾‍🤝‍👨🏻 +👨🏾‍🤝‍👨🏼 +👨🏾‍🤝‍👨🏽 +👨🏾‍🤝‍👨🏿 +👨🏾‍🦯 +👨🏾‍🦯‍➡️ +👨🏾‍🦰 +👨🏾‍🦱 +👨🏾‍🦲 +👨🏾‍🦳 +👨🏾‍🦼 +👨🏾‍🦼‍➡️ +👨🏾‍🦽 +👨🏾‍🦽‍➡️ +👨🏾‍🫯‍👨🏻 +👨🏾‍🫯‍👨🏼 +👨🏾‍🫯‍👨🏽 +👨🏾‍🫯‍👨🏿 +👨🏿 +👨🏿‍⚕️ +👨🏿‍⚖️ +👨🏿‍✈️ +👨🏿‍❤️‍👨🏻 +👨🏿‍❤️‍👨🏼 +👨🏿‍❤️‍👨🏽 +👨🏿‍❤️‍👨🏾 +👨🏿‍❤️‍👨🏿 +👨🏿‍❤️‍💋‍👨🏻 +👨🏿‍❤️‍💋‍👨🏼 +👨🏿‍❤️‍💋‍👨🏽 +👨🏿‍❤️‍💋‍👨🏾 +👨🏿‍❤️‍💋‍👨🏿 +👨🏿‍🌾 +👨🏿‍🍳 +👨🏿‍🍼 +👨🏿‍🎓 +👨🏿‍🎤 +👨🏿‍🎨 +👨🏿‍🏫 +👨🏿‍🏭 +👨🏿‍🐰‍👨🏻 +👨🏿‍🐰‍👨🏼 +👨🏿‍🐰‍👨🏽 +👨🏿‍🐰‍👨🏾 +👨🏿‍💻 +👨🏿‍💼 +👨🏿‍🔧 +👨🏿‍🔬 +👨🏿‍🚀 +👨🏿‍🚒 +👨🏿‍🤝‍👨🏻 +👨🏿‍🤝‍👨🏼 +👨🏿‍🤝‍👨🏽 +👨🏿‍🤝‍👨🏾 +👨🏿‍🦯 +👨🏿‍🦯‍➡️ +👨🏿‍🦰 +👨🏿‍🦱 +👨🏿‍🦲 +👨🏿‍🦳 +👨🏿‍🦼 +👨🏿‍🦼‍➡️ +👨🏿‍🦽 +👨🏿‍🦽‍➡️ +👨🏿‍🫯‍👨🏻 +👨🏿‍🫯‍👨🏼 +👨🏿‍🫯‍👨🏽 +👨🏿‍🫯‍👨🏾 +👩 +👩‍⚕️ +👩‍⚖️ +👩‍✈️ +👩‍❤️‍👨 +👩‍❤️‍👩 +👩‍❤️‍💋‍👨 +👩‍❤️‍💋‍👩 +👩‍🌾 +👩‍🍳 +👩‍🍼 +👩‍🎓 +👩‍🎤 +👩‍🎨 +👩‍🏫 +👩‍🏭 +👩‍👦 +👩‍👦‍👦 +👩‍👧 +👩‍👧‍👦 +👩‍👧‍👧 +👩‍👩‍👦 +👩‍👩‍👦‍👦 +👩‍👩‍👧 +👩‍👩‍👧‍👦 +👩‍👩‍👧‍👧 +👩‍💻 +👩‍💼 +👩‍🔧 +👩‍🔬 +👩‍🚀 +👩‍🚒 +👩‍🦯 +👩‍🦯‍➡️ +👩‍🦰 +👩‍🦱 +👩‍🦲 +👩‍🦳 +👩‍🦼 +👩‍🦼‍➡️ +👩‍🦽 +👩‍🦽‍➡️ +👩🏻 +👩🏻‍⚕️ +👩🏻‍⚖️ +👩🏻‍✈️ +👩🏻‍❤️‍👨🏻 +👩🏻‍❤️‍👨🏼 +👩🏻‍❤️‍👨🏽 +👩🏻‍❤️‍👨🏾 +👩🏻‍❤️‍👨🏿 +👩🏻‍❤️‍👩🏻 +👩🏻‍❤️‍👩🏼 +👩🏻‍❤️‍👩🏽 +👩🏻‍❤️‍👩🏾 +👩🏻‍❤️‍👩🏿 +👩🏻‍❤️‍💋‍👨🏻 +👩🏻‍❤️‍💋‍👨🏼 +👩🏻‍❤️‍💋‍👨🏽 +👩🏻‍❤️‍💋‍👨🏾 +👩🏻‍❤️‍💋‍👨🏿 +👩🏻‍❤️‍💋‍👩🏻 +👩🏻‍❤️‍💋‍👩🏼 +👩🏻‍❤️‍💋‍👩🏽 +👩🏻‍❤️‍💋‍👩🏾 +👩🏻‍❤️‍💋‍👩🏿 +👩🏻‍🌾 +👩🏻‍🍳 +👩🏻‍🍼 +👩🏻‍🎓 +👩🏻‍🎤 +👩🏻‍🎨 +👩🏻‍🏫 +👩🏻‍🏭 +👩🏻‍🐰‍👩🏼 +👩🏻‍🐰‍👩🏽 +👩🏻‍🐰‍👩🏾 +👩🏻‍🐰‍👩🏿 +👩🏻‍💻 +👩🏻‍💼 +👩🏻‍🔧 +👩🏻‍🔬 +👩🏻‍🚀 +👩🏻‍🚒 +👩🏻‍🤝‍👨🏼 +👩🏻‍🤝‍👨🏽 +👩🏻‍🤝‍👨🏾 +👩🏻‍🤝‍👨🏿 +👩🏻‍🤝‍👩🏼 +👩🏻‍🤝‍👩🏽 +👩🏻‍🤝‍👩🏾 +👩🏻‍🤝‍👩🏿 +👩🏻‍🦯 +👩🏻‍🦯‍➡️ +👩🏻‍🦰 +👩🏻‍🦱 +👩🏻‍🦲 +👩🏻‍🦳 +👩🏻‍🦼 +👩🏻‍🦼‍➡️ +👩🏻‍🦽 +👩🏻‍🦽‍➡️ +👩🏻‍🫯‍👩🏼 +👩🏻‍🫯‍👩🏽 +👩🏻‍🫯‍👩🏾 +👩🏻‍🫯‍👩🏿 +👩🏼 +👩🏼‍⚕️ +👩🏼‍⚖️ +👩🏼‍✈️ +👩🏼‍❤️‍👨🏻 +👩🏼‍❤️‍👨🏼 +👩🏼‍❤️‍👨🏽 +👩🏼‍❤️‍👨🏾 +👩🏼‍❤️‍👨🏿 +👩🏼‍❤️‍👩🏻 +👩🏼‍❤️‍👩🏼 +👩🏼‍❤️‍👩🏽 +👩🏼‍❤️‍👩🏾 +👩🏼‍❤️‍👩🏿 +👩🏼‍❤️‍💋‍👨🏻 +👩🏼‍❤️‍💋‍👨🏼 +👩🏼‍❤️‍💋‍👨🏽 +👩🏼‍❤️‍💋‍👨🏾 +👩🏼‍❤️‍💋‍👨🏿 +👩🏼‍❤️‍💋‍👩🏻 +👩🏼‍❤️‍💋‍👩🏼 +👩🏼‍❤️‍💋‍👩🏽 +👩🏼‍❤️‍💋‍👩🏾 +👩🏼‍❤️‍💋‍👩🏿 +👩🏼‍🌾 +👩🏼‍🍳 +👩🏼‍🍼 +👩🏼‍🎓 +👩🏼‍🎤 +👩🏼‍🎨 +👩🏼‍🏫 +👩🏼‍🏭 +👩🏼‍🐰‍👩🏻 +👩🏼‍🐰‍👩🏽 +👩🏼‍🐰‍👩🏾 +👩🏼‍🐰‍👩🏿 +👩🏼‍💻 +👩🏼‍💼 +👩🏼‍🔧 +👩🏼‍🔬 +👩🏼‍🚀 +👩🏼‍🚒 +👩🏼‍🤝‍👨🏻 +👩🏼‍🤝‍👨🏽 +👩🏼‍🤝‍👨🏾 +👩🏼‍🤝‍👨🏿 +👩🏼‍🤝‍👩🏻 +👩🏼‍🤝‍👩🏽 +👩🏼‍🤝‍👩🏾 +👩🏼‍🤝‍👩🏿 +👩🏼‍🦯 +👩🏼‍🦯‍➡️ +👩🏼‍🦰 +👩🏼‍🦱 +👩🏼‍🦲 +👩🏼‍🦳 +👩🏼‍🦼 +👩🏼‍🦼‍➡️ +👩🏼‍🦽 +👩🏼‍🦽‍➡️ +👩🏼‍🫯‍👩🏻 +👩🏼‍🫯‍👩🏽 +👩🏼‍🫯‍👩🏾 +👩🏼‍🫯‍👩🏿 +👩🏽 +👩🏽‍⚕️ +👩🏽‍⚖️ +👩🏽‍✈️ +👩🏽‍❤️‍👨🏻 +👩🏽‍❤️‍👨🏼 +👩🏽‍❤️‍👨🏽 +👩🏽‍❤️‍👨🏾 +👩🏽‍❤️‍👨🏿 +👩🏽‍❤️‍👩🏻 +👩🏽‍❤️‍👩🏼 +👩🏽‍❤️‍👩🏽 +👩🏽‍❤️‍👩🏾 +👩🏽‍❤️‍👩🏿 +👩🏽‍❤️‍💋‍👨🏻 +👩🏽‍❤️‍💋‍👨🏼 +👩🏽‍❤️‍💋‍👨🏽 +👩🏽‍❤️‍💋‍👨🏾 +👩🏽‍❤️‍💋‍👨🏿 +👩🏽‍❤️‍💋‍👩🏻 +👩🏽‍❤️‍💋‍👩🏼 +👩🏽‍❤️‍💋‍👩🏽 +👩🏽‍❤️‍💋‍👩🏾 +👩🏽‍❤️‍💋‍👩🏿 +👩🏽‍🌾 +👩🏽‍🍳 +👩🏽‍🍼 +👩🏽‍🎓 +👩🏽‍🎤 +👩🏽‍🎨 +👩🏽‍🏫 +👩🏽‍🏭 +👩🏽‍🐰‍👩🏻 +👩🏽‍🐰‍👩🏼 +👩🏽‍🐰‍👩🏾 +👩🏽‍🐰‍👩🏿 +👩🏽‍💻 +👩🏽‍💼 +👩🏽‍🔧 +👩🏽‍🔬 +👩🏽‍🚀 +👩🏽‍🚒 +👩🏽‍🤝‍👨🏻 +👩🏽‍🤝‍👨🏼 +👩🏽‍🤝‍👨🏾 +👩🏽‍🤝‍👨🏿 +👩🏽‍🤝‍👩🏻 +👩🏽‍🤝‍👩🏼 +👩🏽‍🤝‍👩🏾 +👩🏽‍🤝‍👩🏿 +👩🏽‍🦯 +👩🏽‍🦯‍➡️ +👩🏽‍🦰 +👩🏽‍🦱 +👩🏽‍🦲 +👩🏽‍🦳 +👩🏽‍🦼 +👩🏽‍🦼‍➡️ +👩🏽‍🦽 +👩🏽‍🦽‍➡️ +👩🏽‍🫯‍👩🏻 +👩🏽‍🫯‍👩🏼 +👩🏽‍🫯‍👩🏾 +👩🏽‍🫯‍👩🏿 +👩🏾 +👩🏾‍⚕️ +👩🏾‍⚖️ +👩🏾‍✈️ +👩🏾‍❤️‍👨🏻 +👩🏾‍❤️‍👨🏼 +👩🏾‍❤️‍👨🏽 +👩🏾‍❤️‍👨🏾 +👩🏾‍❤️‍👨🏿 +👩🏾‍❤️‍👩🏻 +👩🏾‍❤️‍👩🏼 +👩🏾‍❤️‍👩🏽 +👩🏾‍❤️‍👩🏾 +👩🏾‍❤️‍👩🏿 +👩🏾‍❤️‍💋‍👨🏻 +👩🏾‍❤️‍💋‍👨🏼 +👩🏾‍❤️‍💋‍👨🏽 +👩🏾‍❤️‍💋‍👨🏾 +👩🏾‍❤️‍💋‍👨🏿 +👩🏾‍❤️‍💋‍👩🏻 +👩🏾‍❤️‍💋‍👩🏼 +👩🏾‍❤️‍💋‍👩🏽 +👩🏾‍❤️‍💋‍👩🏾 +👩🏾‍❤️‍💋‍👩🏿 +👩🏾‍🌾 +👩🏾‍🍳 +👩🏾‍🍼 +👩🏾‍🎓 +👩🏾‍🎤 +👩🏾‍🎨 +👩🏾‍🏫 +👩🏾‍🏭 +👩🏾‍🐰‍👩🏻 +👩🏾‍🐰‍👩🏼 +👩🏾‍🐰‍👩🏽 +👩🏾‍🐰‍👩🏿 +👩🏾‍💻 +👩🏾‍💼 +👩🏾‍🔧 +👩🏾‍🔬 +👩🏾‍🚀 +👩🏾‍🚒 +👩🏾‍🤝‍👨🏻 +👩🏾‍🤝‍👨🏼 +👩🏾‍🤝‍👨🏽 +👩🏾‍🤝‍👨🏿 +👩🏾‍🤝‍👩🏻 +👩🏾‍🤝‍👩🏼 +👩🏾‍🤝‍👩🏽 +👩🏾‍🤝‍👩🏿 +👩🏾‍🦯 +👩🏾‍🦯‍➡️ +👩🏾‍🦰 +👩🏾‍🦱 +👩🏾‍🦲 +👩🏾‍🦳 +👩🏾‍🦼 +👩🏾‍🦼‍➡️ +👩🏾‍🦽 +👩🏾‍🦽‍➡️ +👩🏾‍🫯‍👩🏻 +👩🏾‍🫯‍👩🏼 +👩🏾‍🫯‍👩🏽 +👩🏾‍🫯‍👩🏿 +👩🏿 +👩🏿‍⚕️ +👩🏿‍⚖️ +👩🏿‍✈️ +👩🏿‍❤️‍👨🏻 +👩🏿‍❤️‍👨🏼 +👩🏿‍❤️‍👨🏽 +👩🏿‍❤️‍👨🏾 +👩🏿‍❤️‍👨🏿 +👩🏿‍❤️‍👩🏻 +👩🏿‍❤️‍👩🏼 +👩🏿‍❤️‍👩🏽 +👩🏿‍❤️‍👩🏾 +👩🏿‍❤️‍👩🏿 +👩🏿‍❤️‍💋‍👨🏻 +👩🏿‍❤️‍💋‍👨🏼 +👩🏿‍❤️‍💋‍👨🏽 +👩🏿‍❤️‍💋‍👨🏾 +👩🏿‍❤️‍💋‍👨🏿 +👩🏿‍❤️‍💋‍👩🏻 +👩🏿‍❤️‍💋‍👩🏼 +👩🏿‍❤️‍💋‍👩🏽 +👩🏿‍❤️‍💋‍👩🏾 +👩🏿‍❤️‍💋‍👩🏿 +👩🏿‍🌾 +👩🏿‍🍳 +👩🏿‍🍼 +👩🏿‍🎓 +👩🏿‍🎤 +👩🏿‍🎨 +👩🏿‍🏫 +👩🏿‍🏭 +👩🏿‍🐰‍👩🏻 +👩🏿‍🐰‍👩🏼 +👩🏿‍🐰‍👩🏽 +👩🏿‍🐰‍👩🏾 +👩🏿‍💻 +👩🏿‍💼 +👩🏿‍🔧 +👩🏿‍🔬 +👩🏿‍🚀 +👩🏿‍🚒 +👩🏿‍🤝‍👨🏻 +👩🏿‍🤝‍👨🏼 +👩🏿‍🤝‍👨🏽 +👩🏿‍🤝‍👨🏾 +👩🏿‍🤝‍👩🏻 +👩🏿‍🤝‍👩🏼 +👩🏿‍🤝‍👩🏽 +👩🏿‍🤝‍👩🏾 +👩🏿‍🦯 +👩🏿‍🦯‍➡️ +👩🏿‍🦰 +👩🏿‍🦱 +👩🏿‍🦲 +👩🏿‍🦳 +👩🏿‍🦼 +👩🏿‍🦼‍➡️ +👩🏿‍🦽 +👩🏿‍🦽‍➡️ +👩🏿‍🫯‍👩🏻 +👩🏿‍🫯‍👩🏼 +👩🏿‍🫯‍👩🏽 +👩🏿‍🫯‍👩🏾 +👪 +👫 +👫🏻 +👫🏼 +👫🏽 +👫🏾 +👫🏿 +👬 +👬🏻 +👬🏼 +👬🏽 +👬🏾 +👬🏿 +👭 +👭🏻 +👭🏼 +👭🏽 +👭🏾 +👭🏿 +👮 +👮‍♀️ +👮‍♂️ +👮🏻 +👮🏻‍♀️ +👮🏻‍♂️ +👮🏼 +👮🏼‍♀️ +👮🏼‍♂️ +👮🏽 +👮🏽‍♀️ +👮🏽‍♂️ +👮🏾 +👮🏾‍♀️ +👮🏾‍♂️ +👮🏿 +👮🏿‍♀️ +👮🏿‍♂️ +👯 +👯‍♀️ +👯‍♂️ +👯🏻 +👯🏻‍♀️ +👯🏻‍♂️ +👯🏼 +👯🏼‍♀️ +👯🏼‍♂️ +👯🏽 +👯🏽‍♀️ +👯🏽‍♂️ +👯🏾 +👯🏾‍♀️ +👯🏾‍♂️ +👯🏿 +👯🏿‍♀️ +👯🏿‍♂️ +👰 +👰‍♀️ +👰‍♂️ +👰🏻 +👰🏻‍♀️ +👰🏻‍♂️ +👰🏼 +👰🏼‍♀️ +👰🏼‍♂️ +👰🏽 +👰🏽‍♀️ +👰🏽‍♂️ +👰🏾 +👰🏾‍♀️ +👰🏾‍♂️ +👰🏿 +👰🏿‍♀️ +👰🏿‍♂️ +👱 +👱‍♀️ +👱‍♂️ +👱🏻 +👱🏻‍♀️ +👱🏻‍♂️ +👱🏼 +👱🏼‍♀️ +👱🏼‍♂️ +👱🏽 +👱🏽‍♀️ +👱🏽‍♂️ +👱🏾 +👱🏾‍♀️ +👱🏾‍♂️ +👱🏿 +👱🏿‍♀️ +👱🏿‍♂️ +👲 +👲🏻 +👲🏼 +👲🏽 +👲🏾 +👲🏿 +👳 +👳‍♀️ +👳‍♂️ +👳🏻 +👳🏻‍♀️ +👳🏻‍♂️ +👳🏼 +👳🏼‍♀️ +👳🏼‍♂️ +👳🏽 +👳🏽‍♀️ +👳🏽‍♂️ +👳🏾 +👳🏾‍♀️ +👳🏾‍♂️ +👳🏿 +👳🏿‍♀️ +👳🏿‍♂️ +👴 +👴🏻 +👴🏼 +👴🏽 +👴🏾 +👴🏿 +👵 +👵🏻 +👵🏼 +👵🏽 +👵🏾 +👵🏿 +👶 +👶🏻 +👶🏼 +👶🏽 +👶🏾 +👶🏿 +👷 +👷‍♀️ +👷‍♂️ +👷🏻 +👷🏻‍♀️ +👷🏻‍♂️ +👷🏼 +👷🏼‍♀️ +👷🏼‍♂️ +👷🏽 +👷🏽‍♀️ +👷🏽‍♂️ +👷🏾 +👷🏾‍♀️ +👷🏾‍♂️ +👷🏿 +👷🏿‍♀️ +👷🏿‍♂️ +👸 +👸🏻 +👸🏼 +👸🏽 +👸🏾 +👸🏿 +👹 +👺 +👻 +👼 +👼🏻 +👼🏼 +👼🏽 +👼🏾 +👼🏿 +👽 +👾 +👿 +💀 +💁 +💁‍♀️ +💁‍♂️ +💁🏻 +💁🏻‍♀️ +💁🏻‍♂️ +💁🏼 +💁🏼‍♀️ +💁🏼‍♂️ +💁🏽 +💁🏽‍♀️ +💁🏽‍♂️ +💁🏾 +💁🏾‍♀️ +💁🏾‍♂️ +💁🏿 +💁🏿‍♀️ +💁🏿‍♂️ +💂 +💂‍♀️ +💂‍♂️ +💂🏻 +💂🏻‍♀️ +💂🏻‍♂️ +💂🏼 +💂🏼‍♀️ +💂🏼‍♂️ +💂🏽 +💂🏽‍♀️ +💂🏽‍♂️ +💂🏾 +💂🏾‍♀️ +💂🏾‍♂️ +💂🏿 +💂🏿‍♀️ +💂🏿‍♂️ +💃 +💃🏻 +💃🏼 +💃🏽 +💃🏾 +💃🏿 +💄 +💅 +💅🏻 +💅🏼 +💅🏽 +💅🏾 +💅🏿 +💆 +💆‍♀️ +💆‍♂️ +💆🏻 +💆🏻‍♀️ +💆🏻‍♂️ +💆🏼 +💆🏼‍♀️ +💆🏼‍♂️ +💆🏽 +💆🏽‍♀️ +💆🏽‍♂️ +💆🏾 +💆🏾‍♀️ +💆🏾‍♂️ +💆🏿 +💆🏿‍♀️ +💆🏿‍♂️ +💇 +💇‍♀️ +💇‍♂️ +💇🏻 +💇🏻‍♀️ +💇🏻‍♂️ +💇🏼 +💇🏼‍♀️ +💇🏼‍♂️ +💇🏽 +💇🏽‍♀️ +💇🏽‍♂️ +💇🏾 +💇🏾‍♀️ +💇🏾‍♂️ +💇🏿 +💇🏿‍♀️ +💇🏿‍♂️ +💈 +💉 +💊 +💋 +💌 +💍 +💎 +💏 +💏🏻 +💏🏼 +💏🏽 +💏🏾 +💏🏿 +💐 +💑 +💑🏻 +💑🏼 +💑🏽 +💑🏾 +💑🏿 +💒 +💓 +💔 +💕 +💖 +💗 +💘 +💙 +💚 +💛 +💜 +💝 +💞 +💟 +💠 +💡 +💢 +💣 +💤 +💥 +💦 +💧 +💨 +💩 +💪 +💪🏻 +💪🏼 +💪🏽 +💪🏾 +💪🏿 +💫 +💬 +💭 +💮 +💯 +💰 +💱 +💲 +💳 +💴 +💵 +💶 +💷 +💸 +💹 +💺 +💻 +💼 +💽 +💾 +💿 +📀 +📁 +📂 +📃 +📄 +📅 +📆 +📇 +📈 +📉 +📊 +📋 +📌 +📍 +📎 +📏 +📐 +📑 +📒 +📓 +📔 +📕 +📖 +📗 +📘 +📙 +📚 +📛 +📜 +📝 +📞 +📟 +📠 +📡 +📢 +📣 +📤 +📥 +📦 +📧 +📨 +📩 +📪 +📫 +📬 +📭 +📮 +📯 +📰 +📱 +📲 +📳 +📴 +📵 +📶 +📷 +📸 +📹 +📺 +📻 +📼 +📽️ +📿 +🔀 +🔁 +🔂 +🔃 +🔄 +🔅 +🔆 +🔇 +🔈 +🔉 +🔊 +🔋 +🔌 +🔍 +🔎 +🔏 +🔐 +🔑 +🔒 +🔓 +🔔 +🔕 +🔖 +🔗 +🔘 +🔙 +🔚 +🔛 +🔜 +🔝 +🔞 +🔟 +🔠 +🔡 +🔢 +🔣 +🔤 +🔥 +🔦 +🔧 +🔨 +🔩 +🔪 +🔫 +🔬 +🔭 +🔮 +🔯 +🔰 +🔱 +🔲 +🔳 +🔴 +🔵 +🔶 +🔷 +🔸 +🔹 +🔺 +🔻 +🔼 +🔽 +🕉️ +🕊️ +🕋 +🕌 +🕍 +🕎 +🕐 +🕑 +🕒 +🕓 +🕔 +🕕 +🕖 +🕗 +🕘 +🕙 +🕚 +🕛 +🕜 +🕝 +🕞 +🕟 +🕠 +🕡 +🕢 +🕣 +🕤 +🕥 +🕦 +🕧 +🕯️ +🕰️ +🕳️ +🕴️ +🕴🏻 +🕴🏼 +🕴🏽 +🕴🏾 +🕴🏿 +🕵️ +🕵️‍♀️ +🕵️‍♂️ +🕵🏻 +🕵🏻‍♀️ +🕵🏻‍♂️ +🕵🏼 +🕵🏼‍♀️ +🕵🏼‍♂️ +🕵🏽 +🕵🏽‍♀️ +🕵🏽‍♂️ +🕵🏾 +🕵🏾‍♀️ +🕵🏾‍♂️ +🕵🏿 +🕵🏿‍♀️ +🕵🏿‍♂️ +🕶️ +🕷️ +🕸️ +🕹️ +🕺 +🕺🏻 +🕺🏼 +🕺🏽 +🕺🏾 +🕺🏿 +🖇️ +🖊️ +🖋️ +🖌️ +🖍️ +🖐️ +🖐🏻 +🖐🏼 +🖐🏽 +🖐🏾 +🖐🏿 +🖕 +🖕🏻 +🖕🏼 +🖕🏽 +🖕🏾 +🖕🏿 +🖖 +🖖🏻 +🖖🏼 +🖖🏽 +🖖🏾 +🖖🏿 +🖤 +🖥️ +🖨️ +🖱️ +🖲️ +🖼️ +🗂️ +🗃️ +🗄️ +🗑️ +🗒️ +🗓️ +🗜️ +🗝️ +🗞️ +🗡️ +🗣️ +🗨️ +🗯️ +🗳️ +🗺️ +🗻 +🗼 +🗽 +🗾 +🗿 +😀 +😁 +😂 +😃 +😄 +😅 +😆 +😇 +😈 +😉 +😊 +😋 +😌 +😍 +😎 +😏 +😐 +😑 +😒 +😓 +😔 +😕 +😖 +😗 +😘 +😙 +😚 +😛 +😜 +😝 +😞 +😟 +😠 +😡 +😢 +😣 +😤 +😥 +😦 +😧 +😨 +😩 +😪 +😫 +😬 +😭 +😮 +😮‍💨 +😯 +😰 +😱 +😲 +😳 +😴 +😵 +😵‍💫 +😶 +😶‍🌫️ +😷 +😸 +😹 +😺 +😻 +😼 +😽 +😾 +😿 +🙀 +🙁 +🙂 +🙂‍↔️ +🙂‍↕️ +🙃 +🙄 +🙅 +🙅‍♀️ +🙅‍♂️ +🙅🏻 +🙅🏻‍♀️ +🙅🏻‍♂️ +🙅🏼 +🙅🏼‍♀️ +🙅🏼‍♂️ +🙅🏽 +🙅🏽‍♀️ +🙅🏽‍♂️ +🙅🏾 +🙅🏾‍♀️ +🙅🏾‍♂️ +🙅🏿 +🙅🏿‍♀️ +🙅🏿‍♂️ +🙆 +🙆‍♀️ +🙆‍♂️ +🙆🏻 +🙆🏻‍♀️ +🙆🏻‍♂️ +🙆🏼 +🙆🏼‍♀️ +🙆🏼‍♂️ +🙆🏽 +🙆🏽‍♀️ +🙆🏽‍♂️ +🙆🏾 +🙆🏾‍♀️ +🙆🏾‍♂️ +🙆🏿 +🙆🏿‍♀️ +🙆🏿‍♂️ +🙇 +🙇‍♀️ +🙇‍♂️ +🙇🏻 +🙇🏻‍♀️ +🙇🏻‍♂️ +🙇🏼 +🙇🏼‍♀️ +🙇🏼‍♂️ +🙇🏽 +🙇🏽‍♀️ +🙇🏽‍♂️ +🙇🏾 +🙇🏾‍♀️ +🙇🏾‍♂️ +🙇🏿 +🙇🏿‍♀️ +🙇🏿‍♂️ +🙈 +🙉 +🙊 +🙋 +🙋‍♀️ +🙋‍♂️ +🙋🏻 +🙋🏻‍♀️ +🙋🏻‍♂️ +🙋🏼 +🙋🏼‍♀️ +🙋🏼‍♂️ +🙋🏽 +🙋🏽‍♀️ +🙋🏽‍♂️ +🙋🏾 +🙋🏾‍♀️ +🙋🏾‍♂️ +🙋🏿 +🙋🏿‍♀️ +🙋🏿‍♂️ +🙌 +🙌🏻 +🙌🏼 +🙌🏽 +🙌🏾 +🙌🏿 +🙍 +🙍‍♀️ +🙍‍♂️ +🙍🏻 +🙍🏻‍♀️ +🙍🏻‍♂️ +🙍🏼 +🙍🏼‍♀️ +🙍🏼‍♂️ +🙍🏽 +🙍🏽‍♀️ +🙍🏽‍♂️ +🙍🏾 +🙍🏾‍♀️ +🙍🏾‍♂️ +🙍🏿 +🙍🏿‍♀️ +🙍🏿‍♂️ +🙎 +🙎‍♀️ +🙎‍♂️ +🙎🏻 +🙎🏻‍♀️ +🙎🏻‍♂️ +🙎🏼 +🙎🏼‍♀️ +🙎🏼‍♂️ +🙎🏽 +🙎🏽‍♀️ +🙎🏽‍♂️ +🙎🏾 +🙎🏾‍♀️ +🙎🏾‍♂️ +🙎🏿 +🙎🏿‍♀️ +🙎🏿‍♂️ +🙏 +🙏🏻 +🙏🏼 +🙏🏽 +🙏🏾 +🙏🏿 +🚀 +🚁 +🚂 +🚃 +🚄 +🚅 +🚆 +🚇 +🚈 +🚉 +🚊 +🚋 +🚌 +🚍 +🚎 +🚏 +🚐 +🚑 +🚒 +🚓 +🚔 +🚕 +🚖 +🚗 +🚘 +🚙 +🚚 +🚛 +🚜 +🚝 +🚞 +🚟 +🚠 +🚡 +🚢 +🚣 +🚣‍♀️ +🚣‍♂️ +🚣🏻 +🚣🏻‍♀️ +🚣🏻‍♂️ +🚣🏼 +🚣🏼‍♀️ +🚣🏼‍♂️ +🚣🏽 +🚣🏽‍♀️ +🚣🏽‍♂️ +🚣🏾 +🚣🏾‍♀️ +🚣🏾‍♂️ +🚣🏿 +🚣🏿‍♀️ +🚣🏿‍♂️ +🚤 +🚥 +🚦 +🚧 +🚨 +🚩 +🚪 +🚫 +🚬 +🚭 +🚮 +🚯 +🚰 +🚱 +🚲 +🚳 +🚴 +🚴‍♀️ +🚴‍♂️ +🚴🏻 +🚴🏻‍♀️ +🚴🏻‍♂️ +🚴🏼 +🚴🏼‍♀️ +🚴🏼‍♂️ +🚴🏽 +🚴🏽‍♀️ +🚴🏽‍♂️ +🚴🏾 +🚴🏾‍♀️ +🚴🏾‍♂️ +🚴🏿 +🚴🏿‍♀️ +🚴🏿‍♂️ +🚵 +🚵‍♀️ +🚵‍♂️ +🚵🏻 +🚵🏻‍♀️ +🚵🏻‍♂️ +🚵🏼 +🚵🏼‍♀️ +🚵🏼‍♂️ +🚵🏽 +🚵🏽‍♀️ +🚵🏽‍♂️ +🚵🏾 +🚵🏾‍♀️ +🚵🏾‍♂️ +🚵🏿 +🚵🏿‍♀️ +🚵🏿‍♂️ +🚶 +🚶‍♀️ +🚶‍♀️‍➡️ +🚶‍♂️ +🚶‍♂️‍➡️ +🚶‍➡️ +🚶🏻 +🚶🏻‍♀️ +🚶🏻‍♀️‍➡️ +🚶🏻‍♂️ +🚶🏻‍♂️‍➡️ +🚶🏻‍➡️ +🚶🏼 +🚶🏼‍♀️ +🚶🏼‍♀️‍➡️ +🚶🏼‍♂️ +🚶🏼‍♂️‍➡️ +🚶🏼‍➡️ +🚶🏽 +🚶🏽‍♀️ +🚶🏽‍♀️‍➡️ +🚶🏽‍♂️ +🚶🏽‍♂️‍➡️ +🚶🏽‍➡️ +🚶🏾 +🚶🏾‍♀️ +🚶🏾‍♀️‍➡️ +🚶🏾‍♂️ +🚶🏾‍♂️‍➡️ +🚶🏾‍➡️ +🚶🏿 +🚶🏿‍♀️ +🚶🏿‍♀️‍➡️ +🚶🏿‍♂️ +🚶🏿‍♂️‍➡️ +🚶🏿‍➡️ +🚷 +🚸 +🚹 +🚺 +🚻 +🚼 +🚽 +🚾 +🚿 +🛀 +🛀🏻 +🛀🏼 +🛀🏽 +🛀🏾 +🛀🏿 +🛁 +🛂 +🛃 +🛄 +🛅 +🛋️ +🛌 +🛌🏻 +🛌🏼 +🛌🏽 +🛌🏾 +🛌🏿 +🛍️ +🛎️ +🛏️ +🛐 +🛑 +🛒 +🛕 +🛖 +🛗 +🛘 +🛜 +🛝 +🛞 +🛟 +🛠️ +🛡️ +🛢️ +🛣️ +🛤️ +🛥️ +🛩️ +🛫 +🛬 +🛰️ +🛳️ +🛴 +🛵 +🛶 +🛷 +🛸 +🛹 +🛺 +🛻 +🛼 +🟠 +🟡 +🟢 +🟣 +🟤 +🟥 +🟦 +🟧 +🟨 +🟩 +🟪 +🟫 +🟰 +🤌 +🤌🏻 +🤌🏼 +🤌🏽 +🤌🏾 +🤌🏿 +🤍 +🤎 +🤏 +🤏🏻 +🤏🏼 +🤏🏽 +🤏🏾 +🤏🏿 +🤐 +🤑 +🤒 +🤓 +🤔 +🤕 +🤖 +🤗 +🤘 +🤘🏻 +🤘🏼 +🤘🏽 +🤘🏾 +🤘🏿 +🤙 +🤙🏻 +🤙🏼 +🤙🏽 +🤙🏾 +🤙🏿 +🤚 +🤚🏻 +🤚🏼 +🤚🏽 +🤚🏾 +🤚🏿 +🤛 +🤛🏻 +🤛🏼 +🤛🏽 +🤛🏾 +🤛🏿 +🤜 +🤜🏻 +🤜🏼 +🤜🏽 +🤜🏾 +🤜🏿 +🤝 +🤝🏻 +🤝🏼 +🤝🏽 +🤝🏾 +🤝🏿 +🤞 +🤞🏻 +🤞🏼 +🤞🏽 +🤞🏾 +🤞🏿 +🤟 +🤟🏻 +🤟🏼 +🤟🏽 +🤟🏾 +🤟🏿 +🤠 +🤡 +🤢 +🤣 +🤤 +🤥 +🤦 +🤦‍♀️ +🤦‍♂️ +🤦🏻 +🤦🏻‍♀️ +🤦🏻‍♂️ +🤦🏼 +🤦🏼‍♀️ +🤦🏼‍♂️ +🤦🏽 +🤦🏽‍♀️ +🤦🏽‍♂️ +🤦🏾 +🤦🏾‍♀️ +🤦🏾‍♂️ +🤦🏿 +🤦🏿‍♀️ +🤦🏿‍♂️ +🤧 +🤨 +🤩 +🤪 +🤫 +🤬 +🤭 +🤮 +🤯 +🤰 +🤰🏻 +🤰🏼 +🤰🏽 +🤰🏾 +🤰🏿 +🤱 +🤱🏻 +🤱🏼 +🤱🏽 +🤱🏾 +🤱🏿 +🤲 +🤲🏻 +🤲🏼 +🤲🏽 +🤲🏾 +🤲🏿 +🤳 +🤳🏻 +🤳🏼 +🤳🏽 +🤳🏾 +🤳🏿 +🤴 +🤴🏻 +🤴🏼 +🤴🏽 +🤴🏾 +🤴🏿 +🤵 +🤵‍♀️ +🤵‍♂️ +🤵🏻 +🤵🏻‍♀️ +🤵🏻‍♂️ +🤵🏼 +🤵🏼‍♀️ +🤵🏼‍♂️ +🤵🏽 +🤵🏽‍♀️ +🤵🏽‍♂️ +🤵🏾 +🤵🏾‍♀️ +🤵🏾‍♂️ +🤵🏿 +🤵🏿‍♀️ +🤵🏿‍♂️ +🤶 +🤶🏻 +🤶🏼 +🤶🏽 +🤶🏾 +🤶🏿 +🤷 +🤷‍♀️ +🤷‍♂️ +🤷🏻 +🤷🏻‍♀️ +🤷🏻‍♂️ +🤷🏼 +🤷🏼‍♀️ +🤷🏼‍♂️ +🤷🏽 +🤷🏽‍♀️ +🤷🏽‍♂️ +🤷🏾 +🤷🏾‍♀️ +🤷🏾‍♂️ +🤷🏿 +🤷🏿‍♀️ +🤷🏿‍♂️ +🤸 +🤸‍♀️ +🤸‍♂️ +🤸🏻 +🤸🏻‍♀️ +🤸🏻‍♂️ +🤸🏼 +🤸🏼‍♀️ +🤸🏼‍♂️ +🤸🏽 +🤸🏽‍♀️ +🤸🏽‍♂️ +🤸🏾 +🤸🏾‍♀️ +🤸🏾‍♂️ +🤸🏿 +🤸🏿‍♀️ +🤸🏿‍♂️ +🤹 +🤹‍♀️ +🤹‍♂️ +🤹🏻 +🤹🏻‍♀️ +🤹🏻‍♂️ +🤹🏼 +🤹🏼‍♀️ +🤹🏼‍♂️ +🤹🏽 +🤹🏽‍♀️ +🤹🏽‍♂️ +🤹🏾 +🤹🏾‍♀️ +🤹🏾‍♂️ +🤹🏿 +🤹🏿‍♀️ +🤹🏿‍♂️ +🤺 +🤼 +🤼‍♀️ +🤼‍♂️ +🤼🏻 +🤼🏻‍♀️ +🤼🏻‍♂️ +🤼🏼 +🤼🏼‍♀️ +🤼🏼‍♂️ +🤼🏽 +🤼🏽‍♀️ +🤼🏽‍♂️ +🤼🏾 +🤼🏾‍♀️ +🤼🏾‍♂️ +🤼🏿 +🤼🏿‍♀️ +🤼🏿‍♂️ +🤽 +🤽‍♀️ +🤽‍♂️ +🤽🏻 +🤽🏻‍♀️ +🤽🏻‍♂️ +🤽🏼 +🤽🏼‍♀️ +🤽🏼‍♂️ +🤽🏽 +🤽🏽‍♀️ +🤽🏽‍♂️ +🤽🏾 +🤽🏾‍♀️ +🤽🏾‍♂️ +🤽🏿 +🤽🏿‍♀️ +🤽🏿‍♂️ +🤾 +🤾‍♀️ +🤾‍♂️ +🤾🏻 +🤾🏻‍♀️ +🤾🏻‍♂️ +🤾🏼 +🤾🏼‍♀️ +🤾🏼‍♂️ +🤾🏽 +🤾🏽‍♀️ +🤾🏽‍♂️ +🤾🏾 +🤾🏾‍♀️ +🤾🏾‍♂️ +🤾🏿 +🤾🏿‍♀️ +🤾🏿‍♂️ +🤿 +🥀 +🥁 +🥂 +🥃 +🥄 +🥅 +🥇 +🥈 +🥉 +🥊 +🥋 +🥌 +🥍 +🥎 +🥏 +🥐 +🥑 +🥒 +🥓 +🥔 +🥕 +🥖 +🥗 +🥘 +🥙 +🥚 +🥛 +🥜 +🥝 +🥞 +🥟 +🥠 +🥡 +🥢 +🥣 +🥤 +🥥 +🥦 +🥧 +🥨 +🥩 +🥪 +🥫 +🥬 +🥭 +🥮 +🥯 +🥰 +🥱 +🥲 +🥳 +🥴 +🥵 +🥶 +🥷 +🥷🏻 +🥷🏼 +🥷🏽 +🥷🏾 +🥷🏿 +🥸 +🥹 +🥺 +🥻 +🥼 +🥽 +🥾 +🥿 +🦀 +🦁 +🦂 +🦃 +🦄 +🦅 +🦆 +🦇 +🦈 +🦉 +🦊 +🦋 +🦌 +🦍 +🦎 +🦏 +🦐 +🦑 +🦒 +🦓 +🦔 +🦕 +🦖 +🦗 +🦘 +🦙 +🦚 +🦛 +🦜 +🦝 +🦞 +🦟 +🦠 +🦡 +🦢 +🦣 +🦤 +🦥 +🦦 +🦧 +🦨 +🦩 +🦪 +🦫 +🦬 +🦭 +🦮 +🦯 +🦴 +🦵 +🦵🏻 +🦵🏼 +🦵🏽 +🦵🏾 +🦵🏿 +🦶 +🦶🏻 +🦶🏼 +🦶🏽 +🦶🏾 +🦶🏿 +🦷 +🦸 +🦸‍♀️ +🦸‍♂️ +🦸🏻 +🦸🏻‍♀️ +🦸🏻‍♂️ +🦸🏼 +🦸🏼‍♀️ +🦸🏼‍♂️ +🦸🏽 +🦸🏽‍♀️ +🦸🏽‍♂️ +🦸🏾 +🦸🏾‍♀️ +🦸🏾‍♂️ +🦸🏿 +🦸🏿‍♀️ +🦸🏿‍♂️ +🦹 +🦹‍♀️ +🦹‍♂️ +🦹🏻 +🦹🏻‍♀️ +🦹🏻‍♂️ +🦹🏼 +🦹🏼‍♀️ +🦹🏼‍♂️ +🦹🏽 +🦹🏽‍♀️ +🦹🏽‍♂️ +🦹🏾 +🦹🏾‍♀️ +🦹🏾‍♂️ +🦹🏿 +🦹🏿‍♀️ +🦹🏿‍♂️ +🦺 +🦻 +🦻🏻 +🦻🏼 +🦻🏽 +🦻🏾 +🦻🏿 +🦼 +🦽 +🦾 +🦿 +🧀 +🧁 +🧂 +🧃 +🧄 +🧅 +🧆 +🧇 +🧈 +🧉 +🧊 +🧋 +🧌 +🧍 +🧍‍♀️ +🧍‍♂️ +🧍🏻 +🧍🏻‍♀️ +🧍🏻‍♂️ +🧍🏼 +🧍🏼‍♀️ +🧍🏼‍♂️ +🧍🏽 +🧍🏽‍♀️ +🧍🏽‍♂️ +🧍🏾 +🧍🏾‍♀️ +🧍🏾‍♂️ +🧍🏿 +🧍🏿‍♀️ +🧍🏿‍♂️ +🧎 +🧎‍♀️ +🧎‍♀️‍➡️ +🧎‍♂️ +🧎‍♂️‍➡️ +🧎‍➡️ +🧎🏻 +🧎🏻‍♀️ +🧎🏻‍♀️‍➡️ +🧎🏻‍♂️ +🧎🏻‍♂️‍➡️ +🧎🏻‍➡️ +🧎🏼 +🧎🏼‍♀️ +🧎🏼‍♀️‍➡️ +🧎🏼‍♂️ +🧎🏼‍♂️‍➡️ +🧎🏼‍➡️ +🧎🏽 +🧎🏽‍♀️ +🧎🏽‍♀️‍➡️ +🧎🏽‍♂️ +🧎🏽‍♂️‍➡️ +🧎🏽‍➡️ +🧎🏾 +🧎🏾‍♀️ +🧎🏾‍♀️‍➡️ +🧎🏾‍♂️ +🧎🏾‍♂️‍➡️ +🧎🏾‍➡️ +🧎🏿 +🧎🏿‍♀️ +🧎🏿‍♀️‍➡️ +🧎🏿‍♂️ +🧎🏿‍♂️‍➡️ +🧎🏿‍➡️ +🧏 +🧏‍♀️ +🧏‍♂️ +🧏🏻 +🧏🏻‍♀️ +🧏🏻‍♂️ +🧏🏼 +🧏🏼‍♀️ +🧏🏼‍♂️ +🧏🏽 +🧏🏽‍♀️ +🧏🏽‍♂️ +🧏🏾 +🧏🏾‍♀️ +🧏🏾‍♂️ +🧏🏿 +🧏🏿‍♀️ +🧏🏿‍♂️ +🧐 +🧑 +🧑‍⚕️ +🧑‍⚖️ +🧑‍✈️ +🧑‍🌾 +🧑‍🍳 +🧑‍🍼 +🧑‍🎄 +🧑‍🎓 +🧑‍🎤 +🧑‍🎨 +🧑‍🏫 +🧑‍🏭 +🧑‍💻 +🧑‍💼 +🧑‍🔧 +🧑‍🔬 +🧑‍🚀 +🧑‍🚒 +🧑‍🤝‍🧑 +🧑‍🦯 +🧑‍🦯‍➡️ +🧑‍🦰 +🧑‍🦱 +🧑‍🦲 +🧑‍🦳 +🧑‍🦼 +🧑‍🦼‍➡️ +🧑‍🦽 +🧑‍🦽‍➡️ +🧑‍🧑‍🧒 +🧑‍🧑‍🧒‍🧒 +🧑‍🧒 +🧑‍🧒‍🧒 +🧑‍🩰 +🧑🏻 +🧑🏻‍⚕️ +🧑🏻‍⚖️ +🧑🏻‍✈️ +🧑🏻‍❤️‍💋‍🧑🏼 +🧑🏻‍❤️‍💋‍🧑🏽 +🧑🏻‍❤️‍💋‍🧑🏾 +🧑🏻‍❤️‍💋‍🧑🏿 +🧑🏻‍❤️‍🧑🏼 +🧑🏻‍❤️‍🧑🏽 +🧑🏻‍❤️‍🧑🏾 +🧑🏻‍❤️‍🧑🏿 +🧑🏻‍🌾 +🧑🏻‍🍳 +🧑🏻‍🍼 +🧑🏻‍🎄 +🧑🏻‍🎓 +🧑🏻‍🎤 +🧑🏻‍🎨 +🧑🏻‍🏫 +🧑🏻‍🏭 +🧑🏻‍🐰‍🧑🏼 +🧑🏻‍🐰‍🧑🏽 +🧑🏻‍🐰‍🧑🏾 +🧑🏻‍🐰‍🧑🏿 +🧑🏻‍💻 +🧑🏻‍💼 +🧑🏻‍🔧 +🧑🏻‍🔬 +🧑🏻‍🚀 +🧑🏻‍🚒 +🧑🏻‍🤝‍🧑🏻 +🧑🏻‍🤝‍🧑🏼 +🧑🏻‍🤝‍🧑🏽 +🧑🏻‍🤝‍🧑🏾 +🧑🏻‍🤝‍🧑🏿 +🧑🏻‍🦯 +🧑🏻‍🦯‍➡️ +🧑🏻‍🦰 +🧑🏻‍🦱 +🧑🏻‍🦲 +🧑🏻‍🦳 +🧑🏻‍🦼 +🧑🏻‍🦼‍➡️ +🧑🏻‍🦽 +🧑🏻‍🦽‍➡️ +🧑🏻‍🩰 +🧑🏻‍🫯‍🧑🏼 +🧑🏻‍🫯‍🧑🏽 +🧑🏻‍🫯‍🧑🏾 +🧑🏻‍🫯‍🧑🏿 +🧑🏼 +🧑🏼‍⚕️ +🧑🏼‍⚖️ +🧑🏼‍✈️ +🧑🏼‍❤️‍💋‍🧑🏻 +🧑🏼‍❤️‍💋‍🧑🏽 +🧑🏼‍❤️‍💋‍🧑🏾 +🧑🏼‍❤️‍💋‍🧑🏿 +🧑🏼‍❤️‍🧑🏻 +🧑🏼‍❤️‍🧑🏽 +🧑🏼‍❤️‍🧑🏾 +🧑🏼‍❤️‍🧑🏿 +🧑🏼‍🌾 +🧑🏼‍🍳 +🧑🏼‍🍼 +🧑🏼‍🎄 +🧑🏼‍🎓 +🧑🏼‍🎤 +🧑🏼‍🎨 +🧑🏼‍🏫 +🧑🏼‍🏭 +🧑🏼‍🐰‍🧑🏻 +🧑🏼‍🐰‍🧑🏽 +🧑🏼‍🐰‍🧑🏾 +🧑🏼‍🐰‍🧑🏿 +🧑🏼‍💻 +🧑🏼‍💼 +🧑🏼‍🔧 +🧑🏼‍🔬 +🧑🏼‍🚀 +🧑🏼‍🚒 +🧑🏼‍🤝‍🧑🏻 +🧑🏼‍🤝‍🧑🏼 +🧑🏼‍🤝‍🧑🏽 +🧑🏼‍🤝‍🧑🏾 +🧑🏼‍🤝‍🧑🏿 +🧑🏼‍🦯 +🧑🏼‍🦯‍➡️ +🧑🏼‍🦰 +🧑🏼‍🦱 +🧑🏼‍🦲 +🧑🏼‍🦳 +🧑🏼‍🦼 +🧑🏼‍🦼‍➡️ +🧑🏼‍🦽 +🧑🏼‍🦽‍➡️ +🧑🏼‍🩰 +🧑🏼‍🫯‍🧑🏻 +🧑🏼‍🫯‍🧑🏽 +🧑🏼‍🫯‍🧑🏾 +🧑🏼‍🫯‍🧑🏿 +🧑🏽 +🧑🏽‍⚕️ +🧑🏽‍⚖️ +🧑🏽‍✈️ +🧑🏽‍❤️‍💋‍🧑🏻 +🧑🏽‍❤️‍💋‍🧑🏼 +🧑🏽‍❤️‍💋‍🧑🏾 +🧑🏽‍❤️‍💋‍🧑🏿 +🧑🏽‍❤️‍🧑🏻 +🧑🏽‍❤️‍🧑🏼 +🧑🏽‍❤️‍🧑🏾 +🧑🏽‍❤️‍🧑🏿 +🧑🏽‍🌾 +🧑🏽‍🍳 +🧑🏽‍🍼 +🧑🏽‍🎄 +🧑🏽‍🎓 +🧑🏽‍🎤 +🧑🏽‍🎨 +🧑🏽‍🏫 +🧑🏽‍🏭 +🧑🏽‍🐰‍🧑🏻 +🧑🏽‍🐰‍🧑🏼 +🧑🏽‍🐰‍🧑🏾 +🧑🏽‍🐰‍🧑🏿 +🧑🏽‍💻 +🧑🏽‍💼 +🧑🏽‍🔧 +🧑🏽‍🔬 +🧑🏽‍🚀 +🧑🏽‍🚒 +🧑🏽‍🤝‍🧑🏻 +🧑🏽‍🤝‍🧑🏼 +🧑🏽‍🤝‍🧑🏽 +🧑🏽‍🤝‍🧑🏾 +🧑🏽‍🤝‍🧑🏿 +🧑🏽‍🦯 +🧑🏽‍🦯‍➡️ +🧑🏽‍🦰 +🧑🏽‍🦱 +🧑🏽‍🦲 +🧑🏽‍🦳 +🧑🏽‍🦼 +🧑🏽‍🦼‍➡️ +🧑🏽‍🦽 +🧑🏽‍🦽‍➡️ +🧑🏽‍🩰 +🧑🏽‍🫯‍🧑🏻 +🧑🏽‍🫯‍🧑🏼 +🧑🏽‍🫯‍🧑🏾 +🧑🏽‍🫯‍🧑🏿 +🧑🏾 +🧑🏾‍⚕️ +🧑🏾‍⚖️ +🧑🏾‍✈️ +🧑🏾‍❤️‍💋‍🧑🏻 +🧑🏾‍❤️‍💋‍🧑🏼 +🧑🏾‍❤️‍💋‍🧑🏽 +🧑🏾‍❤️‍💋‍🧑🏿 +🧑🏾‍❤️‍🧑🏻 +🧑🏾‍❤️‍🧑🏼 +🧑🏾‍❤️‍🧑🏽 +🧑🏾‍❤️‍🧑🏿 +🧑🏾‍🌾 +🧑🏾‍🍳 +🧑🏾‍🍼 +🧑🏾‍🎄 +🧑🏾‍🎓 +🧑🏾‍🎤 +🧑🏾‍🎨 +🧑🏾‍🏫 +🧑🏾‍🏭 +🧑🏾‍🐰‍🧑🏻 +🧑🏾‍🐰‍🧑🏼 +🧑🏾‍🐰‍🧑🏽 +🧑🏾‍🐰‍🧑🏿 +🧑🏾‍💻 +🧑🏾‍💼 +🧑🏾‍🔧 +🧑🏾‍🔬 +🧑🏾‍🚀 +🧑🏾‍🚒 +🧑🏾‍🤝‍🧑🏻 +🧑🏾‍🤝‍🧑🏼 +🧑🏾‍🤝‍🧑🏽 +🧑🏾‍🤝‍🧑🏾 +🧑🏾‍🤝‍🧑🏿 +🧑🏾‍🦯 +🧑🏾‍🦯‍➡️ +🧑🏾‍🦰 +🧑🏾‍🦱 +🧑🏾‍🦲 +🧑🏾‍🦳 +🧑🏾‍🦼 +🧑🏾‍🦼‍➡️ +🧑🏾‍🦽 +🧑🏾‍🦽‍➡️ +🧑🏾‍🩰 +🧑🏾‍🫯‍🧑🏻 +🧑🏾‍🫯‍🧑🏼 +🧑🏾‍🫯‍🧑🏽 +🧑🏾‍🫯‍🧑🏿 +🧑🏿 +🧑🏿‍⚕️ +🧑🏿‍⚖️ +🧑🏿‍✈️ +🧑🏿‍❤️‍💋‍🧑🏻 +🧑🏿‍❤️‍💋‍🧑🏼 +🧑🏿‍❤️‍💋‍🧑🏽 +🧑🏿‍❤️‍💋‍🧑🏾 +🧑🏿‍❤️‍🧑🏻 +🧑🏿‍❤️‍🧑🏼 +🧑🏿‍❤️‍🧑🏽 +🧑🏿‍❤️‍🧑🏾 +🧑🏿‍🌾 +🧑🏿‍🍳 +🧑🏿‍🍼 +🧑🏿‍🎄 +🧑🏿‍🎓 +🧑🏿‍🎤 +🧑🏿‍🎨 +🧑🏿‍🏫 +🧑🏿‍🏭 +🧑🏿‍🐰‍🧑🏻 +🧑🏿‍🐰‍🧑🏼 +🧑🏿‍🐰‍🧑🏽 +🧑🏿‍🐰‍🧑🏾 +🧑🏿‍💻 +🧑🏿‍💼 +🧑🏿‍🔧 +🧑🏿‍🔬 +🧑🏿‍🚀 +🧑🏿‍🚒 +🧑🏿‍🤝‍🧑🏻 +🧑🏿‍🤝‍🧑🏼 +🧑🏿‍🤝‍🧑🏽 +🧑🏿‍🤝‍🧑🏾 +🧑🏿‍🤝‍🧑🏿 +🧑🏿‍🦯 +🧑🏿‍🦯‍➡️ +🧑🏿‍🦰 +🧑🏿‍🦱 +🧑🏿‍🦲 +🧑🏿‍🦳 +🧑🏿‍🦼 +🧑🏿‍🦼‍➡️ +🧑🏿‍🦽 +🧑🏿‍🦽‍➡️ +🧑🏿‍🩰 +🧑🏿‍🫯‍🧑🏻 +🧑🏿‍🫯‍🧑🏼 +🧑🏿‍🫯‍🧑🏽 +🧑🏿‍🫯‍🧑🏾 +🧒 +🧒🏻 +🧒🏼 +🧒🏽 +🧒🏾 +🧒🏿 +🧓 +🧓🏻 +🧓🏼 +🧓🏽 +🧓🏾 +🧓🏿 +🧔 +🧔‍♀️ +🧔‍♂️ +🧔🏻 +🧔🏻‍♀️ +🧔🏻‍♂️ +🧔🏼 +🧔🏼‍♀️ +🧔🏼‍♂️ +🧔🏽 +🧔🏽‍♀️ +🧔🏽‍♂️ +🧔🏾 +🧔🏾‍♀️ +🧔🏾‍♂️ +🧔🏿 +🧔🏿‍♀️ +🧔🏿‍♂️ +🧕 +🧕🏻 +🧕🏼 +🧕🏽 +🧕🏾 +🧕🏿 +🧖 +🧖‍♀️ +🧖‍♂️ +🧖🏻 +🧖🏻‍♀️ +🧖🏻‍♂️ +🧖🏼 +🧖🏼‍♀️ +🧖🏼‍♂️ +🧖🏽 +🧖🏽‍♀️ +🧖🏽‍♂️ +🧖🏾 +🧖🏾‍♀️ +🧖🏾‍♂️ +🧖🏿 +🧖🏿‍♀️ +🧖🏿‍♂️ +🧗 +🧗‍♀️ +🧗‍♂️ +🧗🏻 +🧗🏻‍♀️ +🧗🏻‍♂️ +🧗🏼 +🧗🏼‍♀️ +🧗🏼‍♂️ +🧗🏽 +🧗🏽‍♀️ +🧗🏽‍♂️ +🧗🏾 +🧗🏾‍♀️ +🧗🏾‍♂️ +🧗🏿 +🧗🏿‍♀️ +🧗🏿‍♂️ +🧘 +🧘‍♀️ +🧘‍♂️ +🧘🏻 +🧘🏻‍♀️ +🧘🏻‍♂️ +🧘🏼 +🧘🏼‍♀️ +🧘🏼‍♂️ +🧘🏽 +🧘🏽‍♀️ +🧘🏽‍♂️ +🧘🏾 +🧘🏾‍♀️ +🧘🏾‍♂️ +🧘🏿 +🧘🏿‍♀️ +🧘🏿‍♂️ +🧙 +🧙‍♀️ +🧙‍♂️ +🧙🏻 +🧙🏻‍♀️ +🧙🏻‍♂️ +🧙🏼 +🧙🏼‍♀️ +🧙🏼‍♂️ +🧙🏽 +🧙🏽‍♀️ +🧙🏽‍♂️ +🧙🏾 +🧙🏾‍♀️ +🧙🏾‍♂️ +🧙🏿 +🧙🏿‍♀️ +🧙🏿‍♂️ +🧚 +🧚‍♀️ +🧚‍♂️ +🧚🏻 +🧚🏻‍♀️ +🧚🏻‍♂️ +🧚🏼 +🧚🏼‍♀️ +🧚🏼‍♂️ +🧚🏽 +🧚🏽‍♀️ +🧚🏽‍♂️ +🧚🏾 +🧚🏾‍♀️ +🧚🏾‍♂️ +🧚🏿 +🧚🏿‍♀️ +🧚🏿‍♂️ +🧛 +🧛‍♀️ +🧛‍♂️ +🧛🏻 +🧛🏻‍♀️ +🧛🏻‍♂️ +🧛🏼 +🧛🏼‍♀️ +🧛🏼‍♂️ +🧛🏽 +🧛🏽‍♀️ +🧛🏽‍♂️ +🧛🏾 +🧛🏾‍♀️ +🧛🏾‍♂️ +🧛🏿 +🧛🏿‍♀️ +🧛🏿‍♂️ +🧜 +🧜‍♀️ +🧜‍♂️ +🧜🏻 +🧜🏻‍♀️ +🧜🏻‍♂️ +🧜🏼 +🧜🏼‍♀️ +🧜🏼‍♂️ +🧜🏽 +🧜🏽‍♀️ +🧜🏽‍♂️ +🧜🏾 +🧜🏾‍♀️ +🧜🏾‍♂️ +🧜🏿 +🧜🏿‍♀️ +🧜🏿‍♂️ +🧝 +🧝‍♀️ +🧝‍♂️ +🧝🏻 +🧝🏻‍♀️ +🧝🏻‍♂️ +🧝🏼 +🧝🏼‍♀️ +🧝🏼‍♂️ +🧝🏽 +🧝🏽‍♀️ +🧝🏽‍♂️ +🧝🏾 +🧝🏾‍♀️ +🧝🏾‍♂️ +🧝🏿 +🧝🏿‍♀️ +🧝🏿‍♂️ +🧞 +🧞‍♀️ +🧞‍♂️ +🧟 +🧟‍♀️ +🧟‍♂️ +🧠 +🧡 +🧢 +🧣 +🧤 +🧥 +🧦 +🧧 +🧨 +🧩 +🧪 +🧫 +🧬 +🧭 +🧮 +🧯 +🧰 +🧱 +🧲 +🧳 +🧴 +🧵 +🧶 +🧷 +🧸 +🧹 +🧺 +🧻 +🧼 +🧽 +🧾 +🧿 +🩰 +🩱 +🩲 +🩳 +🩴 +🩵 +🩶 +🩷 +🩸 +🩹 +🩺 +🩻 +🩼 +🪀 +🪁 +🪂 +🪃 +🪄 +🪅 +🪆 +🪇 +🪈 +🪉 +🪊 +🪎 +🪏 +🪐 +🪑 +🪒 +🪓 +🪔 +🪕 +🪖 +🪗 +🪘 +🪙 +🪚 +🪛 +🪜 +🪝 +🪞 +🪟 +🪠 +🪡 +🪢 +🪣 +🪤 +🪥 +🪦 +🪧 +🪨 +🪩 +🪪 +🪫 +🪬 +🪭 +🪮 +🪯 +🪰 +🪱 +🪲 +🪳 +🪴 +🪵 +🪶 +🪷 +🪸 +🪹 +🪺 +🪻 +🪼 +🪽 +🪾 +🪿 +🫀 +🫁 +🫂 +🫃 +🫃🏻 +🫃🏼 +🫃🏽 +🫃🏾 +🫃🏿 +🫄 +🫄🏻 +🫄🏼 +🫄🏽 +🫄🏾 +🫄🏿 +🫅 +🫅🏻 +🫅🏼 +🫅🏽 +🫅🏾 +🫅🏿 +🫆 +🫈 +🫍 +🫎 +🫏 +🫐 +🫑 +🫒 +🫓 +🫔 +🫕 +🫖 +🫗 +🫘 +🫙 +🫚 +🫛 +🫜 +🫟 +🫠 +🫡 +🫢 +🫣 +🫤 +🫥 +🫦 +🫧 +🫨 +🫩 +🫪 +🫯 +🫰 +🫰🏻 +🫰🏼 +🫰🏽 +🫰🏾 +🫰🏿 +🫱 +🫱🏻 +🫱🏻‍🫲🏼 +🫱🏻‍🫲🏽 +🫱🏻‍🫲🏾 +🫱🏻‍🫲🏿 +🫱🏼 +🫱🏼‍🫲🏻 +🫱🏼‍🫲🏽 +🫱🏼‍🫲🏾 +🫱🏼‍🫲🏿 +🫱🏽 +🫱🏽‍🫲🏻 +🫱🏽‍🫲🏼 +🫱🏽‍🫲🏾 +🫱🏽‍🫲🏿 +🫱🏾 +🫱🏾‍🫲🏻 +🫱🏾‍🫲🏼 +🫱🏾‍🫲🏽 +🫱🏾‍🫲🏿 +🫱🏿 +🫱🏿‍🫲🏻 +🫱🏿‍🫲🏼 +🫱🏿‍🫲🏽 +🫱🏿‍🫲🏾 +🫲 +🫲🏻 +🫲🏼 +🫲🏽 +🫲🏾 +🫲🏿 +🫳 +🫳🏻 +🫳🏼 +🫳🏽 +🫳🏾 +🫳🏿 +🫴 +🫴🏻 +🫴🏼 +🫴🏽 +🫴🏾 +🫴🏿 +🫵 +🫵🏻 +🫵🏼 +🫵🏽 +🫵🏾 +🫵🏿 +🫶 +🫶🏻 +🫶🏼 +🫶🏽 +🫶🏾 +🫶🏿 +🫷 +🫷🏻 +🫷🏼 +🫷🏽 +🫷🏾 +🫷🏿 +🫸 +🫸🏻 +🫸🏼 +🫸🏽 +🫸🏾 +🫸🏿 \ No newline at end of file diff --git a/crates/core/database/src/models/files/model.rs b/crates/core/database/src/models/files/model.rs index 5ea78f9b..1aaf8b82 100644 --- a/crates/core/database/src/models/files/model.rs +++ b/crates/core/database/src/models/files/model.rs @@ -70,6 +70,7 @@ auto_derived!( LegacyGroupIcon, ChannelIcon, ServerIcon, + RoleIcon, } /// Information about what the file was used for @@ -239,4 +240,23 @@ impl File { ) .await } + + /// Use a file for a role icon + pub async fn use_role_icon( + db: &Database, + id: &str, + parent: &str, + uploader_id: &str, + ) -> Result { + db.find_and_use_attachment( + id, + "icons", + FileUsedFor { + id: parent.to_owned(), + object_type: FileUsedForType::RoleIcon, + }, + uploader_id.to_owned(), + ) + .await + } } diff --git a/crates/core/database/src/models/servers/model.rs b/crates/core/database/src/models/servers/model.rs index 183dd3d2..50c45c59 100644 --- a/crates/core/database/src/models/servers/model.rs +++ b/crates/core/database/src/models/servers/model.rs @@ -86,6 +86,9 @@ auto_derived_partial!( /// Ranking of this role #[serde(default)] pub rank: i64, + /// Custom icon attachment + #[serde(skip_serializing_if = "Option::is_none")] + pub icon: Option, }, "PartialRole" ); @@ -129,6 +132,7 @@ auto_derived!( /// Optional fields on server object pub enum FieldsRole { Colour, + Icon, } ); @@ -305,6 +309,7 @@ impl Role { colour: self.colour, hoist: Some(self.hoist), rank: Some(self.rank), + icon: self.icon, } } @@ -318,6 +323,7 @@ impl Role { colour: None, hoist: false, permissions: Default::default(), + icon: None, }; db.insert_role(&server.id, &role).await?; @@ -367,6 +373,7 @@ impl Role { pub fn remove_field(&mut self, field: &FieldsRole) { match field { FieldsRole::Colour => self.colour = None, + FieldsRole::Icon => self.icon = None, } } diff --git a/crates/core/database/src/models/servers/ops/mongodb.rs b/crates/core/database/src/models/servers/ops/mongodb.rs index d8ef26f2..964de410 100644 --- a/crates/core/database/src/models/servers/ops/mongodb.rs +++ b/crates/core/database/src/models/servers/ops/mongodb.rs @@ -172,6 +172,7 @@ impl IntoDocumentPath for FieldsRole { fn as_path(&self) -> Option<&'static str> { Some(match self { FieldsRole::Colour => "colour", + FieldsRole::Icon => "icon", }) } } diff --git a/crates/core/database/src/tasks/ack.rs b/crates/core/database/src/tasks/ack.rs index c26de03b..24e3805e 100644 --- a/crates/core/database/src/tasks/ack.rs +++ b/crates/core/database/src/tasks/ack.rs @@ -105,7 +105,11 @@ pub async fn handle_ack_event( if mentions_acked > 0 { if let Err(err) = amqp - .ack_message(user.to_string(), channel.to_string(), id.to_owned()) + .ack_notification_message( + user.to_string(), + channel.to_string(), + id.to_owned(), + ) .await { revolt_config::capture_error(&err); @@ -192,9 +196,7 @@ pub async fn handle_ack_event( .expect("Failed to fetch channel from db"); if let TextChannel { server, .. } = channel { - if let Err(err) = - amqp.mass_mention_message_sent(server, mass_mentions).await - { + if let Err(err) = amqp.mass_mention_message_sent(server, mass_mentions).await { revolt_config::capture_error(&err); } } else { diff --git a/crates/core/database/src/util/acker.rs b/crates/core/database/src/util/acker.rs new file mode 100644 index 00000000..8cc104c5 --- /dev/null +++ b/crates/core/database/src/util/acker.rs @@ -0,0 +1,77 @@ +use redis_kiss::{get_connection, AsyncCommands}; +use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; +use revolt_result::{Result, ToRevoltError}; + +use crate::{events::client::EventV1, Channel, Database, Server, User, AMQP}; + +pub async fn ack_channel(user: &str, channel: &str, message: &str, amqp: &AMQP) -> Result<()> { + let mut redis = get_connection() + .await + .map_err(|_| create_error!(InternalError))?; + + let old: Option = redis + .getset(format!("acker:{user}+{channel}"), message) + .await + .to_internal_error()?; + + if old.is_none() || old.unwrap() == message { + amqp.process_ack(user, Some(channel), None) + .await + .to_internal_error()?; + } + + Ok(()) +} + +pub async fn ack_server(user: &User, server: &Server, db: &Database, amqp: &AMQP) -> Result<()> { + let mut redis = get_connection() + .await + .map_err(|_| create_error!(InternalError))?; + + let channels = db.fetch_channels(&server.channels).await?; + let query = crate::util::permissions::DatabasePermissionQuery::new(db, user).server(server); + + for channel in channels { + let channel_id = channel.id(); + let mut q = query.clone().channel(&channel); + + if calculate_channel_permissions(&mut q) + .await + .has_channel_permission(ChannelPermission::ViewChannel) + { + let channel_last_msg = match &channel { + Channel::TextChannel { + last_message_id, .. + } => last_message_id, + _ => unreachable!(), + } + .clone(); + + if let Some(channel_last_msg) = channel_last_msg { + let old: Option = redis + .getset( + format!("acker:{}+{}", user.id, channel_id), + &channel_last_msg, + ) + .await + .to_internal_error()?; + + if old.is_none() || old.unwrap() == channel_last_msg { + amqp.process_ack(&user.id, Some(channel_id), Some(&server.id)) + .await + .to_internal_error()?; + + EventV1::ChannelAck { + id: channel_id.to_string(), + user: user.id.clone(), + message_id: channel_last_msg, + } + .private(user.id.clone()) + .await; + } + } + } + } + + Ok(()) +} diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index c9df432f..774aca62 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -190,7 +190,7 @@ impl From for Channel { role_permissions, nsfw, voice, - slowmode + slowmode, } => Channel::TextChannel { id, server, @@ -202,7 +202,7 @@ impl From for Channel { role_permissions, nsfw, voice: voice.map(|voice| voice.into()), - slowmode + slowmode, }, } } @@ -256,7 +256,7 @@ impl From for crate::Channel { role_permissions, nsfw, voice, - slowmode + slowmode, } => crate::Channel::TextChannel { id, server, @@ -268,7 +268,7 @@ impl From for crate::Channel { role_permissions, nsfw, voice: voice.map(|voice| voice.into()), - slowmode + slowmode, }, } } @@ -307,7 +307,7 @@ impl From for crate::PartialChannel { default_permissions: value.default_permissions, last_message_id: value.last_message_id, voice: value.voice.map(|voice| voice.into()), - slowmode: value.slowmode + slowmode: value.slowmode, } } } @@ -927,6 +927,7 @@ impl From for Role { colour: value.colour, hoist: value.hoist, rank: value.rank, + icon: value.icon.map(|f| f.into()), } } } @@ -940,6 +941,7 @@ impl From for crate::Role { colour: value.colour, hoist: value.hoist, rank: value.rank, + icon: value.icon.map(|f| f.into()), } } } @@ -953,6 +955,7 @@ impl From for PartialRole { colour: value.colour, hoist: value.hoist, rank: value.rank, + icon: value.icon.map(|f| f.into()), } } } @@ -966,6 +969,7 @@ impl From for crate::PartialRole { colour: value.colour, hoist: value.hoist, rank: value.rank, + icon: value.icon.map(|f| f.into()), } } } @@ -974,6 +978,7 @@ impl From for FieldsRole { fn from(value: crate::FieldsRole) -> Self { match value { crate::FieldsRole::Colour => FieldsRole::Colour, + crate::FieldsRole::Icon => FieldsRole::Icon, } } } @@ -982,6 +987,7 @@ impl From for crate::FieldsRole { fn from(value: FieldsRole) -> Self { match value { FieldsRole::Colour => crate::FieldsRole::Colour, + FieldsRole::Icon => crate::FieldsRole::Icon, } } } diff --git a/crates/core/database/src/util/mod.rs b/crates/core/database/src/util/mod.rs index 10c02a5c..1cb0b31a 100644 --- a/crates/core/database/src/util/mod.rs +++ b/crates/core/database/src/util/mod.rs @@ -1,3 +1,4 @@ +pub mod acker; pub mod basic; pub mod bridge; pub mod bulk_permissions; diff --git a/crates/core/files/Cargo.toml b/crates/core/files/Cargo.toml index 9f30649d..c60bf678 100644 --- a/crates/core/files/Cargo.toml +++ b/crates/core/files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-files" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/models/Cargo.toml b/crates/core/models/Cargo.toml index f8a4c697..c92f3f8a 100644 --- a/crates/core/models/Cargo.toml +++ b/crates/core/models/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-models" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/models/src/v0/channels.rs b/crates/core/models/src/v0/channels.rs index d9fe8e14..faddee74 100644 --- a/crates/core/models/src/v0/channels.rs +++ b/crates/core/models/src/v0/channels.rs @@ -314,6 +314,12 @@ auto_derived!( /// Only used when the user is the first one connected. pub recipients: Option>, } + + pub struct ChannelSlowmode { + pub channel_id: String, + pub duration: u64, + pub retry_after: u64, + } ); impl Channel { diff --git a/crates/core/models/src/v0/emojis.rs b/crates/core/models/src/v0/emojis.rs index 2d7c015e..58d47429 100644 --- a/crates/core/models/src/v0/emojis.rs +++ b/crates/core/models/src/v0/emojis.rs @@ -54,4 +54,22 @@ auto_derived!( #[serde(default)] pub nsfw: bool, } + + /// Partial emoji representation + #[derive(Default)] + pub struct PartialEmoji { + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub name: Option, + } + + /// Edit emoji information + #[cfg_attr(feature = "validator", derive(Validate))] + pub struct DataEditEmoji { + /// Emoji name + #[cfg_attr( + feature = "validator", + validate(length(min = 1, max = 32), regex = "RE_EMOJI") + )] + pub name: Option, + } ); diff --git a/crates/core/models/src/v0/servers.rs b/crates/core/models/src/v0/servers.rs index d9835443..2aab765e 100644 --- a/crates/core/models/src/v0/servers.rs +++ b/crates/core/models/src/v0/servers.rs @@ -106,6 +106,9 @@ auto_derived_partial!( /// Ranking of this role #[cfg_attr(feature = "serde", serde(default))] pub rank: i64, + /// Role icon + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub icon: Option, }, "PartialRole" ); @@ -123,6 +126,7 @@ auto_derived!( /// Optional fields on server object pub enum FieldsRole { Colour, + Icon, } /// Channel category @@ -278,6 +282,11 @@ auto_derived!( /// /// **Removed** - no effect, use the edit server role positions route pub rank: Option, + /// Role icon + /// + /// Provide an Autumn attachment Id. + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))] + pub icon: Option, /// Fields to remove from role object #[cfg_attr(feature = "serde", serde(default))] pub remove: Vec, diff --git a/crates/core/parser/Cargo.toml b/crates/core/parser/Cargo.toml index 1bc35301..5bb079ac 100644 --- a/crates/core/parser/Cargo.toml +++ b/crates/core/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-parser" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/permissions/Cargo.toml b/crates/core/permissions/Cargo.toml index 838b10b2..03ed629d 100644 --- a/crates/core/permissions/Cargo.toml +++ b/crates/core/permissions/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-permissions" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/presence/Cargo.toml b/crates/core/presence/Cargo.toml index 547dcd79..7868342d 100644 --- a/crates/core/presence/Cargo.toml +++ b/crates/core/presence/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-presence" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] diff --git a/crates/core/ratelimits/Cargo.toml b/crates/core/ratelimits/Cargo.toml index 0e862c80..e606d151 100644 --- a/crates/core/ratelimits/Cargo.toml +++ b/crates/core/ratelimits/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-ratelimits" -version = "0.12.1" +version = "0.13.7" edition = "2024" license = "MIT" authors = ["Zomatree ", "Paul Makles "] diff --git a/crates/core/ratelimits/src/rocket.rs b/crates/core/ratelimits/src/rocket.rs index 046317bd..f790abb9 100644 --- a/crates/core/ratelimits/src/rocket.rs +++ b/crates/core/ratelimits/src/rocket.rs @@ -1,12 +1,12 @@ use async_trait::async_trait; use log::info; +use revolt_config::config; use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::uri::Origin; use rocket::http::{Method, Status}; use rocket::request::{FromRequest, Outcome}; use rocket::serde::json::Json; use rocket::{Data, Request, Response, State}; -use revolt_config::config; use revolt_rocket_okapi::r#gen::OpenApiGenerator; use revolt_rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; @@ -28,8 +28,8 @@ pub type RatelimitStorage = crate::ratelimiter::RatelimitStorage) -> String { request - .remote() - .map(|x| x.ip().to_string()) + .client_ip() + .map(|r| r.to_string()) .unwrap_or_default() } diff --git a/crates/core/result/Cargo.toml b/crates/core/result/Cargo.toml index 9a1d1c13..9cc5db3f 100644 --- a/crates/core/result/Cargo.toml +++ b/crates/core/result/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-result" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "MIT" authors = ["Paul Makles "] diff --git a/crates/core/result/src/axum.rs b/crates/core/result/src/axum.rs index bb09621e..b66b619e 100644 --- a/crates/core/result/src/axum.rs +++ b/crates/core/result/src/axum.rs @@ -24,6 +24,7 @@ impl IntoResponse for Error { ErrorType::UnknownChannel => StatusCode::NOT_FOUND, ErrorType::UnknownMessage => StatusCode::NOT_FOUND, ErrorType::UnknownAttachment => StatusCode::BAD_REQUEST, + ErrorType::CannotDeleteMessage => StatusCode::FORBIDDEN, ErrorType::CannotEditMessage => StatusCode::FORBIDDEN, ErrorType::CannotJoinCall => StatusCode::BAD_REQUEST, ErrorType::TooManyAttachments { .. } => StatusCode::BAD_REQUEST, @@ -78,7 +79,7 @@ impl IntoResponse for Error { ErrorType::DuplicateNonce => StatusCode::CONFLICT, ErrorType::VosoUnavailable => StatusCode::BAD_REQUEST, ErrorType::NotFound => StatusCode::NOT_FOUND, - ErrorType::NoEffect => StatusCode::OK, + ErrorType::NoEffect => StatusCode::BAD_REQUEST, ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST, ErrorType::LiveKitUnavailable => StatusCode::BAD_REQUEST, ErrorType::NotConnected => StatusCode::BAD_REQUEST, diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index 6e4ecea2..8b7de16f 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -78,6 +78,7 @@ pub enum ErrorType { UnknownChannel, UnknownAttachment, UnknownMessage, + CannotDeleteMessage, CannotEditMessage, CannotJoinCall, TooManyAttachments { diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index d965d768..84ddbf12 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -30,6 +30,7 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::UnknownChannel => Status::NotFound, ErrorType::UnknownMessage => Status::NotFound, ErrorType::UnknownAttachment => Status::BadRequest, + ErrorType::CannotDeleteMessage => Status::Forbidden, ErrorType::CannotEditMessage => Status::Forbidden, ErrorType::CannotJoinCall => Status::BadRequest, ErrorType::TooManyAttachments { .. } => Status::BadRequest, @@ -84,7 +85,7 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::NotAuthenticated => Status::Unauthorized, ErrorType::DuplicateNonce => Status::Conflict, ErrorType::NotFound => Status::NotFound, - ErrorType::NoEffect => Status::Ok, + ErrorType::NoEffect => Status::BadRequest, ErrorType::FailedValidation { .. } => Status::BadRequest, ErrorType::LiveKitUnavailable => Status::BadRequest, ErrorType::NotAVoiceChannel => Status::BadRequest, diff --git a/crates/daemons/crond/Cargo.toml b/crates/daemons/crond/Cargo.toml index fe33d2b7..bb2f6ced 100644 --- a/crates/daemons/crond/Cargo.toml +++ b/crates/daemons/crond/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-crond" -version = "0.12.1" +version = "0.13.7" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2021" @@ -16,8 +16,22 @@ log = { workspace = true } # Async tokio = { workspace = true } +# Redis +redis-kiss = { workspace = true } + +# RabbitMQ +lapin = { workspace = true } +futures-lite = { workspace = true } + +# Processing +serde_json = { workspace = true } +revolt_optional_struct = { workspace = true } +serde = { workspace = true } +iso8601-timestamp = { workspace = true, features = ["serde", "bson"] } + # Core revolt-database = { workspace = true } revolt-result = { workspace = true } revolt-config = { workspace = true } revolt-files = { workspace = true } +revolt-permissions = { workspace = true } diff --git a/crates/daemons/crond/src/main.rs b/crates/daemons/crond/src/main.rs index b232a10a..2b9a82f6 100644 --- a/crates/daemons/crond/src/main.rs +++ b/crates/daemons/crond/src/main.rs @@ -1,7 +1,7 @@ use revolt_config::configure; -use revolt_database::DatabaseInfo; +use revolt_database::{DatabaseInfo, AMQP}; use revolt_result::Result; -use tasks::{file_deletion, prune_dangling_files, prune_members}; +use tasks::{acks, file_deletion, prune_dangling_files, prune_members}; use tokio::try_join; pub mod tasks; @@ -11,10 +11,13 @@ async fn main() -> Result<()> { configure!(crond); let db = DatabaseInfo::Auto.connect().await.expect("database"); + let amqp = AMQP::new_auto().await; + try_join!( file_deletion::task(db.clone()), prune_dangling_files::task(db.clone()), - prune_members::task(db.clone()) + prune_members::task(db.clone()), + acks::task(db.clone(), amqp.clone()), ) .map(|_| ()) } diff --git a/crates/daemons/crond/src/tasks/acks.rs b/crates/daemons/crond/src/tasks/acks.rs new file mode 100644 index 00000000..7576bac9 --- /dev/null +++ b/crates/daemons/crond/src/tasks/acks.rs @@ -0,0 +1,164 @@ +use futures_lite::stream::StreamExt; +use lapin::{ + options::*, + types::FieldTable, + uri::{AMQPAuthority, AMQPQueryString, AMQPUri, AMQPUserInfo}, + ConnectionBuilder, ConnectionProperties, ExchangeKind, +}; +use log::{debug, info}; +use redis_kiss::{get_connection, AsyncCommands, Conn as RedisConnection}; +use revolt_config::config; +use revolt_database::{events::rabbit::AckEventPayload, Database, AMQP}; +use revolt_result::{Result, ToRevoltError}; +use serde_json; + +pub async fn task(db: Database, amqp: AMQP) -> Result<()> { + let config = config().await; + + let mut redis = get_connection() + .await + .expect("Failed to get redis connection"); + + let uri = AMQPUri { + scheme: lapin::uri::AMQPScheme::AMQP, + authority: AMQPAuthority { + userinfo: AMQPUserInfo { + username: config.rabbit.username, + password: config.rabbit.password, + }, + host: config.rabbit.host, + port: config.rabbit.port, + }, + vhost: "/".to_string(), + query: AMQPQueryString::default(), + }; + + let connection = ConnectionBuilder::new() + .expect("Builder") + .with_uri(uri) + .with_properties(ConnectionProperties::default()) + .connect() + .await + .expect("Failed to connect to rabbitmq"); + + let reader_channel = connection + .create_channel() + .await + .expect("Failed to create channel"); + + reader_channel + .exchange_declare( + config.rabbit.default_exchange.clone().into(), + ExchangeKind::Topic, + ExchangeDeclareOptions { + durable: true, + ..Default::default() + }, + FieldTable::default(), + ) + .await + .expect("Failed to declare exchange"); + + reader_channel + .queue_declare( + config.rabbit.queues.acks.clone().into(), + QueueDeclareOptions { + durable: true, + ..Default::default() + }, + FieldTable::default(), + ) + .await + .expect("Failed to bind queue"); + + reader_channel + .queue_bind( + config.rabbit.queues.acks.clone().into(), + config.rabbit.default_exchange.into(), + config.rabbit.queues.acks.clone().into(), + QueueBindOptions::default(), + FieldTable::default(), + ) + .await + .expect("Failed to bind channel"); + + let mut consumer = reader_channel + .basic_consume( + config.rabbit.queues.acks.into(), + "crond-ack-consumer".into(), + BasicConsumeOptions::default(), + FieldTable::default(), + ) + .await + .expect("Failed to create consumer"); + + while let Some(delivery) = consumer.next().await { + if let Ok(delivery) = delivery { + let payload = serde_json::from_slice::(&delivery.data); + + if let Ok(payload) = payload { + debug!("Received ack event: {payload:?}"); + + if let Err(e) = process_channel_ack( + &db, + &amqp, + payload.user_id, + payload.channel_id.unwrap(), + &mut redis, + ) + .await + { + revolt_config::capture_error(&e); + _ = delivery.reject(BasicRejectOptions { requeue: false }).await; + } else { + _ = delivery.ack(BasicAckOptions { multiple: false }).await; + } + } else { + revolt_config::capture_message( + format!("Failed to decode ack data: {:?}", delivery.data).as_str(), + revolt_config::Level::Error, + ); + } + } + } + Ok(()) +} + +#[allow(clippy::disallowed_methods)] +async fn process_channel_ack( + db: &Database, + amqp: &AMQP, + user: String, + channel: String, + redis: &mut RedisConnection, +) -> Result<()> { + let message_id: Option = redis + .get_del(format!("acker:{user}+{channel}")) + .await + .to_internal_error()?; + + if let Some(message_id) = message_id { + let unread = db.fetch_unread(&user, &channel).await?; + let updated = db.acknowledge_message(&channel, &user, &message_id).await?; + + info!("Set new state for ack: {}:{}:{}", channel, user, message_id); + + if let (Some(before), Some(after)) = (unread, updated) { + let before_mentions = before.mentions.unwrap_or_default().len(); + let after_mentions = after.mentions.unwrap_or_default().len(); + + if after_mentions < before_mentions { + if let Err(err) = amqp + .ack_notification_message(user.to_string(), channel.to_string(), message_id) + .await + { + revolt_config::capture_error(&err); + } + }; + } + + Ok(()) + } else { + Err(message_id.to_internal_error().expect_err("no err")) + } +} diff --git a/crates/daemons/crond/src/tasks/file_deletion.rs b/crates/daemons/crond/src/tasks/file_deletion.rs index 912173b3..42942bf3 100644 --- a/crates/daemons/crond/src/tasks/file_deletion.rs +++ b/crates/daemons/crond/src/tasks/file_deletion.rs @@ -11,22 +11,24 @@ pub async fn task(db: Database) -> Result<()> { let files = db.fetch_deleted_attachments().await?; for file in files { - let count = db - .count_file_hash_references(file.hash.as_ref().expect("no `hash` present")) - .await?; - - // No other files reference this file on disk anymore - if count <= 1 { - let file_hash = db - .fetch_attachment_hash(file.hash.as_ref().expect("no `hash` present")) + if let Some(hash) = &file.hash { + let count = db + .count_file_hash_references(hash) .await?; - // Delete from S3 - delete_from_s3(&file_hash.bucket_id, &file_hash.path).await?; + // No other files reference this file on disk anymore + if count <= 1 { + let file_hash = db + .fetch_attachment_hash(hash) + .await?; - // Delete the hash - db.delete_attachment_hash(&file_hash.id).await?; - info!("Deleted file hash {}", file_hash.id); + // Delete from S3 + delete_from_s3(&file_hash.bucket_id, &file_hash.path).await?; + + // Delete the hash + db.delete_attachment_hash(&file_hash.id).await?; + info!("Deleted file hash {}", file_hash.id); + } } // Delete the file diff --git a/crates/daemons/crond/src/tasks/mod.rs b/crates/daemons/crond/src/tasks/mod.rs index 060f55d8..a7dd9040 100644 --- a/crates/daemons/crond/src/tasks/mod.rs +++ b/crates/daemons/crond/src/tasks/mod.rs @@ -1,3 +1,4 @@ +pub mod acks; pub mod file_deletion; pub mod prune_dangling_files; pub mod prune_members; diff --git a/crates/daemons/pushd/Cargo.toml b/crates/daemons/pushd/Cargo.toml index b26c80e1..9f38020e 100644 --- a/crates/daemons/pushd/Cargo.toml +++ b/crates/daemons/pushd/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-pushd" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" publish = false @@ -15,7 +15,7 @@ revolt-parser = { workspace = true } anyhow = { workspace = true } -amqprs = { workspace = true } +lapin = { workspace = true } fcm_v1 = { workspace = true } web-push = { workspace = true } isahc = { workspace = true, features = ["json"], optional = true } diff --git a/crates/daemons/pushd/src/consumers/inbound/ack.rs b/crates/daemons/pushd/src/consumers/inbound/ack.rs index 77dd5d44..77df8b27 100644 --- a/crates/daemons/pushd/src/consumers/inbound/ack.rs +++ b/crates/daemons/pushd/src/consumers/inbound/ack.rs @@ -1,96 +1,69 @@ -use crate::consumers::inbound::internal::*; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use std::sync::Arc; + +use crate::utils::Consumer; +use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use revolt_database::{events::rabbit::*, Database}; +#[derive(Clone)] +#[allow(unused)] pub struct AckConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for AckConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl AckConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> AckConsumer { - AckConsumer { +#[async_trait] +impl Consumer for AckConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } -} -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for AckConsumer { + fn channel(&self) -> &Arc { + &self.channel + } + /// This consumer processes all acks the platform receives, and sends relevant badge updates to apple platforms. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - let content = String::from_utf8(content).unwrap(); - let payload: AckPayload = serde_json::from_str(content.as_str()).unwrap(); + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: AckPayload = serde_json::from_slice(&delivery.data)?; // Step 1: fetch unreads and don't continue if there's no unreads - #[allow(clippy::disallowed_methods)] - let unreads = self.db.fetch_unread_mentions(&payload.user_id).await; + // #[allow(clippy::disallowed_methods)] debug!("Processing unreads for {:}", &payload.user_id); - if let Ok(u) = &unreads { + let unreads = if let Ok(u) = self.db.fetch_unread_mentions(&payload.user_id).await { if u.is_empty() { debug!( "Discarding unread task (no mentions found) for {:}", &payload.user_id ); - return; - } + return Ok(()); + }; + + u } else { - return; - } + return Ok(()); + }; if let Ok(sessions) = self.authifier_db.find_sessions(&payload.user_id).await { let config = revolt_config::config().await; // Step 2: find any apple sessions, since we don't need to calculate this for anything else. // If there's no apple sessions, we can return early - let apple_sessions: Vec<&authifier::models::Session> = sessions - .iter() + let mut apple_sessions = sessions + .into_iter() .filter(|session| { if let Some(sub) = &session.subscription { sub.endpoint == "apn" @@ -98,19 +71,19 @@ impl AsyncConsumer for AckConsumer { false } }) - .collect(); + .peekable(); - if apple_sessions.is_empty() { + if apple_sessions.peek().is_none() { debug!( "Discarding unread task (no apn sessions found) for {:}", &payload.user_id ); - return; + return Ok(()); } // Step 3: calculate the actual mention count, since we have to send it out let mut mention_count = 0; - for u in &unreads.unwrap() { + for u in &unreads { mention_count += u.mentions.as_ref().unwrap().len() } @@ -123,26 +96,22 @@ impl AsyncConsumer for AckConsumer { token: session.subscription.as_ref().unwrap().auth.clone(), extras: Default::default(), }; - let raw_service_payload = serde_json::to_string(&service_payload); + let payload = serde_json::to_string(&service_payload)?; - if let Ok(p) = raw_service_payload { - let args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); + log::debug!( + "Publishing ack to apn session {}", + session.subscription.as_ref().unwrap().auth + ); - log::debug!( - "Publishing ack to apn session {}", - session.subscription.as_ref().unwrap().auth - ); - - publish_message(self, p.into(), args).await; - } else { - log::warn!("Failed to serialize ack badge update payload!"); - revolt_config::capture_error(&raw_service_payload.unwrap_err()); - } + self.publish_message( + payload.as_bytes(), + &config.pushd.exchange, + &config.pushd.apn.queue, + ) + .await?; } } + + Ok(()) } } diff --git a/crates/daemons/pushd/src/consumers/inbound/dm_call.rs b/crates/daemons/pushd/src/consumers/inbound/dm_call.rs index 873b55cc..3175d8d8 100644 --- a/crates/daemons/pushd/src/consumers/inbound/dm_call.rs +++ b/crates/daemons/pushd/src/consumers/inbound/dm_call.rs @@ -1,70 +1,44 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use crate::consumers::inbound::internal::*; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::Consumer; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use log::debug; use revolt_database::{events::rabbit::*, Database}; +#[derive(Clone)] +#[allow(unused)] pub struct DmCallConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for DmCallConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl DmCallConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> DmCallConsumer { - DmCallConsumer { +#[async_trait] +impl Consumer for DmCallConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let _p: InternalDmCallPayload = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + /// This consumer handles delegating messages into their respective platform queues. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let _p: InternalDmCallPayload = serde_json::from_slice(&delivery.data)?; let payload = _p.payload; debug!("Received dm call start/stop event"); @@ -107,36 +81,27 @@ impl DmCallConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - publish_message(self, payload.into(), args).await; + self.publish_message( + payload.as_bytes(), + &config.pushd.exchange, + routing_key, + ) + .await?; } } } @@ -145,24 +110,3 @@ impl DmCallConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for DmCallConsumer { - /// This consumer handles delegating messages into their respective platform queues. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - warn!("Failed to process dm call start/stop event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs b/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs index 115e4a26..bcfa1c2f 100644 --- a/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs +++ b/crates/daemons/pushd/src/consumers/inbound/fr_accepted.rs @@ -1,70 +1,44 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use crate::consumers::inbound::internal::*; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::Consumer; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use log::debug; use revolt_database::{events::rabbit::*, Database}; +#[derive(Clone)] +#[allow(unused)] pub struct FRAcceptedConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for FRAcceptedConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl FRAcceptedConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> FRAcceptedConsumer { - FRAcceptedConsumer { +#[async_trait] +impl Consumer for FRAcceptedConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: FRAcceptedPayload = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + /// This consumer handles delegating messages into their respective platform queues. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: FRAcceptedPayload = serde_json::from_slice(&delivery.data)?; debug!("Received FR accept event"); @@ -80,36 +54,23 @@ impl FRAcceptedConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - publish_message(self, payload.into(), args).await; + self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key) + .await?; } } } @@ -117,24 +78,3 @@ impl FRAcceptedConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for FRAcceptedConsumer { - /// This consumer handles delegating messages into their respective platform queues. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process friend request accepted event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/fr_received.rs b/crates/daemons/pushd/src/consumers/inbound/fr_received.rs index c611dfaf..66fce72d 100644 --- a/crates/daemons/pushd/src/consumers/inbound/fr_received.rs +++ b/crates/daemons/pushd/src/consumers/inbound/fr_received.rs @@ -1,70 +1,44 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use crate::consumers::inbound::internal::*; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::Consumer; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use log::debug; use revolt_database::{events::rabbit::*, Database}; +#[derive(Clone)] +#[allow(unused)] pub struct FRReceivedConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for FRReceivedConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl FRReceivedConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> FRReceivedConsumer { - FRReceivedConsumer { +#[async_trait] +impl Consumer for FRReceivedConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: FRReceivedPayload = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + /// This consumer handles delegating messages into their respective platform queues. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: FRReceivedPayload = serde_json::from_slice(&delivery.data)?; debug!("Received FR received event"); @@ -80,36 +54,23 @@ impl FRReceivedConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - publish_message(self, payload.into(), args).await; + self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key) + .await?; } } } @@ -117,24 +78,3 @@ impl FRReceivedConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for FRReceivedConsumer { - /// This consumer handles delegating messages into their respective platform queues. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process friend request received event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/generic.rs b/crates/daemons/pushd/src/consumers/inbound/generic.rs index 302b1700..f413661d 100644 --- a/crates/daemons/pushd/src/consumers/inbound/generic.rs +++ b/crates/daemons/pushd/src/consumers/inbound/generic.rs @@ -1,70 +1,44 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use crate::consumers::inbound::internal::*; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::Consumer; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use log::debug; use revolt_database::{events::rabbit::*, Database}; +#[derive(Clone)] +#[allow(unused)] pub struct GenericConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for GenericConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl GenericConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> GenericConsumer { - GenericConsumer { +#[async_trait] +impl Consumer for GenericConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: MessageSentPayload = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + /// This consumer handles delegating messages into their respective platform queues. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: MessageSentPayload = serde_json::from_slice(&delivery.data)?; debug!("Received message event on origin"); @@ -86,36 +60,23 @@ impl GenericConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - publish_message(self, payload.into(), args).await; + self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key) + .await?; } } } @@ -123,24 +84,3 @@ impl GenericConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for GenericConsumer { - /// This consumer handles delegating messages into their respective platform queues. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process generic event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/internal.rs b/crates/daemons/pushd/src/consumers/inbound/internal.rs deleted file mode 100644 index 387c08b5..00000000 --- a/crates/daemons/pushd/src/consumers/inbound/internal.rs +++ /dev/null @@ -1,53 +0,0 @@ -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::{Connection, OpenConnectionArguments}, - BasicProperties, -}; -use log::{debug, warn}; - -pub(crate) trait Channeled { - #[allow(unused)] - fn get_connection(&self) -> Option<&Connection>; - fn get_channel(&self) -> Option<&Channel>; - fn set_connection(&mut self, conn: Connection); - fn set_channel(&mut self, channel: Channel); -} - -pub(crate) async fn make_channel(consumer: &mut T) { - let config = revolt_config::config().await; - - let args = OpenConnectionArguments::new( - &config.rabbit.host, - config.rabbit.port, - &config.rabbit.username, - &config.rabbit.password, - ); - let conn = amqprs::connection::Connection::open(&args).await.unwrap(); - - let channel = conn.open_channel(None).await.unwrap(); - - consumer.set_connection(conn); - consumer.set_channel(channel); -} - -pub(crate) async fn publish_message( - consumer: &mut T, - payload: Vec, - args: BasicPublishArguments, -) { - let routing_key = &args.routing_key.clone(); - let mut channel = consumer.get_channel(); - if channel.is_none() { - make_channel(consumer).await; - channel = consumer.get_channel(); - } - - if let Some(chnl) = channel { - chnl.basic_publish(BasicProperties::default(), payload.clone(), args.clone()) - .await - .unwrap(); - debug!("Sent message to queue for target {}", routing_key); - } else { - warn!("Failed to unwrap channel (including attempt to make a channel)!") - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs b/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs index 8d43cce8..55ab984f 100644 --- a/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs +++ b/crates/daemons/pushd/src/consumers/inbound/mass_mention.rs @@ -1,17 +1,13 @@ use std::{ collections::{HashMap, HashSet}, hash::RandomState, + sync::Arc, }; -use crate::{consumers::inbound::internal::*, utils}; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::{render_notification_content, Consumer}; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use revolt_database::{ events::rabbit::*, util::bulk_permissions::BulkDatabasePermissionQuery, Database, Member, MessageFlagsValue, @@ -19,52 +15,18 @@ use revolt_database::{ use revolt_models::v0::{MessageFlags, PushNotification}; use revolt_result::ToRevoltError; +#[derive(Clone)] +#[allow(unused)] pub struct MassMessageConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, -} - -impl Channeled for MassMessageConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } + connection: Arc, + channel: Arc, } impl MassMessageConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> MassMessageConsumer { - MassMessageConsumer { - db, - authifier_db, - conn: None, - channel: None, - } - } - async fn fire_notification_for_users( - &mut self, + &self, push: &PushNotification, users: &[String], ) -> Result<()> { @@ -84,56 +46,58 @@ impl MassMessageConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - publish_message(self, payload.into(), args).await; + self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key) + .await?; } } } Ok(()) } +} - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { +#[async_trait] +impl Consumer for MassMessageConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { + db, + authifier_db, + connection, + channel, + } + } + + fn channel(&self) -> &Arc { + &self.channel + } + + /// This consumer handles adding mentions for all the users affected by a mass mention ping, and then sends out push notifications. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let mut payload: MassMessageSentPayload = serde_json::from_slice(&delivery.data)?; let config = revolt_config::config().await; - let content = String::from_utf8(content)?; - let mut payload: MassMessageSentPayload = serde_json::from_str(content.as_str())?; for push in payload.notifications.iter_mut() { - if let Ok(body) = utils::render_notification_content(push, &self.db) + if let Ok(body) = render_notification_content(push, &self.db) .await .to_internal_error() { @@ -280,24 +244,3 @@ impl MassMessageConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for MassMessageConsumer { - /// This consumer handles adding mentions for all the users affected by a mass mention ping, and then sends out push notifications - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process mass message event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/message.rs b/crates/daemons/pushd/src/consumers/inbound/message.rs index 45de0b0b..e4aa04d5 100644 --- a/crates/daemons/pushd/src/consumers/inbound/message.rs +++ b/crates/daemons/pushd/src/consumers/inbound/message.rs @@ -1,76 +1,46 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use crate::{consumers::inbound::internal::*, utils}; -use amqprs::{ - channel::{BasicPublishArguments, Channel}, - connection::Connection, - consumer::AsyncConsumer, - BasicProperties, Deliver, -}; +use crate::utils::{render_notification_content, Consumer}; use anyhow::Result; use async_trait::async_trait; +use lapin::{message::Delivery, Channel, Connection}; use log::debug; use revolt_database::{events::rabbit::*, Database}; -use revolt_result::ToRevoltError; +#[derive(Clone)] +#[allow(unused)] pub struct MessageConsumer { - #[allow(dead_code)] db: Database, authifier_db: authifier::Database, - conn: Option, - channel: Option, + connection: Arc, + channel: Arc, } -impl Channeled for MessageConsumer { - fn get_connection(&self) -> Option<&Connection> { - if self.conn.is_none() { - None - } else { - Some(self.conn.as_ref().unwrap()) - } - } - - fn get_channel(&self) -> Option<&Channel> { - if self.channel.is_none() { - None - } else { - Some(self.channel.as_ref().unwrap()) - } - } - - fn set_connection(&mut self, conn: Connection) { - self.conn = Some(conn); - } - - fn set_channel(&mut self, channel: Channel) { - self.channel = Some(channel) - } -} - -impl MessageConsumer { - pub fn new(db: Database, authifier_db: authifier::Database) -> MessageConsumer { - MessageConsumer { +#[async_trait] +impl Consumer for MessageConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { + Self { db, authifier_db, - conn: None, - channel: None, + connection, + channel, } } - async fn consume_event( - &mut self, - _channel: &Channel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let mut payload: MessageSentPayload = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } - if let Ok(body) = utils::render_notification_content(&payload.notification, &self.db) - .await - .to_internal_error() - { + /// This consumer handles delegating messages into their respective platform queues. + async fn consume(&self, delivery: Delivery) -> Result<()> { + let mut payload: MessageSentPayload = serde_json::from_slice(&delivery.data)?; + + if let Ok(body) = render_notification_content(&payload.notification, &self.db).await { payload.notification.raw_body = Some(payload.notification.body); payload.notification.body = body; } @@ -95,36 +65,22 @@ impl MessageConsumer { extras: HashMap::new(), }; - let args: BasicPublishArguments; + let routing_key = match sub.endpoint.as_str() { + "apn" => &config.pushd.apn.queue, + "fcm" => &config.pushd.fcm.queue, + endpoint => { + sendable.extras.insert("p256dh".to_string(), sub.p256dh); + sendable + .extras + .insert("endpoint".to_string(), endpoint.to_string()); - if sub.endpoint == "apn" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.apn.queue.as_str(), - ) - .finish(); - } else if sub.endpoint == "fcm" { - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.fcm.queue.as_str(), - ) - .finish(); - } else { - // web push (vapid) - args = BasicPublishArguments::new( - config.pushd.exchange.as_str(), - config.pushd.vapid.queue.as_str(), - ) - .finish(); - sendable.extras.insert("p256dh".to_string(), sub.p256dh); - sendable - .extras - .insert("endpoint".to_string(), sub.endpoint.clone()); - } + &config.pushd.vapid.queue + } + }; let payload = serde_json::to_string(&sendable)?; - - publish_message(self, payload.into(), args).await; + self.publish_message(payload.as_bytes(), &config.pushd.exchange, routing_key) + .await?; } } } @@ -132,24 +88,3 @@ impl MessageConsumer { Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for MessageConsumer { - /// This consumer handles delegating messages into their respective platform queues. - async fn consume( - &mut self, - channel: &Channel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process message event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/inbound/mod.rs b/crates/daemons/pushd/src/consumers/inbound/mod.rs index 4d6d36b2..7c93cdb6 100644 --- a/crates/daemons/pushd/src/consumers/inbound/mod.rs +++ b/crates/daemons/pushd/src/consumers/inbound/mod.rs @@ -3,6 +3,5 @@ pub mod dm_call; pub mod fr_accepted; pub mod fr_received; pub mod generic; -mod internal; pub mod mass_mention; pub mod message; diff --git a/crates/daemons/pushd/src/consumers/outbound/apn.rs b/crates/daemons/pushd/src/consumers/outbound/apn.rs index ec478c1f..feb6cb94 100644 --- a/crates/daemons/pushd/src/consumers/outbound/apn.rs +++ b/crates/daemons/pushd/src/consumers/outbound/apn.rs @@ -1,12 +1,13 @@ -use std::{borrow::Cow, collections::BTreeMap, io::Cursor}; +use std::{borrow::Cow, collections::BTreeMap, io::Cursor, sync::Arc}; -use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver}; -use anyhow::{anyhow, Result}; +use crate::utils::Consumer; +use anyhow::Result; use async_trait::async_trait; use base64::{ engine::{self}, Engine as _, }; +use lapin::{message::Delivery, Channel as AMQPChannel, Connection}; use revolt_a2::{ request::{ notification::{DefaultAlert, NotificationOptions}, @@ -42,7 +43,7 @@ impl<'a> PayloadLike for MessagePayload<'a> { fn get_device_token(&self) -> &'a str { self.device_token } - fn get_options(&self) -> &NotificationOptions { + fn get_options(&self) -> &NotificationOptions<'a> { &self.options } } @@ -68,16 +69,20 @@ impl<'a> PayloadLike for CallStartStopPayload<'a> { fn get_device_token(&self) -> &'a str { self.device_token } - fn get_options(&self) -> &NotificationOptions { + fn get_options(&self) -> &NotificationOptions<'a> { &self.options } } // region: consumer +#[derive(Clone)] +#[allow(unused)] pub struct ApnsOutboundConsumer { - #[allow(dead_code)] db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, client: Client, } @@ -117,15 +122,21 @@ impl ApnsOutboundConsumer { } } -impl ApnsOutboundConsumer { - pub async fn new(db: Database) -> Result { +#[async_trait] +impl Consumer for ApnsOutboundConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { let config = revolt_config::config().await; if config.pushd.apn.pkcs8.is_empty() || config.pushd.apn.key_id.is_empty() || config.pushd.apn.team_id.is_empty() { - return Err("Missing APN keys."); + panic!("Missing APN keys."); } let endpoint = if config.pushd.apn.sandbox { @@ -148,18 +159,21 @@ impl ApnsOutboundConsumer { ) .expect("could not create APN client"); - Ok(ApnsOutboundConsumer { db, client }) + Self { + db, + authifier_db, + connection, + channel, + client, + } } - async fn consume_event( - &mut self, - _channel: &AmqpChannel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: PayloadToService = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: PayloadToService = serde_json::from_slice(&delivery.data)?; let payload_options = NotificationOptions { apns_id: None, @@ -170,20 +184,15 @@ impl ApnsOutboundConsumer { apns_collapse_id: None, }; - let resp: Result; - - match payload.notification { + let resp = match payload.notification { PayloadKind::FRReceived(alert) => { let loc_args = vec![Cow::from( - alert - .from_user - .display_name - .or(Some(format!( + alert.from_user.display_name.clone().unwrap_or_else(|| { + format!( "{}#{}", alert.from_user.username, alert.from_user.discriminator - ))) - .clone() - .ok_or_else(|| anyhow!("missing name"))?, + ) + }), )]; let apn_payload = Payload { @@ -216,20 +225,17 @@ impl ApnsOutboundConsumer { "Sending friend request received for user: {:}", &payload.user_id ); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } PayloadKind::FRAccepted(alert) => { let loc_args = vec![Cow::from( - alert - .accepted_user - .display_name - .or(Some(format!( + alert.accepted_user.display_name.clone().unwrap_or_else(|| { + format!( "{}#{}", alert.accepted_user.username, alert.accepted_user.discriminator - ))) - .clone() - .ok_or_else(|| anyhow!("missing name"))?, + ) + }), )]; let apn_payload = Payload { @@ -262,7 +268,7 @@ impl ApnsOutboundConsumer { "Sending friend request accept for user: {:}", &payload.user_id ); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } PayloadKind::Generic(alert) => { let apn_payload = Payload { @@ -295,7 +301,7 @@ impl ApnsOutboundConsumer { "Sending generic notification for user: {:}", &payload.user_id ); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } PayloadKind::MessageNotification(alert) => { @@ -334,7 +340,7 @@ impl ApnsOutboundConsumer { "Sending message notification for user: {:}", &payload.user_id ); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } PayloadKind::BadgeUpdate(badge) => { @@ -349,7 +355,7 @@ impl ApnsOutboundConsumer { }; debug!("Sending badge update for user: {:}", &payload.user_id); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } PayloadKind::DmCallStartEnd(alert) => { @@ -378,58 +384,37 @@ impl ApnsOutboundConsumer { "Sending call start/stop notification for user: {:}", &payload.user_id ); - resp = self.client.send(apn_payload).await; + self.client.send(apn_payload).await } - } + }; - if let Err(err) = resp { - match err { - Error::ResponseError(Response { - error: - Some(ErrorBody { - reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered, - .. - }), - .. - }) => { - info!( - "Removing APNS subscription id {:} (user: {:}) due to invalid token", - &payload.session_id, &payload.user_id - ); - if let Err(err) = self - .db - .remove_push_subscription_by_session_id(&payload.session_id) - .await - { - revolt_config::capture_error(&err); - } - } - err => { + match resp { + Err(Error::ResponseError(Response { + error: + Some(ErrorBody { + reason: ErrorReason::BadDeviceToken | ErrorReason::Unregistered, + .. + }), + .. + })) => { + info!( + "Removing APNS subscription id {:} (user: {:}) due to invalid token", + &payload.session_id, &payload.user_id + ); + + if let Err(err) = self + .db + .remove_push_subscription_by_session_id(&payload.session_id) + .await + { revolt_config::capture_error(&err); } } - } + resp => { + resp?; + } + }; Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for ApnsOutboundConsumer { - async fn consume( - &mut self, - channel: &AmqpChannel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process APN event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/outbound/fcm.rs b/crates/daemons/pushd/src/consumers/outbound/fcm.rs index 9ff70fc8..e3bd185c 100644 --- a/crates/daemons/pushd/src/consumers/outbound/fcm.rs +++ b/crates/daemons/pushd/src/consumers/outbound/fcm.rs @@ -1,17 +1,16 @@ -use std::{collections::HashMap, time::Duration}; +use std::{collections::HashMap, sync::Arc, time::Duration}; -use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver}; - -use anyhow::{anyhow, bail, Result}; +use crate::utils::Consumer; +use anyhow::{bail, Result}; use async_trait::async_trait; use fcm_v1::{ auth::{Authenticator, ServiceAccountKey}, message::Message, Client, Error as FcmError, }; +use lapin::{message::Delivery, Channel as AMQPChannel, Connection}; use revolt_config::config; use revolt_database::{events::rabbit::*, Database}; -use revolt_models::v0::{Channel, PushNotification}; use serde_json::Value; /// Custom notification data @@ -31,10 +30,12 @@ pub enum NotificationData { image: Option, }, Message { - title: String, + message: String, body: String, image: String, - tag: String, + channel: String, + author: String, + author_name: String, }, DmCallStartEnd { initiator_id: String, @@ -81,15 +82,19 @@ impl NotificationData { } } NotificationData::Message { - title, + message, body, image, - tag, + channel, + author, + author_name, } => { - data.insert("title".to_string(), Value::String(title)); + data.insert("message".to_string(), Value::String(message)); data.insert("body".to_string(), Value::String(body)); data.insert("image".to_string(), Value::String(image)); - data.insert("tag".to_string(), Value::String(tag)); + data.insert("channel".to_string(), Value::String(channel)); + data.insert("author".to_string(), Value::String(author)); + data.insert("author_name".to_string(), Value::String(author_name)); } NotificationData::DmCallStartEnd { initiator_id, @@ -110,37 +115,31 @@ impl NotificationData { } } +#[derive(Clone)] +#[allow(unused)] pub struct FcmOutboundConsumer { db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, client: Client, } -impl FcmOutboundConsumer { - fn format_title(&self, notification: &PushNotification) -> String { - // ideally this changes depending on context - // in a server, it would look like "Sendername, #channelname in servername" - // in a group, it would look like "Sendername in groupname" - // in a dm it should just be "Sendername". - // not sure how feasible all those are given the PushNotification object as it currently stands. - - #[allow(deprecated)] - match ¬ification.channel { - Channel::DirectMessage { .. } => notification.author.clone(), - Channel::Group { name, .. } => format!("{}, #{}", notification.author, name), - Channel::TextChannel { name, .. } => { - format!("{} in #{}", notification.author, name) - } - _ => "Unknown".to_string(), - } - } -} - -impl FcmOutboundConsumer { - pub async fn new(db: Database) -> Result { +#[async_trait] +impl Consumer for FcmOutboundConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { let config = revolt_config::config().await; - Ok(FcmOutboundConsumer { + Self { db, + authifier_db, + connection, + channel, client: Client::new( Authenticator::service_account::<&str>(ServiceAccountKey { key_type: Some(config.pushd.fcm.key_type), @@ -160,33 +159,27 @@ impl FcmOutboundConsumer { false, Duration::from_secs(5), ), - }) + } } - async fn consume_event( - &mut self, - _channel: &AmqpChannel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: PayloadToService = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: PayloadToService = serde_json::from_slice(&delivery.data)?; #[allow(clippy::needless_late_init)] let resp: Result; match payload.notification { PayloadKind::FRReceived(alert) => { - let name = alert - .from_user - .display_name - .or(Some(format!( + let name = alert.from_user.display_name.clone().unwrap_or_else(|| { + format!( "{}#{}", alert.from_user.username, alert.from_user.discriminator - ))) - .clone() - .ok_or_else(|| anyhow!("missing name"))?; + ) + }); let data = NotificationData::FRReceived { id: alert.from_user.id, @@ -203,15 +196,12 @@ impl FcmOutboundConsumer { } PayloadKind::FRAccepted(alert) => { - let name = alert - .accepted_user - .display_name - .or(Some(format!( + let name = alert.accepted_user.display_name.clone().unwrap_or_else(|| { + format!( "{}#{}", alert.accepted_user.username, alert.accepted_user.discriminator - ))) - .clone() - .ok_or_else(|| anyhow!("missing name"))?; + ) + }); let data = NotificationData::FRAccepted { id: alert.accepted_user.id, @@ -244,10 +234,12 @@ impl FcmOutboundConsumer { PayloadKind::MessageNotification(alert) => { let data = NotificationData::Message { - title: self.format_title(&alert), + message: alert.message.id, body: alert.body, image: alert.icon, - tag: alert.tag, + channel: alert.message.channel, + author: alert.message.author, + author_name: alert.author, }; let msg = Message { @@ -282,43 +274,21 @@ impl FcmOutboundConsumer { } } - if let Err(err) = resp { - match err { - FcmError::Auth => { - if let Err(err) = self - .db - .remove_push_subscription_by_session_id(&payload.session_id) - .await - { - revolt_config::capture_error(&err); - } - } - err => { + match resp { + Err(FcmError::Auth) => { + if let Err(err) = self + .db + .remove_push_subscription_by_session_id(&payload.session_id) + .await + { revolt_config::capture_error(&err); } } - } + res => { + res?; + } + }; Ok(()) } } - -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for FcmOutboundConsumer { - async fn consume( - &mut self, - channel: &AmqpChannel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process FCM event: {err:?}"); - } - } -} diff --git a/crates/daemons/pushd/src/consumers/outbound/vapid.rs b/crates/daemons/pushd/src/consumers/outbound/vapid.rs index 67ec31fb..0acd8be9 100644 --- a/crates/daemons/pushd/src/consumers/outbound/vapid.rs +++ b/crates/daemons/pushd/src/consumers/outbound/vapid.rs @@ -1,6 +1,6 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -use amqprs::{channel::Channel as AmqpChannel, consumer::AsyncConsumer, BasicProperties, Deliver}; +use crate::utils::Consumer; use anyhow::{anyhow, bail, Result}; use async_trait::async_trait; @@ -8,46 +8,60 @@ use base64::{ engine::{self}, Engine as _, }; +use lapin::{message::Delivery, Channel as AMQPChannel, Connection}; use revolt_database::{events::rabbit::*, util::format_display_name, Database}; use web_push::{ ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, VapidSignatureBuilder, WebPushClient, WebPushError, WebPushMessageBuilder, }; +#[derive(Clone)] +#[allow(unused)] pub struct VapidOutboundConsumer { db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, client: IsahcWebPushClient, - pkey: Vec, + pkey: Arc>, } -impl VapidOutboundConsumer { - pub async fn new(db: Database) -> Result { +#[async_trait] +impl Consumer for VapidOutboundConsumer { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self { let config = revolt_config::config().await; - if config.pushd.vapid.private_key.is_empty() | config.pushd.vapid.public_key.is_empty() { - bail!("no Vapid keys present"); + if config.pushd.vapid.private_key.is_empty() || config.pushd.vapid.public_key.is_empty() { + panic!("no Vapid keys present"); } - let web_push_private_key = engine::general_purpose::URL_SAFE_NO_PAD - .decode(config.pushd.vapid.private_key) - .expect("valid `VAPID_PRIVATE_KEY`"); + let web_push_private_key = Arc::new( + engine::general_purpose::URL_SAFE_NO_PAD + .decode(config.pushd.vapid.private_key) + .expect("valid `VAPID_PRIVATE_KEY`"), + ); - Ok(VapidOutboundConsumer { + Self { db, + authifier_db, + connection, + channel, client: IsahcWebPushClient::new().unwrap(), pkey: web_push_private_key, - }) + } } - async fn consume_event( - &mut self, - _channel: &AmqpChannel, - _deliver: Deliver, - _basic_properties: BasicProperties, - content: Vec, - ) -> Result<()> { - let content = String::from_utf8(content)?; - let payload: PayloadToService = serde_json::from_str(content.as_str())?; + fn channel(&self) -> &Arc { + &self.channel + } + + async fn consume(&self, delivery: Delivery) -> Result<()> { + let payload: PayloadToService = serde_json::from_slice(&delivery.data)?; let subscription = SubscriptionInfo { endpoint: payload @@ -65,10 +79,7 @@ impl VapidOutboundConsumer { }, }; - #[allow(clippy::needless_late_init)] - let payload_body: String; - - match payload.notification { + let payload_body = match payload.notification { PayloadKind::FRReceived(alert) => { let name = alert .from_user @@ -83,7 +94,7 @@ impl VapidOutboundConsumer { let mut body = HashMap::new(); body.insert("body", format!("{} sent you a friend request", name)); - payload_body = serde_json::to_string(&body)?; + serde_json::to_string(&body)? } PayloadKind::FRAccepted(alert) => { let name = alert @@ -99,14 +110,10 @@ impl VapidOutboundConsumer { let mut body = HashMap::new(); body.insert("body", format!("{} accepted your friend request", name)); - payload_body = serde_json::to_string(&body)?; - } - PayloadKind::Generic(alert) => { - payload_body = serde_json::to_string(&alert)?; - } - PayloadKind::MessageNotification(alert) => { - payload_body = serde_json::to_string(&alert)?; + serde_json::to_string(&body)? } + PayloadKind::Generic(alert) => serde_json::to_string(&alert)?, + PayloadKind::MessageNotification(alert) => serde_json::to_string(&alert)?, PayloadKind::DmCallStartEnd(alert) => { let initiator_name = if let Some(server_id) = self.db.fetch_channel(&alert.channel_id).await?.server() @@ -132,59 +139,41 @@ impl VapidOutboundConsumer { _ => bail!("Invalid DmCallStart/End channel type"), } - payload_body = serde_json::to_string(&body)?; + serde_json::to_string(&body)? } PayloadKind::BadgeUpdate(_) => { bail!("Vapid cannot handle badge updates and they should not be sent here."); } - } + }; - match VapidSignatureBuilder::from_pem(std::io::Cursor::new(&self.pkey), &subscription) { - Ok(sig_builder) => match sig_builder.build() { - Ok(signature) => { - let mut builder = WebPushMessageBuilder::new(&subscription); - builder.set_vapid_signature(signature); + let signature = VapidSignatureBuilder::from_pem( + std::io::Cursor::new(self.pkey.as_ref()), + &subscription, + )? + .build()?; - builder.set_payload(ContentEncoding::AesGcm, payload_body.as_bytes()); + let mut builder = WebPushMessageBuilder::new(&subscription); + builder.set_vapid_signature(signature); - match builder.build() { - Ok(msg) => { - if let Err(err) = self.client.send(msg).await { - if err == WebPushError::Unauthorized { - self.db - .remove_push_subscription_by_session_id(&payload.session_id) - .await?; - } - } + builder.set_payload(ContentEncoding::AesGcm, payload_body.as_bytes()); - Ok(()) - } - Err(err) => Err(err.into()), - } + let msg = builder.build()?; + + match self.client.send(msg).await { + Err(WebPushError::Unauthorized) => { + if let Err(err) = self + .db + .remove_push_subscription_by_session_id(&payload.session_id) + .await + { + revolt_config::capture_error(&err); } - Err(err) => Err(err.into()), - }, - Err(err) => Err(err.into()), - } - } -} + } + res => { + res?; + } + }; -#[allow(unused_variables)] -#[async_trait] -impl AsyncConsumer for VapidOutboundConsumer { - async fn consume( - &mut self, - channel: &AmqpChannel, - deliver: Deliver, - basic_properties: BasicProperties, - content: Vec, - ) { - if let Err(err) = self - .consume_event(channel, deliver, basic_properties, content) - .await - { - revolt_config::capture_anyhow(&err); - eprintln!("Failed to process Vapid event: {err:?}"); - } + Ok(()) } } diff --git a/crates/daemons/pushd/src/main.rs b/crates/daemons/pushd/src/main.rs index eaa2e1b8..b4824b65 100644 --- a/crates/daemons/pushd/src/main.rs +++ b/crates/daemons/pushd/src/main.rs @@ -1,17 +1,16 @@ #[macro_use] extern crate log; -use amqprs::{ - channel::{ - BasicConsumeArguments, Channel, ExchangeDeclareArguments, QueueBindArguments, - QueueDeclareArguments, - }, - connection::{Connection, OpenConnectionArguments}, - consumer::AsyncConsumer, - FieldTable, +use std::sync::Arc; + +use lapin::{ + options::{BasicConsumeOptions, ExchangeDeclareOptions, QueueBindOptions, QueueDeclareOptions}, + types::{AMQPValue, FieldTable}, + Channel, Connection, ConnectionProperties, }; use revolt_config::{config, Settings}; -use tokio::sync::Notify; +use revolt_database::Database; +use tokio::signal::ctrl_c; mod consumers; mod utils; @@ -24,6 +23,8 @@ use consumers::{ outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer}, }; +use crate::utils::{Consumer, Delegate}; + #[tokio::main(flavor = "multi_thread", worker_threads = 2)] async fn main() { // Configure logging and environment @@ -43,7 +44,24 @@ async fn main() { panic!("Mongo is not in use, can't connect via authifier!") } - let mut connections: Vec<(Channel, Connection)> = Vec::new(); + let config = config().await; + + let connection = Arc::new( + Connection::connect( + &format!( + "amqp://{}:{}@{}:{}", + &config.rabbit.username, + &config.rabbit.password, + &config.rabbit.host, + &config.rabbit.port, + ), + ConnectionProperties::default(), + ) + .await + .expect("Failed to connect to RabbitMQ"), + ); + + let mut channels = Vec::new(); // An explainer of how this works: // The inbound connections are on separate routing keys, such that they only receive the proper payload @@ -54,171 +72,178 @@ async fn main() { // This'll require some interesting shimming if we need to add more events once this is in prod (different payloads between prod and test), // but that sounds like a problem for future us. - let config = config().await; - - // inbound: generic - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.generic_queue, - config.pushd.get_generic_routing_key().as_str(), + &config.pushd.get_generic_routing_key(), None, - GenericConsumer::new(db.clone(), authifier.clone()), ) .await, ); - // inbound: messages - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.message_queue, - config.pushd.get_message_routing_key().as_str(), + &config.pushd.get_message_routing_key(), None, - MessageConsumer::new(db.clone(), authifier.clone()), ) .await, ); - // inbound: FR received - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.fr_received_queue, - config.pushd.get_fr_received_routing_key().as_str(), + &config.pushd.get_fr_received_routing_key(), None, - FRReceivedConsumer::new(db.clone(), authifier.clone()), ) .await, ); - // inbound: FR accepted - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.fr_accepted_queue, - config.pushd.get_fr_accepted_routing_key().as_str(), + &config.pushd.get_fr_accepted_routing_key(), None, - FRAcceptedConsumer::new(db.clone(), authifier.clone()), ) .await, ); - // inbound: Mass Mentions - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.mass_mention_queue, - config.pushd.get_mass_mention_routing_key().as_str(), + &config.pushd.get_mass_mention_routing_key(), None, - MassMessageConsumer::new(db.clone(), authifier.clone()), ) .await, ); - // inbound: Dm Calls - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.dm_call_queue, - config.pushd.get_dm_call_routing_key().as_str(), + &config.pushd.get_dm_call_routing_key(), None, - DmCallConsumer::new(db.clone(), authifier.clone()), ) .await, ); if !config.pushd.apn.pkcs8.is_empty() { - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.apn.queue, &config.pushd.apn.queue, None, - ApnsOutboundConsumer::new(db.clone()).await.unwrap(), ) .await, ); - let mut table = FieldTable::new(); - table.insert("x-message-deduplication".try_into().unwrap(), "true".into()); + let mut table = FieldTable::default(); + table.insert("x-message-deduplication".into(), AMQPValue::Boolean(true)); - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.ack_queue, &config.pushd.ack_queue, Some(table), - AckConsumer::new(db.clone(), authifier.clone()), ) .await, ); } if !config.pushd.fcm.auth_uri.is_empty() { - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.fcm.queue, &config.pushd.fcm.queue, None, - FcmOutboundConsumer::new(db.clone()).await.unwrap(), ) .await, - ) + ); } if !config.pushd.vapid.public_key.is_empty() { - connections.push( - make_queue_and_consume( + channels.push( + make_queue_and_consume::( + &db, + &authifier, + &connection, &config, &config.pushd.vapid.queue, &config.pushd.vapid.queue, None, - VapidOutboundConsumer::new(db.clone()).await.unwrap(), ) .await, - ) + ); } - let guard = Notify::new(); - guard.notified().await; + ctrl_c().await.unwrap(); - for (channel, conn) in connections { - channel.close().await.expect("Unable to close channel"); - conn.close().await.expect("Unable to close connection"); + for channel in channels { + let _ = channel.close(0, "close".into()).await; } } async fn make_queue_and_consume( + db: &Database, + authifier_db: &authifier::Database, + connection: &Arc, config: &Settings, queue_name: &str, routing_key: &str, queue_args: Option, - consumer: F, -) -> (Channel, Connection) +) -> Arc where - F: AsyncConsumer + Send + 'static, + F: Consumer, { - let connection = Connection::open(&OpenConnectionArguments::new( - &config.rabbit.host, - config.rabbit.port, - &config.rabbit.username, - &config.rabbit.password, - )) - .await - .unwrap(); - - let channel = connection.open_channel(None).await.unwrap(); + let channel = Arc::new(connection.create_channel().await.unwrap()); channel .exchange_declare( - ExchangeDeclareArguments::new(&config.pushd.exchange, "direct") - .durable(true) - .finish(), + config.pushd.exchange.clone().into(), + lapin::ExchangeKind::Direct, + ExchangeDeclareOptions { + durable: true, + ..Default::default() + }, + FieldTable::default(), ) .await - .expect("Failed to declare pushd exchange"); + .expect("Failed to declare exchange"); let mut queue_name = queue_name.to_string(); @@ -230,35 +255,59 @@ where let queue_name = queue_name.as_str(); - let mut args = QueueDeclareArguments::new(queue_name); - args.durable(true); - - if let Some(arg) = queue_args { - args.arguments(arg); - } - - let args = args.finish(); - _ = channel.queue_declare(args).await.unwrap().unwrap(); + let args = QueueDeclareOptions { + durable: true, + ..Default::default() + }; channel - .queue_bind(QueueBindArguments::new( - queue_name, - &config.pushd.exchange, - routing_key, - )) + .queue_declare(queue_name.into(), args, queue_args.unwrap_or_default()) + .await + .unwrap(); + + channel + .queue_bind( + queue_name.into(), + config.pushd.exchange.clone().into(), + routing_key.into(), + QueueBindOptions::default(), + FieldTable::default(), + ) .await .expect( "This probably means the revolt.notifications exchange does not exist in rabbitmq!", ); - let args = BasicConsumeArguments::new(queue_name, "") - .manual_ack(false) - .finish(); - - let routing_key = channel.basic_consume(consumer, args).await.unwrap(); + let consumer = channel + .basic_consume( + queue_name.into(), + "".into(), + BasicConsumeOptions { + no_ack: true, + ..Default::default() + }, + FieldTable::default(), + ) + .await + .unwrap(); info!( "Consuming routing key {} as queue {}, tag {}", - routing_key, queue_name, routing_key + routing_key, + queue_name, + consumer.tag() ); - (channel, connection) + + let delegate = Delegate( + F::create( + db.clone(), + authifier_db.clone(), + connection.clone(), + channel.clone(), + ) + .await, + ); + + consumer.set_delegate(delegate); + + channel } diff --git a/crates/daemons/pushd/src/utils/consumer.rs b/crates/daemons/pushd/src/utils/consumer.rs new file mode 100644 index 00000000..9006aa32 --- /dev/null +++ b/crates/daemons/pushd/src/utils/consumer.rs @@ -0,0 +1,91 @@ +use std::{ + future::{ready, Future}, + pin::Pin, + sync::Arc, +}; + +use anyhow::Result; +use async_trait::async_trait; +use lapin::{ + message::{Delivery, DeliveryResult}, + options::BasicPublishOptions, + BasicProperties, Channel, Connection, ConsumerDelegate, Error as AMQPError, +}; +use log::debug; +use revolt_database::Database; + +#[async_trait] +pub trait Consumer: Clone + Send + Sync + 'static { + async fn create( + db: Database, + authifier_db: authifier::Database, + connection: Arc, + channel: Arc, + ) -> Self; + fn channel(&self) -> &Arc; + async fn consume(&self, delivery: Delivery) -> Result<()>; + + async fn publish_message_with_options( + &self, + payload: &[u8], + exchange: &str, + routing_key: &str, + options: BasicPublishOptions, + properties: BasicProperties, + ) -> Result<(), AMQPError> { + let channel = self.channel(); + + channel + .basic_publish( + exchange.into(), + routing_key.into(), + options, + payload, + properties, + ) + .await?; + debug!("Sent message to queue for target {}", routing_key); + + Ok(()) + } + + async fn publish_message( + &self, + payload: &[u8], + exchange: &str, + routing_key: &str, + ) -> Result<(), AMQPError> { + self.publish_message_with_options( + payload, + exchange, + routing_key, + BasicPublishOptions::default(), + BasicProperties::default(), + ) + .await + } +} + +pub struct Delegate(pub C); + +impl ConsumerDelegate for Delegate { + fn on_new_delivery( + &self, + delivery: DeliveryResult, + ) -> Pin + Send>> { + match delivery { + Ok(Some(delivery)) => { + let consumer = self.0.clone(); + + Box::pin(async move { + if let Err(e) = consumer.consume(delivery).await { + revolt_config::capture_anyhow(&e); + log::error!("{e:?}"); + }; + }) + } + Ok(None) => Box::pin(ready(())), + Err(e) => Box::pin(async move { log::error!("Received bad delivery: {e:?}") }), + } + } +} diff --git a/crates/daemons/pushd/src/utils/mod.rs b/crates/daemons/pushd/src/utils/mod.rs index 62032e80..2a6866e0 100644 --- a/crates/daemons/pushd/src/utils/mod.rs +++ b/crates/daemons/pushd/src/utils/mod.rs @@ -1,2 +1,5 @@ mod renderer; +mod consumer; + pub use renderer::render_notification_content; +pub use consumer::{Consumer, Delegate}; \ No newline at end of file diff --git a/crates/daemons/voice-ingress/Cargo.toml b/crates/daemons/voice-ingress/Cargo.toml index b8d15250..e28e7120 100644 --- a/crates/daemons/voice-ingress/Cargo.toml +++ b/crates/daemons/voice-ingress/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-voice-ingress" -version = "0.12.1" +version = "0.13.7" license = "AGPL-3.0-or-later" edition = "2021" publish = false @@ -44,6 +44,3 @@ revolt-permissions = { workspace = true } livekit-api = { workspace = true } livekit-protocol = { workspace = true } livekit-runtime = { workspace = true, features = ["tokio"] } - -# RabbitMQ -amqprs = { workspace = true } diff --git a/crates/delta/Cargo.toml b/crates/delta/Cargo.toml index 2919ff56..87028aba 100644 --- a/crates/delta/Cargo.toml +++ b/crates/delta/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-delta" -version = "0.12.1" +version = "0.13.7" license = "AGPL-3.0-or-later" authors = ["Paul Makles "] edition = "2018" @@ -64,7 +64,7 @@ schemars = { workspace = true } revolt_rocket_okapi = { workspace = true, features = ["swagger"] } # rabbit -amqprs = { workspace = true } +lapin = { workspace = true, features = ["tokio"] } # core authifier = { workspace = true } diff --git a/crates/delta/src/main.rs b/crates/delta/src/main.rs index 882609cc..c1dd1dac 100644 --- a/crates/delta/src/main.rs +++ b/crates/delta/src/main.rs @@ -9,8 +9,7 @@ pub mod routes; pub mod util; use revolt_config::config; -use revolt_database::events::client::EventV1; -use revolt_database::AMQP; +use revolt_database::{AMQP, events::client::EventV1}; use revolt_ratelimits::rocket as ratelimiter; use rocket::{Build, Rocket}; use rocket_cors::{AllowedOrigins, CorsOptions}; @@ -18,14 +17,10 @@ use rocket_prometheus::PrometheusMetrics; use std::net::Ipv4Addr; use std::str::FromStr; -use amqprs::{ - channel::ExchangeDeclareArguments, - connection::{Connection, OpenConnectionArguments}, -}; use async_std::channel::unbounded; use authifier::AuthifierEvent; -use rocket::data::ToByteUnit; use revolt_database::voice::VoiceClient; +use rocket::data::ToByteUnit; pub async fn web() -> Rocket { // Get settings @@ -36,7 +31,6 @@ pub async fn web() -> Rocket { // Setup database let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap(); - log::info!("database_here {db:?}"); db.migrate_database().await.unwrap(); // Setup Authifier event channel @@ -93,49 +87,11 @@ pub async fn web() -> Rocket { ) .into(); - let swagger_0_8 = revolt_rocket_okapi::swagger_ui::make_swagger_ui( - &revolt_rocket_okapi::swagger_ui::SwaggerUIConfig { - url: "/0.8/openapi.json".to_owned(), - ..Default::default() - }, - ) - .into(); - - let swagger_0_8 = revolt_rocket_okapi::swagger_ui::make_swagger_ui( - &revolt_rocket_okapi::swagger_ui::SwaggerUIConfig { - url: "/0.8/openapi.json".to_owned(), - ..Default::default() - }, - ) - .into(); - // Voice handler let voice_client = VoiceClient::new(config.api.livekit.nodes.clone()); // Configure Rabbit - let connection = Connection::open(&OpenConnectionArguments::new( - &config.rabbit.host, - config.rabbit.port, - &config.rabbit.username, - &config.rabbit.password, - )) - .await - .expect("Failed to connect to RabbitMQ"); - let channel = connection - .open_channel(None) - .await - .expect("Failed to open RabbitMQ channel"); - - channel - .exchange_declare( - ExchangeDeclareArguments::new(&config.pushd.exchange, "direct") - .durable(true) - .finish(), - ) - .await - .expect("Failed to declare exchange"); - - let amqp = AMQP::new(connection, channel); + let amqp = AMQP::new_auto().await; // Launch background task workers revolt_database::tasks::start_workers(db.clone(), amqp.clone()); @@ -153,7 +109,6 @@ pub async fn web() -> Rocket { .mount("/", rocket_cors::catch_all_options_routes()) .mount("/", ratelimiter::routes()) .mount("/swagger/", swagger) - .mount("/0.8/swagger/", swagger_0_8) .manage(authifier) .manage(db) .manage(amqp) @@ -166,6 +121,7 @@ pub async fn web() -> Rocket { limits: rocket::data::Limits::default().limit("string", 5.megabytes()), address: Ipv4Addr::new(0, 0, 0, 0).into(), port: 14702, + ip_header: Some("X-Forwarded-For".into()), ..Default::default() }) } diff --git a/crates/delta/src/routes/channels/channel_ack.rs b/crates/delta/src/routes/channels/channel_ack.rs index 2ae0d3b0..e20da642 100644 --- a/crates/delta/src/routes/channels/channel_ack.rs +++ b/crates/delta/src/routes/channels/channel_ack.rs @@ -1,6 +1,6 @@ use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, - Database, User, + Database, User, AMQP, }; use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_result::{create_error, Result}; @@ -14,6 +14,7 @@ use rocket_empty::EmptyResponse; #[put("//ack/")] pub async fn ack( db: &State, + amqp: &State, user: User, target: Reference<'_>, message: Reference<'_>, @@ -29,7 +30,7 @@ pub async fn ack( .throw_if_lacking_channel_permission(ChannelPermission::ViewChannel)?; channel - .ack(&user.id, message.id) + .ack(&user.id, message.id, amqp) .await .map(|_| EmptyResponse) } diff --git a/crates/delta/src/routes/channels/message_send.rs b/crates/delta/src/routes/channels/message_send.rs index 05c345bb..59c1a9c9 100644 --- a/crates/delta/src/routes/channels/message_send.rs +++ b/crates/delta/src/routes/channels/message_send.rs @@ -1,12 +1,14 @@ use std::time::Duration; use redis_kiss::{get_connection, redis, AsyncCommands}; +use revolt_database::events::client::EventV1; use revolt_database::util::permissions::DatabasePermissionQuery; use revolt_database::{ util::idempotency::IdempotencyKey, util::reference::Reference, Database, User, }; use revolt_database::{Channel, Interactions, Message, AMQP}; use revolt_models::v0; +use revolt_models::v0::ChannelSlowmode; use revolt_permissions::PermissionQuery; use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_result::{create_error, Result}; @@ -84,6 +86,16 @@ pub async fn message_send( .await .unwrap_or(None); + if set_result.is_some() { + let idx_key = format!("slowmode_idx:{}", user.id); + conn.sadd::<_, _, ()>(&idx_key, channel_id.as_str()) + .await + .ok(); + conn.expire::<_, ()>(&idx_key, *channel_slowmode as usize) + .await + .ok(); + } + // If `set_result` is None, the `NX` condition failed because the key already exists. // This means the user is currently in slowmode. if set_result.is_none() { @@ -92,10 +104,29 @@ pub async fn message_send( // Redis returns positive integers for valid TTLs if ttl > 0 { + EventV1::UserSlowmodes { + slowmodes: vec![ChannelSlowmode { + channel_id: channel_id.to_string(), + duration: *channel_slowmode, + retry_after: ttl as u64, + }], + } + .private(user.id.clone()) + .await; return Err(create_error!(InSlowmode { retry_after: ttl as u64 })); } + } else { + EventV1::UserSlowmodes { + slowmodes: vec![ChannelSlowmode { + channel_id: channel_id.to_string(), + duration: *channel_slowmode, + retry_after: *channel_slowmode, + }], + } + .private(user.id.clone()) + .await; } } // If Redis connection fails, just skip the slowmode check diff --git a/crates/delta/src/routes/customisation/emoji_edit.rs b/crates/delta/src/routes/customisation/emoji_edit.rs new file mode 100644 index 00000000..72e99e34 --- /dev/null +++ b/crates/delta/src/routes/customisation/emoji_edit.rs @@ -0,0 +1,212 @@ +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, + Database, EmojiParent, PartialEmoji, User, +}; +use revolt_models::v0; +use revolt_permissions::{calculate_server_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use validator::Validate; + +/// # Edit Emoji +/// +/// Edit an emoji by its id. +#[openapi(tag = "Emojis")] +#[patch("/emoji/", data = "")] +pub async fn edit_emoji( + db: &State, + user: User, + emoji_id: Reference<'_>, + data: Json, +) -> Result> { + let data = data.into_inner(); + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + let mut emoji = emoji_id.as_emoji(db).await?; + + match &emoji.parent { + EmojiParent::Server { id } => { + let server = db.fetch_server(id.as_str()).await?; + + let mut query = DatabasePermissionQuery::new(db, &user).server(&server); + calculate_server_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageCustomisation)?; + } + EmojiParent::Detached => return Err(create_error!(NotAuthenticated)), + } + + if data.name.is_none() { + return Ok(Json(emoji.into())); + } + + let partial = PartialEmoji { name: data.name }; + emoji.update(db, partial).await?; + + Ok(Json(emoji.into())) +} + +#[cfg(test)] +mod test { + use crate::util::test::TestHarness; + use revolt_database::{Emoji, EmojiParent, Member}; + use revolt_models::v0; + use rocket::http::{ContentType, Header, Status}; + use ulid::Ulid; + + #[rocket::async_test] + async fn edit_emoji_name_as_creator() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + let (server, _) = harness.new_server(&user).await; + + let emoji_id = Ulid::new().to_string(); + let emoji = Emoji { + id: emoji_id.clone(), + parent: EmojiParent::Server { + id: server.id.clone(), + }, + creator_id: user.id.clone(), + name: "initial_name".to_string(), + animated: false, + nsfw: false, + }; + emoji.create(&harness.db).await.expect("`Emoji` created"); + + let response = harness + .client + .patch(format!("/custom/emoji/{emoji_id}")) + .header(Header::new("x-session-token", session.token.to_string())) + .header(ContentType::JSON) + .body( + json!(v0::DataEditEmoji { + name: Some("renamed_emoji".to_string()), + }) + .to_string(), + ) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Ok); + + let edited: v0::Emoji = response.into_json().await.expect("`Emoji`"); + assert_eq!(edited.name, "renamed_emoji"); + } + + #[rocket::async_test] + async fn reject_invalid_emoji_name() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + let (server, _) = harness.new_server(&user).await; + + let emoji_id = Ulid::new().to_string(); + let emoji = Emoji { + id: emoji_id.clone(), + parent: EmojiParent::Server { + id: server.id.clone(), + }, + creator_id: user.id.clone(), + name: "valid_name".to_string(), + animated: false, + nsfw: false, + }; + emoji.create(&harness.db).await.expect("`Emoji` created"); + + let response = harness + .client + .patch(format!("/custom/emoji/{emoji_id}")) + .header(Header::new("x-session-token", session.token.to_string())) + .header(ContentType::JSON) + .body( + json!(v0::DataEditEmoji { + name: Some("Invalid Name".to_string()), + }) + .to_string(), + ) + .dispatch() + .await; + + assert_eq!(response.status(), Status::BadRequest); + } + + #[rocket::async_test] + async fn reject_edit_for_detached_emoji() { + let harness = TestHarness::new().await; + let (_, session, user) = harness.new_user().await; + + let emoji_id = Ulid::new().to_string(); + let emoji = Emoji { + id: emoji_id.clone(), + parent: EmojiParent::Detached, + creator_id: user.id.clone(), + name: "detached_name".to_string(), + animated: false, + nsfw: false, + }; + emoji.create(&harness.db).await.expect("`Emoji` created"); + + let response = harness + .client + .patch(format!("/custom/emoji/{emoji_id}")) + .header(Header::new("x-session-token", session.token.to_string())) + .header(ContentType::JSON) + .body( + json!(v0::DataEditEmoji { + name: Some("should_not_apply".to_string()), + }) + .to_string(), + ) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Unauthorized); + } + + #[rocket::async_test] + async fn reject_edit_for_creator_without_manage_customisation() { + let harness = TestHarness::new().await; + let (_, _, owner) = harness.new_user().await; + let (_, creator_session, creator) = harness.new_user().await; + let (server, _) = harness.new_server(&owner).await; + + Member::create(&harness.db, &server, &creator, None) + .await + .expect("`Member` created"); + + let emoji_id = Ulid::new().to_string(); + let emoji = Emoji { + id: emoji_id.clone(), + parent: EmojiParent::Server { + id: server.id.clone(), + }, + creator_id: creator.id.clone(), + name: "member_uploaded_name".to_string(), + animated: false, + nsfw: false, + }; + emoji.create(&harness.db).await.expect("`Emoji` created"); + + let response = harness + .client + .patch(format!("/custom/emoji/{emoji_id}")) + .header(Header::new( + "x-session-token", + creator_session.token.to_string(), + )) + .header(ContentType::JSON) + .body( + json!(v0::DataEditEmoji { + name: Some("renamed_without_permission".to_string()), + }) + .to_string(), + ) + .dispatch() + .await; + + assert_eq!(response.status(), Status::Forbidden); + } +} diff --git a/crates/delta/src/routes/customisation/mod.rs b/crates/delta/src/routes/customisation/mod.rs index 56d40995..5ff6f699 100644 --- a/crates/delta/src/routes/customisation/mod.rs +++ b/crates/delta/src/routes/customisation/mod.rs @@ -3,12 +3,14 @@ use rocket::Route; mod emoji_create; mod emoji_delete; +mod emoji_edit; mod emoji_fetch; pub fn routes() -> (Vec, OpenApi) { openapi_get_routes_spec![ emoji_create::create_emoji, emoji_delete::delete_emoji, + emoji_edit::edit_emoji, emoji_fetch::fetch_emoji ] } diff --git a/crates/delta/src/routes/root.rs b/crates/delta/src/routes/root.rs index 14ead75c..c0fb4fae 100644 --- a/crates/delta/src/routes/root.rs +++ b/crates/delta/src/routes/root.rs @@ -57,6 +57,8 @@ pub struct RevoltFeatures { pub livekit: VoiceFeature, /// Limits pub limits: LimitsConfig, + /// Legal links + pub legal_links: LegalLinks, } /// # Limits For Users @@ -70,6 +72,17 @@ pub struct LimitsConfig { pub default: UserLimits, } +/// # Legal links +#[derive(Serialize, JsonSchema, Debug)] +pub struct LegalLinks { + /// Terms of Service URL + pub terms_of_service: String, + /// Privacy Policy URL + pub privacy_policy: String, + /// Guidelines URL + pub guidelines: String, +} + /// # Global limits #[derive(Serialize, JsonSchema, Debug)] pub struct GlobalLimits { @@ -238,6 +251,11 @@ pub async fn root() -> Result> { new_user: UserLimits::from_feature_limits(config.features.limits.new_user), default: UserLimits::from_feature_limits(config.features.limits.default), }, + legal_links: LegalLinks { + terms_of_service: config.features.legal_links.terms_of_service, + privacy_policy: config.features.legal_links.privacy_policy, + guidelines: config.features.legal_links.guidelines, + }, }, ws: config.hosts.events, app: config.hosts.app, diff --git a/crates/delta/src/routes/servers/roles_edit.rs b/crates/delta/src/routes/servers/roles_edit.rs index 0a122c1f..e46932ef 100644 --- a/crates/delta/src/routes/servers/roles_edit.rs +++ b/crates/delta/src/routes/servers/roles_edit.rs @@ -1,7 +1,7 @@ use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, voice::{sync_voice_permissions, VoiceClient}, - Database, PartialRole, User + Database, File, PartialRole, User, }; use revolt_models::v0; use revolt_permissions::{calculate_server_permissions, ChannelPermission}; @@ -47,14 +47,27 @@ pub async fn edit( name, colour, hoist, + icon, remove, .. } = data; + if remove.contains(&v0::FieldsRole::Icon) { + if let Some(existing_icon) = &role.icon { + db.mark_attachment_as_deleted(&existing_icon.id).await?; + } + } + + let mut final_icon = None; + if let Some(icon_id) = icon { + final_icon = Some(File::use_role_icon(db, &icon_id, &role_id, &user.id).await?); + } + let partial = PartialRole { name, colour, hoist, + icon: final_icon, ..Default::default() }; @@ -69,8 +82,9 @@ pub async fn edit( for channel_id in &server.channels { let channel = Reference::from_unchecked(channel_id).as_channel(db).await?; - sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)).await?; - }; + sync_voice_permissions(db, voice_client, &channel, Some(&server), Some(&role_id)) + .await?; + } Ok(Json(role.into())) } else { diff --git a/crates/delta/src/routes/servers/server_ack.rs b/crates/delta/src/routes/servers/server_ack.rs index 789617e1..81ae683e 100644 --- a/crates/delta/src/routes/servers/server_ack.rs +++ b/crates/delta/src/routes/servers/server_ack.rs @@ -1,6 +1,6 @@ use revolt_database::{ - util::{permissions::DatabasePermissionQuery, reference::Reference}, - Database, User, + util::{acker, permissions::DatabasePermissionQuery, reference::Reference}, + Database, User, AMQP, }; use revolt_permissions::PermissionQuery; use revolt_result::{create_error, Result}; @@ -12,7 +12,12 @@ use rocket_empty::EmptyResponse; /// Mark all channels in a server as read. #[openapi(tag = "Server Information")] #[put("//ack")] -pub async fn ack(db: &State, user: User, target: Reference<'_>) -> Result { +pub async fn ack( + db: &State, + amqp: &State, + user: User, + target: Reference<'_>, +) -> Result { if user.bot.is_some() { return Err(create_error!(IsBot)); } @@ -23,7 +28,6 @@ pub async fn ack(db: &State, user: User, target: Reference<'_>) -> Res return Err(create_error!(NotFound)); } - db.acknowledge_channels(&user.id, &server.channels) - .await - .map(|_| EmptyResponse) + acker::ack_server(&user, &server, db, amqp).await?; + Ok(EmptyResponse) } diff --git a/crates/delta/src/routes/webhooks/mod.rs b/crates/delta/src/routes/webhooks/mod.rs index 6b0ef448..1aec16f4 100644 --- a/crates/delta/src/routes/webhooks/mod.rs +++ b/crates/delta/src/routes/webhooks/mod.rs @@ -1,19 +1,23 @@ -use rocket::Route; use revolt_rocket_okapi::revolt_okapi::openapi3::OpenApi; +use rocket::Route; mod webhook_delete; +mod webhook_delete_message; mod webhook_delete_token; mod webhook_edit; +mod webhook_edit_message; mod webhook_edit_token; mod webhook_execute; -mod webhook_fetch_token; -mod webhook_fetch; mod webhook_execute_github; +mod webhook_fetch; +mod webhook_fetch_token; pub fn routes() -> (Vec, OpenApi) { openapi_get_routes_spec![ + webhook_delete_message::webhook_delete_message, webhook_delete_token::webhook_delete_token, webhook_delete::webhook_delete, + webhook_edit_message::webhook_edit_message, webhook_edit_token::webhook_edit_token, webhook_edit::webhook_edit, webhook_execute_github::webhook_execute_github, diff --git a/crates/delta/src/routes/webhooks/webhook_delete_message.rs b/crates/delta/src/routes/webhooks/webhook_delete_message.rs new file mode 100644 index 00000000..be4cb63b --- /dev/null +++ b/crates/delta/src/routes/webhooks/webhook_delete_message.rs @@ -0,0 +1,27 @@ +use revolt_database::{util::reference::Reference, Database}; +use revolt_result::{create_error, Result}; +use rocket::State; +use rocket_empty::EmptyResponse; + +/// # Deletes a webhook message +/// +/// Deletes a message sent by a webhook +#[openapi(tag = "Webhooks")] +#[delete("///")] +pub async fn webhook_delete_message( + db: &State, + webhook_id: Reference<'_>, + token: String, + message_id: Reference<'_>, +) -> Result { + let webhook = webhook_id.as_webhook(db).await?; + webhook.assert_token(&token)?; + + let message = message_id.as_message(db).await?; + + if message.author != webhook.id { + return Err(create_error!(CannotDeleteMessage)); + } + + message.delete(db).await.map(|_| EmptyResponse) +} diff --git a/crates/delta/src/routes/webhooks/webhook_edit_message.rs b/crates/delta/src/routes/webhooks/webhook_edit_message.rs new file mode 100644 index 00000000..a6e4036f --- /dev/null +++ b/crates/delta/src/routes/webhooks/webhook_edit_message.rs @@ -0,0 +1,84 @@ +use iso8601_timestamp::Timestamp; +use revolt_config::config; +use revolt_database::{ + tasks::process_embeds::queue, util::reference::Reference, Database, Message, PartialMessage, +}; +use revolt_models::v0::{self, DataEditMessage, Embed}; +use revolt_models::validator::Validate; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +/// # Edits a webhook message +/// +/// Edits a message sent by a webhook +#[openapi(tag = "Webhooks")] +#[patch("///", data = "")] +pub async fn webhook_edit_message( + db: &State, + webhook_id: Reference<'_>, + token: String, + message_id: Reference<'_>, + data: Json, +) -> Result> { + let edit = data.into_inner(); + edit.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + Message::validate_sum( + &edit.content, + edit.embeds.as_deref().unwrap_or_default(), + config().await.features.limits.default.message_length, + )?; + + let webhook = webhook_id.as_webhook(db).await?; + webhook.assert_token(&token)?; + + let mut message = message_id.as_message(db).await?; + if message.author != webhook.id { + return Err(create_error!(CannotEditMessage)); + } + + message.edited = Some(Timestamp::now_utc()); + let mut partial = PartialMessage { + edited: message.edited, + ..Default::default() + }; + + // 1. Handle content update + if let Some(content) = &edit.content { + partial.content = Some(content.clone()); + } + + // 2. Clear any auto generated embeds + let mut new_embeds = vec![]; + if let Some(embeds) = &message.embeds { + for embed in embeds { + if let Embed::Text(embed) = embed { + new_embeds.push(Embed::Text(embed.clone())) + } + } + } + + // 3. Replace if we are given new embeds + if let Some(embeds) = edit.embeds { + new_embeds.clear(); + + for embed in embeds { + new_embeds.push(message.create_embed(db, embed).await?); + } + } + + partial.embeds = Some(new_embeds); + + message.update(db, partial, vec![]).await?; + + // Queue up a task for processing embeds + if let Some(content) = edit.content { + queue(message.channel.to_string(), message.id.to_string(), content).await; + } + + Ok(Json(message.into_model(None, None))) +} diff --git a/crates/delta/src/util/test.rs b/crates/delta/src/util/test.rs index 0c53b668..a283f1dc 100644 --- a/crates/delta/src/util/test.rs +++ b/crates/delta/src/util/test.rs @@ -4,7 +4,7 @@ use authifier::{ }; use futures::StreamExt; use rand::Rng; -use redis_kiss::redis::aio::PubSub; +use redis_kiss::{redis::aio::PubSub}; use revolt_database::{ events::client::EventV1, Channel, Database, Member, Message, PartialRole, Server, User, AMQP, }; @@ -25,8 +25,6 @@ pub struct TestHarness { impl TestHarness { pub async fn new() -> TestHarness { - let config = revolt_config::config().await; - let client = Client::tracked(crate::web().await) .await .expect("valid rocket instance"); @@ -49,19 +47,7 @@ impl TestHarness { .expect("`Authifier`") .clone(); - let connection = amqprs::connection::Connection::open( - &amqprs::connection::OpenConnectionArguments::new( - &config.rabbit.host, - config.rabbit.port, - &config.rabbit.username, - &config.rabbit.password, - ), - ) - .await - .unwrap(); - let channel = connection.open_channel(None).await.unwrap(); - - let amqp = AMQP::new(connection, channel); + let amqp = AMQP::new_auto().await; TestHarness { client, diff --git a/crates/services/autumn/Cargo.toml b/crates/services/autumn/Cargo.toml index 762c4ee3..7c134170 100644 --- a/crates/services/autumn/Cargo.toml +++ b/crates/services/autumn/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-autumn" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" publish = false @@ -18,6 +18,7 @@ kamadak-exif = { workspace = true } # revolt_little_exif = "0.5.1" image = { workspace = true } thumbhash = { workspace = true } +lcms2 = { workspace = true } # File processing revolt_clamav-client = { workspace = true } @@ -31,7 +32,7 @@ imagesize = { workspace = true } # Utility lazy_static = { workspace = true } moka = { workspace = true, features = ["future"] } - +url-escape = { workspace = true } # Serialisation strum_macros = { workspace = true } serde_json = { workspace = true } diff --git a/crates/services/autumn/src/api.rs b/crates/services/autumn/src/api.rs index 49a61c43..c0e45d72 100644 --- a/crates/services/autumn/src/api.rs +++ b/crates/services/autumn/src/api.rs @@ -24,6 +24,7 @@ use sha2::Digest; use tempfile::NamedTempFile; use tokio::time::Instant; use tower_http::cors::{AllowHeaders, Any, CorsLayer}; +use url_escape::encode_component; use utoipa::ToSchema; use crate::{ @@ -479,8 +480,10 @@ async fn fetch_file( // Ensure filename is correct if file_name != file.filename { if file_name == "original" { + let safe_filename = encode_component(&file.filename); + return Ok( - Redirect::permanent(&format!("/{tag}/{file_id}/{}", file.filename)).into_response(), + Redirect::permanent(&format!("/{tag}/{file_id}/{}", safe_filename)).into_response(), ); } diff --git a/crates/services/autumn/src/exif.rs b/crates/services/autumn/src/exif.rs index af02953e..be8e6150 100644 --- a/crates/services/autumn/src/exif.rs +++ b/crates/services/autumn/src/exif.rs @@ -1,13 +1,24 @@ use std::io::{Cursor, Read}; +use crate::utils::apply_icc_profile; use exif::Reader; -use image::{ImageFormat, ImageReader}; +use image::{ImageEncoder, ImageReader}; use revolt_config::report_internal_error; use revolt_database::Metadata; use revolt_result::{create_error, Result}; use tempfile::NamedTempFile; use tokio::process::Command; +macro_rules! encode_with_icc { + ($encoder:expr, $icc:expr, $image:expr, $width:expr, $height:expr, $color:expr) => {{ + let mut encoder = $encoder; + if let Some(icc) = $icc { + let _ = encoder.set_icc_profile(icc.clone()); + } + encoder.write_image($image, $width, $height, $color) + }}; +} + /// Strip EXIF data from given file and produce new file and metadata pub async fn strip_metadata( file: NamedTempFile, @@ -17,8 +28,8 @@ pub async fn strip_metadata( ) -> Result<(Vec, Metadata)> { match &metadata { Metadata::Image { - width, - height, + width: _, + height: _, thumbhash, animated, } => match mime { @@ -46,11 +57,12 @@ pub async fn strip_metadata( let mut cursor = Cursor::new(buf); // Decode the image - let image = report_internal_error!(report_internal_error!(ImageReader::new( - &mut cursor - ) - .with_guessed_format())? - .decode()); + let reader = + report_internal_error!(ImageReader::new(&mut cursor).with_guessed_format())?; + let mut decoder = report_internal_error!(reader.into_decoder())?; + let mut icc_profile = + report_internal_error!(image::ImageDecoder::icc_profile(&mut decoder))?; + let mut image = report_internal_error!(image::DynamicImage::from_decoder(decoder))?; // Reset read position cursor.set_position(0); @@ -71,38 +83,68 @@ pub async fn strip_metadata( // Apply the EXIF rotation // See https://jdhao.github.io/2019/07/31/image_rotation_exif_info/ - report_internal_error!(match &rotation { - 2 => image?.fliph(), - 3 => image?.rotate180(), - 4 => image?.rotate180().fliph(), - 5 => image?.rotate90().fliph(), - 6 => image?.rotate90(), - 7 => image?.rotate270().fliph(), - 8 => image?.rotate270(), - _ => image?, - } - .write_to( - &mut writer, - match mime { - "image/jpeg" => ImageFormat::Jpeg, - "image/png" => ImageFormat::Png, - "image/avif" => ImageFormat::Avif, - "image/tiff" => ImageFormat::Tiff, - _ => todo!(), - }, - ))?; - - // Calculate dimensions after rotation. - let (width, height) = match &rotation { - 2 | 4 | 5 | 7 => (*height, *width), - _ => (*width, *height), + image = match &rotation { + 2 => image.fliph(), + 3 => image.rotate180(), + 4 => image.rotate180().fliph(), + 5 => image.rotate90().fliph(), + 6 => image.rotate90(), + 7 => image.rotate270().fliph(), + 8 => image.rotate270(), + _ => image, }; + if let Some(icc) = &icc_profile { + image = apply_icc_profile(image, icc); + icc_profile = None; + } + + let color_type = image.color(); + let width = image.width(); + let height = image.height(); + + report_internal_error!(match mime { + "image/jpeg" => encode_with_icc!( + image::codecs::jpeg::JpegEncoder::new(&mut writer), + &icc_profile, + image.as_bytes(), + width, + height, + color_type.into() + ), + "image/png" => encode_with_icc!( + image::codecs::png::PngEncoder::new(&mut writer), + &icc_profile, + image.as_bytes(), + width, + height, + color_type.into() + ), + "image/avif" => { + // avif encoder doesn't implement set_icc_profile currently + image::codecs::avif::AvifEncoder::new(&mut writer).write_image( + image.as_bytes(), + width, + height, + color_type.into(), + ) + } + "image/tiff" => encode_with_icc!( + image::codecs::tiff::TiffEncoder::new(&mut writer), + &icc_profile, + image.as_bytes(), + width, + height, + color_type.into() + ), + _ => unreachable!(), + })?; + Ok(( bytes, Metadata::Image { - width, - height, + width: width as isize, + height: height as isize, thumbhash: thumbhash.clone(), animated: *animated, }, diff --git a/crates/services/autumn/src/main.rs b/crates/services/autumn/src/main.rs index cb936e0c..e6234318 100644 --- a/crates/services/autumn/src/main.rs +++ b/crates/services/autumn/src/main.rs @@ -18,6 +18,7 @@ pub mod exif; pub mod metadata; pub mod mime_type; mod ratelimits; +mod utils; #[derive(FromRef, Clone)] struct AppState { diff --git a/crates/services/autumn/src/metadata.rs b/crates/services/autumn/src/metadata.rs index 8e0c89cd..c476eac4 100644 --- a/crates/services/autumn/src/metadata.rs +++ b/crates/services/autumn/src/metadata.rs @@ -1,5 +1,6 @@ use std::io::Cursor; +use crate::utils::apply_icc_profile; use image::{GenericImageView, ImageError, ImageReader}; use revolt_database::Metadata; use revolt_files::{image_size, is_animated, video_size}; @@ -27,16 +28,26 @@ pub fn generate_metadata(f: &NamedTempFile, mime_type: &str) -> Metadata { .map(|(width, height)| Metadata::Image { width: width as isize, height: height as isize, - thumbhash: ImageReader::open(f) - .and_then(|r| r.with_guessed_format()) - .map_err(ImageError::from) - .and_then(|r| r.decode()) - .map(|img| img.thumbnail(100, 100)) - .map(|img| (img.dimensions(), img.to_rgba8().into_raw())) - .map(|((width, height), rgba)| { - thumbhash::rgba_to_thumb_hash(width as usize, height as usize, &rgba) - }) - .ok(), + thumbhash: (|| { + let reader = ImageReader::open(f).ok()?.with_guessed_format().ok()?; + let mut decoder = reader.into_decoder().ok()?; + let icc_profile = image::ImageDecoder::icc_profile(&mut decoder) + .ok() + .flatten(); + let mut img = image::DynamicImage::from_decoder(decoder).ok()?; + + if let Some(icc) = icc_profile { + img = apply_icc_profile(img, &icc); + } + + let img = img.thumbnail(100, 100); + let (width, height) = img.dimensions(); + Some(thumbhash::rgba_to_thumb_hash( + width as usize, + height as usize, + &img.into_rgba8().into_raw(), + )) + })(), animated: is_animated(f, mime_type).or(Some(false)), }) .unwrap_or_default() diff --git a/crates/services/autumn/src/utils.rs b/crates/services/autumn/src/utils.rs new file mode 100644 index 00000000..5e2fbfb8 --- /dev/null +++ b/crates/services/autumn/src/utils.rs @@ -0,0 +1,31 @@ +/// Convert image to sRGB using the provided ICC profile. +/// Returns the converted image, or the original if conversion fails. +pub fn apply_icc_profile(image: image::DynamicImage, icc: &[u8]) -> image::DynamicImage { + let Ok(src_profile) = lcms2::Profile::new_icc(icc) else { + return image; + }; + let dst_profile = lcms2::Profile::new_srgb(); + let format = if image.color().has_alpha() { + lcms2::PixelFormat::RGBA_8 + } else { + lcms2::PixelFormat::RGB_8 + }; + let Ok(t) = lcms2::Transform::new( + &src_profile, + format, + &dst_profile, + format, + lcms2::Intent::Perceptual, + ) else { + return image; + }; + if image.color().has_alpha() { + let mut rgba_image = image.into_rgba8(); + t.transform_in_place(rgba_image.as_mut()); + image::DynamicImage::ImageRgba8(rgba_image) + } else { + let mut rgb_image = image.into_rgb8(); + t.transform_in_place(rgb_image.as_mut()); + image::DynamicImage::ImageRgb8(rgb_image) + } +} diff --git a/crates/services/gifbox/Cargo.toml b/crates/services/gifbox/Cargo.toml index 6e7b467c..a569b2cd 100644 --- a/crates/services/gifbox/Cargo.toml +++ b/crates/services/gifbox/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-gifbox" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" publish = false diff --git a/crates/services/january/Cargo.toml b/crates/services/january/Cargo.toml index 78ec07ad..f72f6572 100644 --- a/crates/services/january/Cargo.toml +++ b/crates/services/january/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "revolt-january" -version = "0.12.1" +version = "0.13.7" edition = "2021" license = "AGPL-3.0-or-later" publish = false @@ -27,6 +27,8 @@ tokio = { workspace = true, features = [] } # Web requests reqwest = { workspace = true, features = ["json"] } +pdk-ip-filter-lib = "1.8.0" +url = { workspace = true } # Logging tracing = { workspace = true } diff --git a/crates/services/january/src/requests.rs b/crates/services/january/src/requests.rs index cf681171..c818619b 100644 --- a/crates/services/january/src/requests.rs +++ b/crates/services/january/src/requests.rs @@ -1,42 +1,43 @@ use encoding_rs::{Encoding, UTF_8_INIT}; use lazy_static::lazy_static; use mime::Mime; +use pdk_ip_filter_lib::IpFilter; use regex::Regex; use reqwest::{ + dns::{Addrs, Name, Resolve}, header::{self, CONTENT_TYPE}, redirect, Client, Response, }; -use revolt_config::report_internal_error; +use revolt_config::{config, report_internal_error}; use revolt_files::{create_thumbnail, decode_image, image_size_vec, is_valid_image, video_size}; use revolt_models::v0::{Embed, Image, ImageSize, Video}; -use revolt_result::{create_error, Error, Result}; +use revolt_result::{create_error, Error, Result, ToRevoltError}; +use std::net::{IpAddr, SocketAddr}; use std::{ io::{Cursor, Write}, + str::FromStr, time::Duration, }; +use url::{Host, Url}; lazy_static! { /// Request client static ref CLIENT: Client = reqwest::Client::builder() + .dns_resolver(CachedDnsResolver {}) .timeout(Duration::from_secs(10)) // TODO config .connect_timeout(Duration::from_secs(5)) // TODO config - .redirect(redirect::Policy::custom(|attempt| { - if attempt.previous().len() > 5 { // TODO config - attempt.error("too many redirects") - } else if attempt.url().host_str() == Some("jan.revolt.chat") { // TODO config - attempt.stop() - } else { - attempt.follow() - } - })) + .redirect(redirect::Policy::none()) .build() .expect("reqwest Client"); /// Spoof User Agent as Discord - static ref RE_USER_AGENT_SPOOFING_AS_DISCORD: Regex = Regex::new("^(?:(?:https?:)?//)?(?:(?:vx|fx)?twitter|(?:fixv|fixup)?x|(?:old\\.|new\\.|www\\.)reddit).com").expect("valid regex"); + static ref RE_USER_AGENT_SPOOFING_AS_DISCORD: Regex = Regex::new("^(?:(?:vx|fx)?twitter|(?:fixv|fixup)?x|(?:old\\.|new\\.|www\\.)reddit).com").expect("valid regex"); /// Regex for matching new Reddit URLs - static ref RE_URL_NEW_REDDIT: Regex = Regex::new("^(?:(?:https?:)?//)?(?:(?:new\\.|www\\.)?reddit).com").expect("valid regex"); + static ref RE_URL_NEW_REDDIT: Regex = Regex::new("^(?:(?:new\\.|www\\.)?reddit).com").expect("valid regex"); + + /// Regex for matching YouTube Shorts URLs + static ref RE_URL_YOUTUBE_SHORTS: Regex = Regex::new("^(?:(?:https?:)?//)?(?:(?:www\\.)?youtube\\.com)/shorts/([a-zA-Z0-9_-]+)").expect("valid regex"); /// Cache for proxy results static ref PROXY_CACHE: moka::future::Cache)>> = moka::future::Cache::builder() @@ -59,6 +60,72 @@ lazy_static! { .max_capacity(10_000) // Cache up to 10k embeds .time_to_live(Duration::from_secs(60)) // For up to 1 minute .build(); + + static ref DNS_CACHE: moka::future::Cache> = moka::future::Cache::builder() + .max_capacity(10_000) + .time_to_idle(Duration::from_secs(30)) + .build(); + + static ref IP_BLOCKLIST: IpFilter = IpFilter::block(&[ + "0.0.0.0/8", + "10.0.0.0/8", + "192.168.0.0/16", + "127.0.0.0/8", + "172.16.0.0/12", + "169.254.0.0/16", + "::1", + "fc00::/7", + ] + ).unwrap(); +} + +#[derive(Clone)] +pub struct IPRequest { + url: Url, + ip: IpAddr, + pub blocked: bool, +} + +impl From for Url { + fn from(value: IPRequest) -> Self { + let mut url = value.url.clone(); + url.set_host(Some(&value.ip.to_string())) + .map(|_| url) + .unwrap_or(value.url) + } +} + +struct CachedDnsResolver {} + +impl reqwest::dns::Resolve for CachedDnsResolver { + fn resolve(&self, name: Name) -> reqwest::dns::Resolving { + Box::pin(async move { + { + if let Some(addrs) = DNS_CACHE.get(&name.as_str().to_string()).await { + let resp: Addrs = Box::new(addrs.clone().into_iter()); + return Ok(resp); + } + } + + let mut lookup = name.as_str().to_string(); + if !lookup.contains(":") { + lookup += ":0"; + } + + let fallback: Vec = tokio::net::lookup_host(&lookup) + .await + .map_err(|e| -> Box { Box::new(e) })? + .collect(); + + { + DNS_CACHE + .insert(name.as_str().to_string().clone(), fallback.clone()) + .await; + let addrs: Addrs = Box::new(fallback.clone().into_iter()); + Ok(addrs) + } + }) + } } /// Information about a successful request @@ -73,7 +140,7 @@ impl Request { if let Some(hit) = PROXY_CACHE.get(url).await { hit } else { - let Request { response, mime } = Request::new(url).await?; + let Request { response, mime } = Request::new_from_str(url).await?; if matches!(mime.type_(), mime::IMAGE | mime::VIDEO) { let bytes = report_internal_error!(response.bytes().await); @@ -135,7 +202,7 @@ impl Request { let request = if let Some(request) = request { request } else { - let request = Request::new(url).await?; + let request = Request::new_from_str(url).await?; if matches!(request.mime.type_(), mime::IMAGE) { request } else { @@ -173,7 +240,7 @@ impl Request { let response = if let Some(Request { response, .. }) = request { response } else { - let Request { response, mime } = Request::new(url).await?; + let Request { response, mime } = Request::new_from_str(url).await?; if matches!(mime.type_(), mime::VIDEO) { response } else { @@ -208,11 +275,18 @@ impl Request { .to_string(); } + // Re-map Youtube Shorts to regular Youtube links + if let Some(captures) = RE_URL_YOUTUBE_SHORTS.captures(&url) { + if let Some(video_id) = captures.get(1) { + url = format!("https://youtube.com/watch?v={}", video_id.as_str()); + } + } + // Generate the actual embed if let Some(hit) = EMBED_CACHE.get(&url).await { Ok(hit) } else { - let request = Request::new(&url).await?; + let request = Request::new_from_str(&url).await?; let embed = match (request.mime.type_(), request.mime.subtype()) { (_, mime::HTML) => { let content_type = request @@ -255,15 +329,27 @@ impl Request { } /// Send a new request to a service - pub async fn new(url: &str) -> Result { - let response = CLIENT + pub async fn new(url: Url) -> Result { + let mut url = url; + let url_host_str = url.host_str().ok_or(create_error!(ProxyError))?.to_string(); + + let mut blocker = Request::url_is_blacklisted(&url).await?; + + if blocker.blocked { + return Err(create_error!(InvalidOperation)); + } + + let mut redirect_count = 0; + + loop { + let response = CLIENT .get(url) .header( "User-Agent", - if RE_USER_AGENT_SPOOFING_AS_DISCORD.is_match(url) { + if RE_USER_AGENT_SPOOFING_AS_DISCORD.is_match(&url_host_str) { "Mozilla/5.0 (compatible; Discordbot/2.0; +https://discordapp.com)" } else { - "Mozilla/5.0 (compatible; January/2.0; +https://github.com/revoltchat/backend)" + "Mozilla/5.0 (compatible; January/2.0; +https://github.com/stoatchat/stoatchat)" }, ) .header("Accept-Language", "en-US,en;q=0.5") @@ -271,31 +357,130 @@ impl Request { .await .map_err(|_| create_error!(ProxyError))?; - if !response.status().is_success() { - tracing::error!("{:?}", response); - return Err(create_error!(ProxyError)); + if response.status().is_redirection() { + redirect_count += 1; + + if redirect_count > 5 { + return Err(create_error!(ProxyError)); + } + if let Some(location) = response.headers().get("location") { + let location = location.to_str().map_err(|_| create_error!(ProxyError))?; + url = Url::from_str(location).to_internal_error()?; + + blocker = Request::url_is_blacklisted(&url).await?; + + if blocker.blocked { + return Err(create_error!(InvalidOperation)); + } + + continue; + } else { + return Err(create_error!(ProxyError)); + } + } + + if !response.status().is_success() { + tracing::error!("{:?}", response); + return Err(create_error!(ProxyError)); + } + + let content_type = response + .headers() + .get(CONTENT_TYPE) + .ok_or(create_error!(ProxyError))? + .to_str() + .map_err(|_| create_error!(ProxyError))?; + + let mime: mime::Mime = content_type + .parse() + .map_err(|_| create_error!(ProxyError))?; + + return Ok(Request { response, mime }); } + } - let content_type = response - .headers() - .get(CONTENT_TYPE) - .ok_or(create_error!(ProxyError))? - .to_str() - .map_err(|_| create_error!(ProxyError))?; - - let mime: mime::Mime = content_type - .parse() - .map_err(|_| create_error!(ProxyError))?; - - Ok(Request { response, mime }) + pub async fn new_from_str(url: &str) -> Result { + let proper_url = Url::parse(url).map_err(|_| create_error!(ProxyError))?; + Request::new(proper_url).await } /// Check if something exists - pub async fn exists(url: &str) -> bool { + pub async fn exists(url: Url) -> bool { if let Ok(response) = CLIENT.head(url).send().await { response.status().is_success() } else { false } } + + pub async fn exists_from_str(url: &str) -> Result { + let proper_url = Url::parse(url).map_err(|_| create_error!(ProxyError))?; + Ok(Request::exists(proper_url).await) + } + + pub async fn url_is_blacklisted(url: &Url) -> Result { + let resolved_address: IpAddr; + + if let Some(host) = url.host() { + match host { + Host::Ipv4(ipv4) => { + resolved_address = ipv4.into(); + if !IP_BLOCKLIST.is_allowed(&ipv4.to_string()) { + return Err(create_error!(InvalidOperation)); + } + } + Host::Ipv6(ipv6) => { + resolved_address = ipv6.into(); + if !IP_BLOCKLIST.is_allowed(&ipv6.to_string()) { + return Err(create_error!(InvalidOperation)); + } + } + Host::Domain(domain) => { + let domain = domain.to_string(); + + let config = config().await; + + // First step: TLDs and blocked domains + if !domain.contains(".") // lazily block TLDs + || config.january.blocked_domains.iter().any(|x| x == &domain) + { + return Err(create_error!(InvalidOperation)); + } + + // Second step: resolve the IP and check the blocklist + let resolver = CachedDnsResolver {}; + if let Ok(mut resolved_ip) = resolver + .resolve( + Name::from_str(&domain) + .map_err(|_| create_error!(ProxyError)) + .unwrap(), + ) + .await + { + if let Some(resolved_ip) = resolved_ip.next() { + resolved_address = resolved_ip.ip(); + let resolved_string = resolved_address.to_string(); + if !IP_BLOCKLIST.is_allowed(&resolved_string) + || resolved_string.contains("::ffff:") + { + return Err(create_error!(InvalidOperation)); + } + } else { + return Err(create_error!(InvalidOperation)); + } + } else { + return Err(create_error!(ProxyError)); + } + } + } + } else { + return Err(create_error!(ProxyError)); + }; + + Ok(IPRequest { + url: url.clone(), + ip: resolved_address, + blocked: false, + }) + } } diff --git a/crates/services/january/src/website_embed.rs b/crates/services/january/src/website_embed.rs index 25de1537..0e025b73 100644 --- a/crates/services/january/src/website_embed.rs +++ b/crates/services/january/src/website_embed.rs @@ -190,7 +190,7 @@ pub async fn create_website_embed(original_url: &str, document: &str) -> Option< pub async fn populate_special(original_url: String, metadata: &mut WebsiteMetadata) { lazy_static! { - static ref RE_YOUTUBE: Regex = Regex::new("^(?:(?:https?:)?//)?(?:(?:www|m)\\.)?(?:(?:youtube\\.com|youtu.be))(?:/(?:[\\w\\-]+\\?v=|embed/|v/)?)([\\w\\-]+)(?:\\S+)?$").unwrap(); + static ref RE_YOUTUBE: Regex = Regex::new("^(?:(?:https?:)?//)?(?:(?:www|m)\\.)?(?:(?:youtube\\.com|youtu.be))(?:/(?:[\\w\\-]+\\?v=|embed/|v/|shorts/)?)([\\w\\-]+)(?:\\S+)?$").unwrap(); static ref RE_LIGHTSPEED: Regex = Regex::new("^(?:https?://)?(?:[\\w]+\\.)?lightspeed\\.tv/([a-z0-9_]{4,25})").unwrap(); @@ -236,11 +236,12 @@ pub async fn populate_special(original_url: String, metadata: &mut WebsiteMetada metadata.site_name.take(); // Verify the video exists - if !crate::requests::Request::exists(&format!( + if !crate::requests::Request::exists_from_str(&format!( "http://img.youtube.com/vi/{}/sddefault.jpg", id )) .await + .unwrap_or(false) { return; } diff --git a/docs/docs/developers/events/protocol.md b/docs/docs/developers/events/protocol.md index 1c8a8618..6e0c8d5e 100644 --- a/docs/docs/developers/events/protocol.md +++ b/docs/docs/developers/events/protocol.md @@ -539,6 +539,22 @@ Emoji created, the event object has the same schema as the Emoji object in the A } ``` +### EmojiUpdate + +Emoji has been updated. + +```json +{ + "type": "EmojiUpdate", + "id": "{emoji_id}", + "data": { + "name"?: "{emoji_name}" + } +} +``` + +- `data` field contains a partial Emoji object. + ### EmojiDelete Emoji has been deleted. diff --git a/version.txt b/version.txt index 34a83616..5daaa7ba 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.12.1 +0.13.7