Compare commits

...

266 Commits

Author SHA1 Message Date
Zomatree
a22378c35c fix(docs): update react version (#842)
Signed-off-by: Zomatree <me@zomatree.live>
2026-06-27 04:24:00 +01:00
Zomatree
164be6a2da Revert "chore(deps): update dependency gh to v2.95.0" (#840)
Signed-off-by: Zomatree <me@zomatree.live>
2026-06-25 18:57:12 +01:00
Zomatree
9217c6e49b ci: fix concurrency group (#837)
Signed-off-by: Zomatree <me@zomatree.live>
2026-06-25 13:38:39 +01:00
stoat-release[bot]
23ba7d8fd8 chore(deps): update mwader/static-ffmpeg docker tag to v7.1.1 (#827)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-06-24 10:54:14 -07:00
stoat-release[bot]
6ece5cde3b chore(deps): update dependency gh to v2.95.0 (#822)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-06-24 10:47:21 -07:00
stoat-release[bot]
7c2098c611 chore(deps): update dependency github:git-town/git-town to v22.7.1 (#823)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-06-24 10:46:38 -07:00
Zomatree
01c7d925b0 chore: replace async-std with tokio (#813)
Signed-off-by: Zomatree <me@zomatree.live>
2026-06-24 13:38:20 +01:00
stoat-release[bot]
a15a542f43 chore(deps): update react monorepo to v19.2.7 (#787)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-06-22 12:26:53 -07:00
stoat-release[bot]
34dffa9425 chore(deps): update rust crate regex to v1.12.4 (#816)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-06-22 12:24:35 -07:00
stoat-release[bot]
a386e84c7d chore(deps): update rust crate aws-config to v1.8.18 (#799)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-06-21 20:23:23 -07:00
stoat-release[bot]
abdba5ce93 chore(deps): update rust crate chrono to v0.4.45 (#815)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-06-21 20:19:14 -07:00
İspik
ffab2369ab feat: add pronouns to user and server members field (#811)
Signed-off-by: İspik <ispik@ispik.dev>
2026-06-21 00:55:23 -07:00
Zomatree
d27917b824 chore: migrate authifier into codebase (#658)
Co-authored-by: izzy <me@insrt.uk>
Signed-off-by: Zomatree <me@zomatree.live>
Signed-off-by: izzy <me@insrt.uk>
2026-06-21 00:50:06 +01:00
Kenyon Hopkins
a7af24b38d fix: channel role permissions fail with 400 InvalidOperation for server owners/admins (#802)
fix: channel role permissions fail with InvalidOperation for owners/admins

Fix: explicitly populate the server reference via
set_server_from_channel() before relying on server_ref().

Signed-off-by: bluecords <133072610+bluecords@users.noreply.github.com>
2026-06-20 11:34:54 -07:00
Kenyon Hopkins
0af376c26b fix: server owner should bypass rank check on channel role-permission overrides (#805)
fix: allow true server owner to bypass rank check on channel role-permission overrides

The set_role_permissions route blocks editing a role's channel permission
overrides whenever role.rank <= the acting member's rank
(NotElevated), to prevent privilege-escalation loops.

However this check has no exemption for the server's true owner
(server.owner == user.id). If an owner has assigned themselves their
own top-level role (e.g. "Admin"/"Server Admin" - extremely common
since most server setups have the owner hold their highest role for
visible status/cosmetics), that role's rank is necessarily equal to
their own computed member rank, so the check incorrectly throws
NotElevated for the owner too - even though server owners by definition
outrank every role and every member, owner-held roles included.

This produces a confusing experience: the owner cannot edit channel-level
overrides for their own top role via the UI, with no clear explanation,
and may reasonably believe something is broken or their permissions are
miscconfigured (they aren't).

This adds a short-circuit: if the acting user is the server's owner,
skip the rank comparison entirely, matching how Stoat already treats
true ownership as an absolute bypass elsewhere in the permission system
(e.g. channel-visibility lockout cascades).

Signed-off-by: bluecords <133072610+bluecords@users.noreply.github.com>
2026-06-20 11:33:28 -07:00
Asraye
aa907e28c3 chore: update email favicons (#800)
Signed-off-by: AsrayeDev <asrayeofficial@gmail.com>
2026-06-20 11:30:27 -07:00
Tom
c70459b10c fix: openapi using old naming (#777)
* fix: openapi using old naming

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: remove january openapi security header

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix(docs): more Revolt usage

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-06-07 16:56:00 -07:00
Asraye
bebfe34922 fix: point docs favicon to correct location (#789)
chore(docs): update favicon

Signed-off-by: Asraye <asrayeofficial@gmail.com>
2026-06-03 11:06:38 -07:00
Tom
5b769b60de chore(docs): update header logo (#796)
* chore(docs): update header logo

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: don't step on toes

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-06-02 17:42:04 -07:00
stoat-tofu[bot]
0896e68882 chore: modify renovate.json 2026-06-01 13:28:29 +00:00
stoat-tofu[bot]
65acc64034 chore: modify .github/workflows/renovate.yml 2026-06-01 13:28:27 +00:00
İspik
bd987bf72a chore: update unicode emoji list (#781)
Signed-off-by: ispik <ispik@ispik.dev>
2026-05-28 14:55:49 -07:00
stoat-release[bot]
7937179db7 chore(main): release 0.13.7 (#770)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-21 19:33:49 +01:00
İspik
2d308e03d5 fix: sanitize emoji input to handle variation selectors (#774)
Signed-off-by: ispik <ispik@ispik.dev>
2026-05-21 08:21:43 -07:00
Paul Makles
b38499a05b ci: hard code packages, gh token limitation [skip ci] (#773) 2026-05-20 20:10:09 +01:00
Paul Makles
4815429952 ci: create Docker images for PR preview (#772) 2026-05-20 19:58:18 +01:00
Zomatree
0d9ae508d9 fix: update mention count badge for channel acks (#769)
Signed-off-by: Zomatree <me@zomatree.live>
2026-05-18 23:38:07 -07:00
stoat-release[bot]
03b52655ff chore(main): release 0.13.6 (#762)
* chore(main): release 0.13.6

* chore: update Cargo.lock

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-18 15:53:42 -07:00
Angelo Kontaxis
5b1985381a chore: switch to lapin (#767)
* chore: begin switching to lapin fully

Signed-off-by: Zomatree <me@zomatree.live>

* chore: update rest of pushd to lapin

Signed-off-by: Zomatree <me@zomatree.live>

* chore: cleanup code

Signed-off-by: Zomatree <me@zomatree.live>

* chore: cleanup code

Signed-off-by: Zomatree <me@zomatree.live>

* fix: github webui sucks

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

---------

Signed-off-by: Zomatree <me@zomatree.live>
Signed-off-by: Tom <iamtomahawkx@gmail.com>
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
Co-authored-by: Tom <iamtomahawkx@gmail.com>
Release-As: 0.13.6
2026-05-18 15:46:17 -07:00
Tom
018afaf38f fix: set env var for publishing crates (#768)
* fix: set env var for publishing crates

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
Release-As: 0.13.6
2026-05-18 15:11:44 -07:00
İspik
af0d8aad14 feat: user slowmode events (#760)
* feat: user slowmode events

Signed-off-by: ispik <ispik@ispik.dev>

* fix: remove debug print statement for slowmodes

Signed-off-by: ispik <ispik@ispik.dev>

* refactor: Send user slowmodes as websocket connects instead of trying to send it in ready payload

Signed-off-by: ispik <ispik@ispik.dev>

* refactor: optimize user slowmode handling with bulk operations

Signed-off-by: ispik <ispik@ispik.dev>

* chore: specify release version

Release-As: 0.13.6

---------

Signed-off-by: ispik <ispik@ispik.dev>
2026-05-18 14:51:43 -07:00
Tom
acbc087982 feat: Update FCM payload for android notifications (#766)
* feat: modify fcm payload to jens will

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: add message id

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: rename field

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: whitespace

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-05-18 10:57:23 -07:00
Tom
2871632382 fix: voice ingress crashing due to new Result in AMQP::new_auto() (#765)
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-05-18 10:56:01 -07:00
Tom
494c8b7cab fix: Use proper headers to determine IP when not behind cloudflare (#764)
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-05-18 10:51:19 -07:00
Paul Makles
26a8692677 ci: ignore test errors on main (#763) 2026-05-17 14:54:57 -05:00
Paul Makles
298742dbad fix: include minio region as tests need it (#761) 2026-05-17 14:41:44 -05:00
stoat-release[bot]
6c920de03a chore(main): release 0.13.5 (#759)
* chore(main): release 0.13.5

* chore: update Cargo.lock

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-17 11:02:21 -07:00
Angelo Kontaxis
c902077cf5 fix: dont panic on hash missing when deleting files (#755)
Signed-off-by: Zomatree <me@zomatree.live>
2026-05-17 10:59:56 -07:00
Tom
19ee535f45 Merge commit from fork
* fix: cache dns & block more ranges

* fix: idle time instead of ttl

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-05-17 10:55:38 -07:00
stoat-release[bot]
ee4575470b chore(main): release 0.13.4 (#754)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-16 18:27:44 +01:00
Paul Makles
6cfee1f601 fix: add TLS feature to livekit-api crate (#753) 2026-05-16 18:23:45 +01:00
stoat-release[bot]
ab9b8ccfca chore(main): release 0.13.3 (#750)
* chore(main): release 0.13.3

* chore: update Cargo.lock

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-15 12:22:27 -07:00
Tom
7647cfc8d9 fix: don't automatically set up rabbitmq in delta (#749)
fix: don't declare queues which seem to cause the backend to crash in prod
for now, these exchanges/queues/bindings will need to be declared manually. Hopefully the lapin rewrite will fix this.

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-05-15 12:17:36 -07:00
stoat-release[bot]
8157e1f6e9 chore(main): release 0.13.2 (#747)
* chore(main): release 0.13.2

* chore: update Cargo.lock

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-11 15:26:03 +01:00
Paul Makles
fcb8091cd7 fix: update default exchange to revolt.default (#746)
Signed-off-by: Paul Makles <me@insrt.uk>
2026-05-11 15:21:34 +01:00
stoat-release[bot]
260036488d chore(main): release 0.13.1 (#745)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-10 15:34:41 +01:00
Tom
1100eaf46f fix: amqprs startup bug (#744) 2026-05-10 15:22:26 +01:00
stoat-release[bot]
d52e84c5d3 chore(main): release 0.13.0 (#722)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-09 17:26:38 +01:00
İspik
d76a71141f fix: don't strip ICC from exif (#735)
* fix: don't strip ICC from exif

Signed-off-by: ispik <ispik@ispik.dev>

* fix: refactor ICC profile handling in image processing

Signed-off-by: ispik <ispik@ispik.dev>

---------

Signed-off-by: ispik <ispik@ispik.dev>
2026-05-08 15:49:31 -07:00
Taureon
6f3441cf4a feat: Add webhook endpoints for editing and deleting messages (#682)
* feat: ErrorType.CannotDeleteMessage, needed later

Signed-off-by: Taureon <taureon@noreply.codeberg.org>

* feat: webhook edit/delete message endpoints

Signed-off-by: Taureon <taureon@noreply.codeberg.org>

* lol, lmao even

Signed-off-by: Taureon <taureon@noreply.codeberg.org>

* fix contradictory comment

Signed-off-by: Taureon <taureon@noreply.codeberg.org>

---------

Signed-off-by: Taureon <taureon@noreply.codeberg.org>
Co-authored-by: Taureon <taureon@noreply.codeberg.org>
2026-05-08 15:37:00 -07:00
Gabriel
23ad135983 feat: add emoji rename endpoint (#714)
* feat: add rename endpoint with rename-only update

Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com>

* fix: enforce detached emoji edit restrictions

Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com>

* fix: always enforce emoji edit permissions

Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com>

---------

Signed-off-by: Gabriel <60961939+gabrielfordevelopment@users.noreply.github.com>
2026-05-08 15:23:57 -07:00
İspik
d46c7f7f3c feat: add embed support for YouTube Shorts (#734)
* feat: add embed support for YouTube Shorts

Signed-off-by: ispik <ispik@ispik.dev>

* feat: blacklist private ip ranges and add january domain blocklist

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: remove duplicates

Signed-off-by: ispik <ispik@ispik.dev>

---------

Signed-off-by: ispik <ispik@ispik.dev>
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
Co-authored-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-05-08 15:23:30 -07:00
Tom
0719985ac5 fix: docker compose file had personal url in it (#742) 2026-05-08 15:22:10 -07:00
Tom
ab5bd47a39 feat: Rewrite acks (#741)
* feat: rewrite ack system

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* feat: rewrite acks to crond + rabbit task

* fix: review changes

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-05-08 14:39:16 -07:00
İspik
9fd7128f80 fix: encode filenames in redirects (#737)
* fix: encode filenames in redirects

Signed-off-by: ispik <ispik@ispik.dev>

* refactor: don't add another dependency when the one needed exists

Signed-off-by: ispik <ispik@ispik.dev>

---------

Signed-off-by: ispik <ispik@ispik.dev>
2026-05-08 14:38:57 -07:00
İspik
df276ac40b chore: update emoji list (#740)
Signed-off-by: ispik <ispik@ispik.dev>
2026-05-08 14:37:35 -07:00
Tom
356491e934 fix: january ip redirects & domain resolver (#738)
* fix: properly block private ip ranges
I'm a dunce and forgot that domains do in fact resolve to ips.

* fix: reimplement max redirects

* fix: remove my debug error

* fix: actually check redirect urls
kind of the whole point of this thing.
2026-05-06 21:33:52 -07:00
İspik
21d82018cf feat: add legal links to root payload (#733)
Signed-off-by: ispik <ispik@ispik.dev>
2026-05-06 16:35:20 -07:00
Tom
6b41db984b feat: blacklist private ip ranges and add january domain blocklist (#731)
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-05-06 16:25:31 -07:00
İspik
5378cd22b4 fix: use correct response for NoEffect errors (#732)
Signed-off-by: ispik <ispik@ispik.dev>
2026-05-05 11:57:42 -07:00
İspik
841985d3b9 feat: add role icon support (#724)
Signed-off-by: ispik <ispik@ispik.dev>
2026-05-02 17:04:28 -07:00
İspik
279f5d5fd7 fix: add new_user_hours to configuration limits (#729)
Signed-off-by: ispik <ispik@ispik.dev>
2026-04-26 22:47:52 -07:00
jarvarvarvis
e93769786c feat: automatically sanitise usernames on create/update (#689)
* feat: automatically sanitise usernames on create/update

Signed-off-by: higgs01 <6546697+higgs01@users.noreply.github.com>

* test: add tests for validation and sanitasion

Signed-off-by: higgs01 <6546697+higgs01@users.noreply.github.com>

* fix: Return only the sanitised string in sanitise_username (#424)

* fix: Use as_str for role.id in insert_role
* Run rustfmt for code changes

Signed-off-by: jarvarvarvis <jarvistrigo@gmail.com>

* feat: Make minimum username length configurable in Revolt.toml (#424)

* fix: Remove redundant call to is_match with blocked username regex

Signed-off-by: jarvarvarvis <jarvistrigo@gmail.com>

* Update crates/core/database/src/models/users/model.rs

Co-authored-by: Tom <iamtomahawkx@gmail.com>
Signed-off-by: jarvarvarvis <53998846+jarvarvarvis@users.noreply.github.com>

* Update crates/core/database/src/models/users/model.rs

Co-authored-by: Tom <iamtomahawkx@gmail.com>
Signed-off-by: jarvarvarvis <53998846+jarvarvarvis@users.noreply.github.com>

* Update crates/core/database/src/models/users/model.rs

Co-authored-by: Tom <iamtomahawkx@gmail.com>
Signed-off-by: jarvarvarvis <53998846+jarvarvarvis@users.noreply.github.com>

* Update crates/core/database/src/models/users/model.rs

Co-authored-by: Tom <iamtomahawkx@gmail.com>
Signed-off-by: jarvarvarvis <53998846+jarvarvarvis@users.noreply.github.com>

* fix: Implement suggested changes and clean up last 4 commits

Signed-off-by: jarvarvarvis <jarvistrigo@gmail.com>

* fix: Disallow stoat as username, update create_user test

Signed-off-by: jarvarvarvis <jarvistrigo@gmail.com>

* fix: Use sanitised username to find updated discriminator

Signed-off-by: jarvarvarvis <jarvistrigo@gmail.com>

* feat: Sanitise revolt.chat in username

Signed-off-by: jarvarvarvis <jarvistrigo@gmail.com>

* fix: Implement discussed changes

Signed-off-by: jarvarvarvis <jarvistrigo@gmail.com>

* test: Fix create_user test

Signed-off-by: jarvarvarvis <jarvistrigo@gmail.com>

* fix: don't overflow the stack
not entirely sure why this fixes it and I don't like it. But work it does.

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: revert odd file mode change

Signed-off-by: jarvarvarvis <jarvistrigo@gmail.com>

---------

Signed-off-by: higgs01 <6546697+higgs01@users.noreply.github.com>
Signed-off-by: jarvarvarvis <jarvistrigo@gmail.com>
Signed-off-by: jarvarvarvis <53998846+jarvarvarvis@users.noreply.github.com>
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
Co-authored-by: higgs01 <6546697+higgs01@users.noreply.github.com>
Co-authored-by: Tom <iamtomahawkx@gmail.com>
2026-04-23 21:02:28 -07:00
İspik
ed4fd5ebfe fix: update message length validation to remove upper limit (#723)
Signed-off-by: ispik <ispik@ispik.dev>
2026-04-23 20:39:13 -07:00
Angelo Kontaxis
89171e9bd0 fix: dont send notification in fcm (#721)
* fix: dont send notification in fcm

Signed-off-by: Zomatree <me@zomatree.live>

* fix: add notification type to fcm

Signed-off-by: Zomatree <me@zomatree.live>

* fix: switch to structured notification data

Signed-off-by: Zomatree <me@zomatree.live>

---------

Signed-off-by: Zomatree <me@zomatree.live>
2026-04-22 23:48:26 -07:00
Infiland
057f2bb8b3 fix: add reconnection policy to Redis subscriber to prevent ghost state (#708)
* fix: add reconnection policy to Redis subscriber to prevent ghost state

- Add ReconnectPolicy::new_exponential(0, 100, 30_000, 2) to the subscriber builder, unlimited retries with exponential backoff (100ms min, 30s max)
- Add on_reconnect handler that signals the listener loop to force a subscription reset, re-subscribing to all topics on the new connection
- Add warn-level logging to on_error for all Redis subscriber errors (previously only Canceled was handled, others were silently ignored)

Signed-off-by: Infiland <ljubica.citydesign@gmail.com>

* Update websocket.rs

Signed-off-by: Infiland <88491175+Infiland@users.noreply.github.com>

* Auto-manage subscriptions on reconnect

Call subscriber.manage_subscriptions() so the subscriber will automatically re-subscribe tracked channels after a Redis reconnect. Remove the manual reconnect channel and on_reconnect handler along with the select branch that forced SubscriptionStateChange::Reset.

Signed-off-by: Infiland <88491175+Infiland@users.noreply.github.com>

---------

Signed-off-by: Infiland <ljubica.citydesign@gmail.com>
Signed-off-by: Infiland <88491175+Infiland@users.noreply.github.com>
2026-04-17 20:15:54 -07:00
Angelo Kontaxis
3675ff1a1f chore: migrate all local dependancies to workspace dependancies (#710)
* chore: start moving all deps to workspace deps

Signed-off-by: Zomatree <me@zomatree.live>

* chore: migrate all deps to workspace deps

Signed-off-by: Zomatree <me@zomatree.live>

* chore: add more dep groups

Signed-off-by: Zomatree <me@zomatree.live>

* fix: add migration to update existing files to be animated (#705)

* fix: add migration to update existing files to be animated

Signed-off-by: Zomatree <me@zomatree.live>

* Revert "fix: add migration to update existing files to be animated"

This reverts commit 4e1f1c116c.

Signed-off-by: Zomatree <me@zomatree.live>

* fix: calculate animated for existing files when fetched

Signed-off-by: Zomatree <me@zomatree.live>

---------

Signed-off-by: Zomatree <me@zomatree.live>

* fix: mise start + missing docker image (#564)

* fix: mise start + missing docker image

Signed-off-by: Damocles078 <hellodamocles078@gmail.com>

* fix: bump livekit version

Signed-off-by: Damocles <106018783+Damocles078@users.noreply.github.com>

---------

Signed-off-by: Damocles078 <hellodamocles078@gmail.com>
Signed-off-by: Tom <iamtomahawkx@gmail.com>
Signed-off-by: Damocles <106018783+Damocles078@users.noreply.github.com>
Co-authored-by: Tom <iamtomahawkx@gmail.com>

* docs: update donation link (#709)

Signed-off-by: Zomatree <me@zomatree.live>

* fix: remove usage of deprecated functions

Signed-off-by: Zomatree <me@zomatree.live>

---------

Signed-off-by: Zomatree <me@zomatree.live>
Signed-off-by: Damocles078 <hellodamocles078@gmail.com>
Signed-off-by: Tom <iamtomahawkx@gmail.com>
Signed-off-by: Damocles <106018783+Damocles078@users.noreply.github.com>
Co-authored-by: Damocles <106018783+Damocles078@users.noreply.github.com>
Co-authored-by: Tom <iamtomahawkx@gmail.com>
Co-authored-by: Paul Makles <me@insrt.uk>
2026-04-17 19:02:18 -07:00
infi
144e939c6b docs: add run in yaak button to endpoints page (#711) 2026-04-12 00:46:05 +01:00
stoat-release[bot]
61fd13629f chore(main): release 0.12.1 (#700)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-11 12:31:11 +01:00
Paul Makles
47336a7940 docs: pr template (#713) 2026-04-10 20:44:29 +01:00
Paul Makles
4ea8f90c0d docs: update donation link (#709) 2026-04-03 00:28:49 +01:00
Damocles
fb8fe16557 fix: mise start + missing docker image (#564)
* fix: mise start + missing docker image

Signed-off-by: Damocles078 <hellodamocles078@gmail.com>

* fix: bump livekit version

Signed-off-by: Damocles <106018783+Damocles078@users.noreply.github.com>

---------

Signed-off-by: Damocles078 <hellodamocles078@gmail.com>
Signed-off-by: Tom <iamtomahawkx@gmail.com>
Signed-off-by: Damocles <106018783+Damocles078@users.noreply.github.com>
Co-authored-by: Tom <iamtomahawkx@gmail.com>
2026-04-01 19:51:14 -07:00
Angelo Kontaxis
f2c056a151 fix: add migration to update existing files to be animated (#705)
* fix: add migration to update existing files to be animated

Signed-off-by: Zomatree <me@zomatree.live>

* Revert "fix: add migration to update existing files to be animated"

This reverts commit 4e1f1c116c.

Signed-off-by: Zomatree <me@zomatree.live>

* fix: calculate animated for existing files when fetched

Signed-off-by: Zomatree <me@zomatree.live>

---------

Signed-off-by: Zomatree <me@zomatree.live>
2026-04-01 19:48:22 -07:00
Tom
f30b729ca9 fix: don't send self dm notifications (#706)
* fix: don't send self dm notifications

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: test failure due to wrong assertion (#707)

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: RA auto import moment

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-03-31 15:25:36 -07:00
Tom
f81e3291bd fix: test failure due to wrong assertion (#707) 2026-03-31 11:06:39 +01:00
Tom
1e80916b65 Fix/release please fix 4 (#703)
* fix: release-please-3

* fix: release please bs v4
2026-03-28 23:05:01 -07:00
Tom
8814bf4c23 fix: release-please-3 (#702) 2026-03-28 22:53:12 -07:00
Tom
5466ada9ae Fix: release please bs 2 electric boogaloo (#701)
* fix: update release (me from this) please

* fix: add dep to release please

* Revert "fix: update release (me from this) please"

This reverts commit 0618c492c7.
2026-03-28 22:42:13 -07:00
Tom
f0e513ccae fix: update release (me from this) please (#699) 2026-03-28 22:35:51 -07:00
stoat-release[bot]
4d4b0dd864 chore(main): release 0.12.0 (#602)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-03-28 22:22:37 -07:00
Tom
f181edc8f2 feat: update livekit (#698) 2026-03-27 21:14:22 -07:00
Angelo Kontaxis
ccda6f5c53 fix: pass &str to Reference (#697)
Signed-off-by: Zomatree <me@zomatree.live>
2026-03-27 18:39:00 -07:00
Damocles
52ed5100c2 fix: improve generated openapi.json (#584)
* fix: /bots/{bot}

Paths "/bots/{bot}" and "/bots/{target}" must not be equivalent.

Signed-off-by: Damocles078 <hellodamocles078@gmail.com>

* fix: /channels/{group_id}/recipients/{member_id}

Paths "/channels/{group_id}/recipients/{member_id}" and "/channels/{target}/recipients/{member}" must not be equivalent.

Signed-off-by: Damocles078 <hellodamocles078@gmail.com>

* fix: /channels/{channel_id}/webhook

Paths "/channels/{target}/webhooks" and "/channels/{channel_id}/webhooks" must not be equivalent.

Signed-off-by: Damocles078 <hellodamocles078@gmail.com>

* fix: /servers/{server_id}/members/{member_id}

Paths "/servers/{target}/members/{member}" and "/servers/{server}/members/{member}" must not be equivalent.

Signed-off-by: Damocles078 <hellodamocles078@gmail.com>

* fix: /custom/emoji/{emoji_id}

Paths "/custom/emoji/{id}" and "/custom/emoji/{emoji_id}" must not be equivalent.

Signed-off-by: Damocles078 <hellodamocles078@gmail.com>

---------

Signed-off-by: Damocles078 <hellodamocles078@gmail.com>
Signed-off-by: Damocles <106018783+Damocles078@users.noreply.github.com>
2026-03-27 17:31:57 -07:00
newt (
c2d4369e16 feat: compute thumbhash for images (#596)
* feat: compute thumbhash for images

Signed-off-by: newt <hi@newty.dev>

* Merge branch 'main' into feat/thumbhash

Signed-off-by: newt <hi@newty.dev>

* style: move comment onto newline

Signed-off-by: newt <hi@newty.dev>

* feat: make thumbhash optional

Signed-off-by: newt <hi@newty.dev>

---------

Signed-off-by: newt <hi@newty.dev>
Signed-off-by: newt (: <hi@newty.dev>
Signed-off-by: Tom <iamtomahawkx@gmail.com>
Co-authored-by: Tom <iamtomahawkx@gmail.com>
2026-03-27 17:29:55 -07:00
Angelo Kontaxis
91783b9066 fix: only show first line on commit messages (#696)
Signed-off-by: Zomatree <me@zomatree.live>
2026-03-27 17:29:37 -07:00
LazyCat
735d644e04 feat: Transfer ownership (#396)
* feat: Transfer ownership

Signed-off-by: LazyCat2 <68156188+LazyCat2@users.noreply.github.com>

* Allow privileged users to change ownership

Signed-off-by: LazyCat2 <68156188+LazyCat2@users.noreply.github.com>

* Require TOTP

Signed-off-by: LazyCat2 <68156188+LazyCat2@users.noreply.github.com>

* Require TOTP or password

Signed-off-by: LazyCat2 <68156188+LazyCat2@users.noreply.github.com>

---------

Signed-off-by: LazyCat2 <68156188+LazyCat2@users.noreply.github.com>
Signed-off-by: Tom <iamtomahawkx@gmail.com>
Co-authored-by: Tom <iamtomahawkx@gmail.com>
2026-03-27 17:24:53 -07:00
Tom
d1e72cee42 feat: parse message push notification content and replace internal formatting (#693)
* parse message push notification content and replace internal formatting

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: don't unwrap the error I was using for db debugging

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: emojis & remove compiler warning

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: emojis in pushd

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* feat: report errors in the render function to sentry

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-03-27 17:15:05 -07:00
İspik
6107f242fd feat: Add slowmode functionality to text channels (#680)
* feat: Add slowmode functionality to text channels

Signed-off-by: arsabutispik <ispik@ispik.dev>

* fix: use atomic check-and-set to prevent spamming with scripts

Signed-off-by: arsabutispik <ispik@ispik.dev>

* feat: Add BypassSlowmode permission to channel permissions

Signed-off-by: arsabutispik <ispik@ispik.dev>

* refactor: Use set_options instead of manually building the command

Signed-off-by: arsabutispik <ispik@ispik.dev>

---------

Signed-off-by: arsabutispik <ispik@ispik.dev>
2026-03-27 17:09:15 -07:00
Tom
a5cd08a655 fix: add flag for disabling events instead of commenting them out (#695)
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-03-27 17:03:48 -07:00
İspik
98c7b1b5a5 feat: implement time based message sweep on user ban (#670)
* feat: implement time based message sweep on user ban
- Adds `delete_message_seconds` (0 to 7 days in seconds) to the ban request payload.

Signed-off-by: arsabutispik <ispik@ispik.dev>

* refactor: pass ulid conversion to database

Signed-off-by: arsabutispik <ispik@ispik.dev>

* fix: use COL constant instead of hardcoded string in error mapper

Signed-off-by: arsabutispik <ispik@ispik.dev>

* refactor: broadcast bulk delete events during ban sweep

Updates the `delete_messages_by_author_since` trait to return a
HashMap of deleted message IDs grouped by channel.

The MongoDB implementation now uses a two-step process: it first
runs a projected `find` query to gather the target `_id` and
`channel` fields, then executes the `delete_many` operation.
This allows the ban route to loop through the affected channels and
dispatch `EventV1::BulkMessageDelete` WebSocket events, ensuring
that the swept messages are instantly removed from the UI for
all connected clients.

Signed-off-by: arsabutispik <ispik@ispik.dev>

* refactor: optimize message deletion by using $group and aggregate

Signed-off-by: arsabutispik <ispik@ispik.dev>

* refactor: use with_type in query

Signed-off-by: arsabutispik <ispik@ispik.dev>

* refactor: abstract function to Message model and mark attachments as deleted

Signed-off-by: arsabutispik <ispik@ispik.dev>

---------

Signed-off-by: arsabutispik <ispik@ispik.dev>
2026-03-27 17:02:12 -07:00
flexxyfluxx
ec22deb2cd docs(readme): remove outdated reference to scripts/install.sh; add instruction to run mise install (#691)
* chore: fill in missing install step: mise install

Signed-off-by: flexxyfluxx <82163962+flexxyfluxx@users.noreply.github.com>

* chore: remove outdated reference to scripts/start.sh

Signed-off-by: flexxyfluxx <82163962+flexxyfluxx@users.noreply.github.com>

---------

Signed-off-by: flexxyfluxx <82163962+flexxyfluxx@users.noreply.github.com>
2026-03-27 13:25:57 -07:00
stoat-tofu[bot]
ecb94e278c chore: modify .github/workflows/validate-pr-title.yml 2026-03-24 19:03:59 +00:00
stoat-tofu[bot]
1a9d5b4b65 chore: modify .github/workflows/release-webhook.yml 2026-03-24 19:03:39 +00:00
stoat-tofu[bot]
abfb3925fd chore: modify .github/workflows/git-town.yml 2026-03-24 19:03:37 +00:00
stoat-tofu[bot]
2a73d82250 chore: modify .github/workflows/git-town.yml 2026-03-24 18:59:15 +00:00
Tom
edfa97db10 feat: Allow restricting server creation to specific users (#685)
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-03-23 12:25:33 -07:00
Tom
cf2cedcee1 chore: silence git town errors (#687)
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-03-23 11:51:22 -07:00
sinus-x
b830631bd2 fix(docs): Update GitHub links (#647)
Updates user-facing links to point to the new organization. Does not
update references in source code (user-agent, dependency references),
as they should be handled separately.

Signed-off-by: sinus-x <14353790+sinus-x@users.noreply.github.com>
2026-03-22 22:18:25 -07:00
Erik LaBine
3fa0abf47f feat: Detect animation in image files for fetch_preview (#574)
* Implement animated metadata TODOs for database and thumbnailing.

Signed-off-by: Assisting <erik@eriklabine.com>

* Run linter for code changes

Signed-off-by: Assisting <erik@eriklabine.com>

---------

Signed-off-by: Assisting <erik@eriklabine.com>
2026-03-22 22:07:22 -07:00
Nico
5191bd16b2 feat: load config from env vars (#576)
Signed-off-by: Nico <nico.macioszek@gmail.com>
2026-03-22 20:33:01 -07:00
Paul Makles
2547fc6cd3 docs: add LLM policy to contrib.md (#683) 2026-03-22 14:02:58 +00:00
Tom
5701b5c18c fix: don't use a bitop for OR (#676)
* fix: uname is missing from crond

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: don't use a bitop
lol, two of us missed that.

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-03-15 09:15:54 -07:00
Tom
dc4438bc3c fix: uname is missing from crond (#675)
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2026-03-15 09:04:37 -07:00
Damocles
f2fc1ee00c docs: documentation generation (#575)
Signed-off-by: Damocles078 <hellodamocles078@gmail.com>
2026-03-14 00:53:51 -07:00
İspik
ab525699bd fix: add masquerade permission to default direct message settings (#665)
Signed-off-by: arsabutispik <ispik@ispik.dev>
2026-03-14 00:41:31 -07:00
Christopher Hultin
d56135e0cb fix: Check for appropriate permission for removing other users avatar (#657)
* Check for appropriate permission for removing other users avatar

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>

* limit check for non-self to just removal

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>

* else if change

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>

---------

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>
2026-03-10 15:38:41 -07:00
Alex
52c0d2f266 fix: send push notifications for DM and group messages (#660)
DMs and group messages never triggered push notifications because
the condition only checked for explicit @mentions. DMs don't use
mentions, so users were never notified of new direct messages.

Signed-off-by: sanasol <mail@sanasol.ws>
2026-03-10 15:15:00 -07:00
Angelo Kontaxis
49c6289580 fix: store server id in redis and in room metadata to be able to delete voice state in all scenarios (#656)
* fix: store server id in redis values and in room metadata to be able to fully delete voice state in all scenarios

* fix: undo logging change
2026-03-10 15:14:32 -07:00
Tom
b0b728fb0d fix: redis_url vs redis_uri in config (#666) 2026-03-10 15:00:06 -07:00
Damocles
d629e89304 fix: replace some links and Revolt mentions to current Stoat (#515) 2026-03-08 01:28:50 +00:00
Tom
24d0d2b726 fix: no node state set on channel creation (#653)
* fix: no node state set on channel creation

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* Update "join_call" logic to handle issue of "user is already connected to a channel that cannot be found"

Fixes stoatchat/for-web#951

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>

* feat: remove node key when channel is empty or deleted

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
Signed-off-by: Chris Hultin <chris.hultin@gmail.com>
Co-authored-by: Chris Hultin <chris.hultin@gmail.com>
2026-03-02 20:45:42 -08:00
Kristian Kjærgård
f777e2863c feat: add bug report template for issue tracking (#627)
Signed-off-by: Bobby <31723128+kris6673@users.noreply.github.com>
2026-03-01 11:16:45 -08:00
Jacob Schlecht
a80ad1cbe5 fix: Fix typo for p256dh in vapid notification flow (#622)
* fix: Fix typo for p256dh in vapid notification flow

This commit was made without the use of generative AI.

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>

* fix: Revert .clone() change as it works fine without that change

This commit was made without the use of generative AI.

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>

---------

Signed-off-by: Jacob Schlecht <dadadah@echoha.us>
2026-03-01 11:06:13 -08:00
Christopher Hultin
0b522ebddc feat: expose global and user limits in root API response (#644)
* feat: expose global and user limits in root API response

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>

* add missing file upload limits

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>

* extract logic to generate "UserLimits" struct from "config"

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>

* convert user limits from feature limits function to impl function

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>

* picking nits

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>

---------

Signed-off-by: Chris Hultin <chris.hultin@gmail.com>
2026-03-01 11:03:49 -08:00
stoat-tofu[bot]
6bd045eb79 chore: modify .github/workflows/release-webhook.yml 2026-02-20 20:45:25 +00:00
Tom
0698e115e8 fix: default video resolution is a non-existent size (#601)
fixes #588
2026-02-18 10:07:35 +00:00
stoat-release[bot]
8e3765df4c chore(main): release 0.11.5 (#598)
* chore(main): release 0.11.5

* chore: update Cargo.lock

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-17 22:14:57 +00:00
Paul Makles
1c98ead695 revert: disable user update events (#593)
* Revert "fix: cut presence traffic too while we engineer a new events architecture (#561)"

This reverts commit 1f8ea963ad.

* Revert "fix: cut events traffic while we engineer a new events architecture (#559)"

This reverts commit a11986ba1a.

* chore: only revert user update
2026-02-17 22:13:17 +00:00
stoat-release[bot]
8849d03cd2 chore(main): release 0.11.4 (#591)
* chore(main): release 0.11.4

* chore: update Cargo.lock

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-16 20:02:01 +00:00
Paul Makles
a75e4eabfc fix: add separate config option for redis events replica url (#590) 2026-02-16 19:37:04 +00:00
stoat-release[bot]
74f0c537e6 chore(main): release 0.11.3 (#562)
* chore(main): release 0.11.3

* chore: update Cargo.lock

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-13 19:50:32 +00:00
Paul Makles
1f8ea963ad fix: cut presence traffic too while we engineer a new events architecture (#561)
Signed-off-by: izzy <me@insrt.uk>
2026-02-13 19:49:23 +00:00
stoat-release[bot]
7c7f4f24de chore(main): release 0.11.2 (#560)
* chore(main): release 0.11.2

* chore: update Cargo.lock

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-13 19:42:13 +00:00
Paul Makles
a11986ba1a fix: cut events traffic while we engineer a new events architecture (#559)
Signed-off-by: izzy <me@insrt.uk>
2026-02-13 19:40:51 +00:00
stoat-release[bot]
2e87696c83 chore(main): release 0.11.1 (#554) 2026-02-13 00:21:29 +00:00
Angelo Kontaxis
94cb916231 fix: bots in multiple voice channel logic (#544) 2026-02-13 00:11:15 +00:00
Paul Makles
95c7dbc793 ci: do not interpolate directly into release workflows (#545) 2026-02-12 16:47:05 +00:00
stoat-release[bot]
ba798490e2 chore(main): release 0.11.0 (#525)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-10 20:48:36 +00:00
Paul Makles
5132270f2e feat: appeal to the almighty Spamhaus (#524) 2026-02-10 20:46:49 +00:00
Tanner Stirrat
4a0957b5ec Update repo readme header (#511)
Update README.md

Signed-off-by: Tanner Stirrat <tstirrat@gmail.com>
2026-02-09 20:51:56 +00:00
stoat-release[bot]
1bfb2a7214 chore(main): release 0.10.3 (#509)
* chore(main): release 0.10.3

* chore: update Cargo.lock

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

---------

Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-09 20:51:30 +00:00
Paul Makles
84483cee7a fix: update Revolt -> Stoat in email titles/desc. (#508) 2026-02-07 16:24:53 +00:00
stoat-release[bot]
4bfd79c42c chore(main): release 0.10.2 (#506)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-25 22:43:56 +00:00
Paul Makles
413aa04dca fix: thumbnailification requires rgb8/rgba8 (#505) 2026-01-25 22:36:31 +00:00
stoat-release[bot]
5d3e65e076 chore(main): release 0.10.1 (#504)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-25 21:27:20 +00:00
Paul Makles
98da8a28a0 fix: use Rust 1.92.0 for Docker build (#503) 2026-01-25 21:03:34 +00:00
stoat-release[bot]
84b57dee57 chore(main): release 0.10.0 (#502)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-25 20:58:16 +00:00
stoat-tofu[bot]
1fef495d5e chore: modify .github/workflows/git-town.yml 2026-01-25 20:50:19 +00:00
stoat-tofu[bot]
310d82819d chore: modify .github/workflows/git-town.yml 2026-01-25 20:44:37 +00:00
stoat-tofu[bot]
5611dab86f chore: modify .github/workflows/git-town.yml 2026-01-25 20:41:30 +00:00
Paul Makles
01ded209c6 feat: repository architecture for files crate w. added tests (#498) 2026-01-25 20:32:22 +00:00
Angelo Kontaxis
a1a21252d0 fix: expose ratelimit headers via cors (#496) 2026-01-25 19:43:06 +00:00
Angelo Kontaxis
0dc5442498 feat: allow kicking members from voice channels (#495) 2026-01-25 19:42:48 +00:00
Paul Makles
9fb55f74dc chore: add git-town to mise tools (#497) 2026-01-24 23:46:10 +00:00
stoat-tofu[bot]
24cfc33907 chore: modify .github/workflows/git-town.yml 2026-01-24 23:44:00 +00:00
stoat-tofu[bot]
ce2a4eccbd chore: modify .github/workflows/git-town.yml 2026-01-24 23:36:32 +00:00
stoat-tofu[bot]
ff68679bad chore: modify .github/workflows/git-town.yml 2026-01-24 23:35:02 +00:00
stoat-tofu[bot]
9bdb7d7feb chore: modify .github/workflows/git-town.yml 2026-01-24 02:20:09 +00:00
stoat-tofu[bot]
3c51fad74d chore: modify .github/workflows/git-town.yml 2026-01-24 02:18:41 +00:00
stoat-tofu[bot]
4344419cdb chore: modify .github/workflows/git-town.yml 2026-01-24 02:10:51 +00:00
stoat-tofu[bot]
65efea020d chore: modify .github/workflows/git-town.yml 2026-01-24 02:01:05 +00:00
stoat-tofu[bot]
b9360b4d88 chore: modify .github/workflows/git-town.yml 2026-01-24 01:54:01 +00:00
stoat-tofu[bot]
12c187a386 chore: modify .github/workflows/git-town.yml 2026-01-24 01:11:53 +00:00
Paul Makles
2846f09c45 chore: switch to mise for tooling & update crates for publishing (#494) 2026-01-21 23:41:58 +00:00
stoat-release[bot]
50835f08c8 chore(main): release 0.9.4 (#491)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: izzy <me@insrt.uk>
2026-01-10 17:55:04 +00:00
stoat-tofu[bot]
c413fbcb58 chore: modify .github/workflows/release-webhook.yml 2026-01-10 16:33:17 +00:00
Paul Makles
c674a9fd4e fix: persist credentials for git repo (#492) 2026-01-10 16:25:29 +00:00
Paul Makles
b2da2a8587 fix: checkout repo. before bumping lock (#490) 2026-01-10 16:22:42 +00:00
stoat-release[bot]
d0ea88f1f8 chore(main): release 0.9.3 (#488)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-01-10 16:04:28 +00:00
Paul Makles
92b466a5f0 chore: switch back to docker hub since we cache it now (#489) 2026-01-10 14:49:51 +00:00
Paul Makles
aeeafebefc fix: pipeline fixes (#487) 2026-01-10 14:46:57 +00:00
stoat-tofu[bot]
bf0048a052 chore: modify .github/workflows/release-webhook.yml 2026-01-10 13:19:24 +00:00
stoat-release[bot]
b4ba02218f chore(main): release 0.9.2 (#486)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-01-10 13:07:06 +00:00
Paul Makles
d13609c372 fix: disable publish for services (#485) 2026-01-10 13:06:33 +00:00
stoat-release[bot]
58fb269450 chore(main): release 0.9.1 (#484)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-01-10 12:59:50 +00:00
Paul Makles
303e52b476 fix(ci): pipeline fixes (marked as fix to force release) (#483) 2026-01-10 12:59:20 +00:00
stoat-release[bot]
868894f500 chore(main): release 0.9.0 (#475)
Co-authored-by: stoat-release[bot] <245062572+stoat-release[bot]@users.noreply.github.com>
2026-01-10 12:51:09 +00:00
Paul Makles
67d49b461f ci: use our arc runner set for Rust builds (#482) 2026-01-10 12:44:26 +00:00
Paul Makles
f89ee5a370 ci: send release notification on create (#481) 2026-01-09 21:05:42 +00:00
Angelo Kontaxis
2afea56e56 feat: add id field to role (#470) 2026-01-09 20:06:16 +00:00
Paul Makles
a5387b64af ci: use dev-dependency for revolt-presence (#480) 2026-01-09 19:21:45 +00:00
Paul Makles
0b178fc791 fix: correct string_cache in lockfile (#479) 2026-01-09 19:10:01 +00:00
Paul Makles
5d27a91e90 fix: correct miniz_oxide in lockfile (#478) 2026-01-09 19:05:49 +00:00
Paul Makles
e2016a2ab4 ci: add the dependency chain into release please (#477) 2026-01-08 23:07:33 +00:00
Paul Makles
924c7f5b18 ci: bootstrap sha (#476) 2026-01-08 22:39:44 +00:00
Paul Makles
22e46194ab ci: switch release please to simple mode (#474) 2026-01-08 22:36:04 +00:00
Paul Makles
ffdb2b6900 ci: switch api server as main package for release please (#473) 2026-01-08 22:24:45 +00:00
Paul Makles
a7b870c397 chore: configure release please (#472) 2026-01-08 22:16:50 +00:00
Simon Marty
b48eb28fc2 docs: remove outdated notice about perf. regression (#467) 2026-01-07 21:12:24 +00:00
Angelo Kontaxis
dc9c82aa4e fix: github webhook incorrect payload and formatting (#468) 2026-01-07 21:11:42 +00:00
Paul Makles
24fedf8c4d fix: respond with 201 if no body in requests (#465)
Signed-off-by: izzy <me@insrt.uk>
2025-11-16 15:24:39 +00:00
Paul Makles
dda20f44ae ci: correct build to ./docs/build (#463) 2025-11-14 22:11:21 +00:00
Paul Makles
0b7c132ace docs: initialise new docs with docusaurus (#460)
* docs: initialise new docs with docusaurus

* chore: sign off
Signed-off-by: izzy <me@insrt.uk>

* chore: lock pnpm version in package.json

Signed-off-by: izzy <me@insrt.uk>

* ci: specify package json to load pnpm version from

Signed-off-by: izzy <me@insrt.uk>

* docs: update repo. lifecycle details

Signed-off-by: izzy <me@insrt.uk>

* docs: add external links to for-web, for-android
chore: add default.nix to docs

* docs: correct headings in contrib

* ci: use cache dependency path

* fix: docusaurus configuration

---------

Signed-off-by: izzy <me@insrt.uk>
2025-11-14 22:06:20 +00:00
Paul Makles
e006cdd051 ci: prevent concurrency on docker/rust actions (#461) 2025-11-14 21:41:22 +00:00
stoat-tofu[bot]
ac2971a78d chore: modify .github/workflows/validate-pr-title.yml 2025-11-14 20:25:09 +00:00
Paul Makles
b7c2ca84ab chore: update branding in transactional emails (#458)
Signed-off-by: izzy <me@insrt.uk>
2025-11-12 22:19:34 +00:00
Paul Makles
82b492e529 ci: normalise workflow triggers (#459)
Signed-off-by: izzy <me@insrt.uk>
2025-11-12 22:17:11 +00:00
Angelo Kontaxis
d567155f12 feat: voice chats v2 (#414)
* initial livekit support

fix up code

undo changes to compose file

add back .env.example

* move voice states to global

* track current state

* more stuff

* fix redis key inconsistancy

* split voice ops into its own library

* feat(livekit): more permission handling

* feat(livekit): push unfinished code

* fix: include voice states in servercreate event
feat: call started system message in dms

* feat: implement missing permission syncs

* fix: remove locked rocket version

* fix: remove local testing values from config

* chore: switch to ids for parameter

* feat: make Channel::server return a reference

* feat: support multiple voice nodes

* fix: expose list of nodes

* fix: respond with the url when joining a voice channel

* feat: being moved between voice channels

* fix: use existing node if someone is already in the voice channel

* refactor: Remove VoiceChannel

* chore: add pushd to debug image script

* refactor: add error messages for Rabbit issues

* docs: add some notes on overrides; fix defaults

* fix: ensure limit is always at least 1 when sent to database

* fix: change permission check for fetching channel webhooks

* chore: mount API at /0.8/ as well

* fix: ensure ratelimiter adjusts for version prefix

* chore: bump version to 0.8.2

* chore: disable LTO because it likely overloads GitHub worker

* fix: don't allow users to time themselves out
closes #376

* fix: match new error type for status code

* feat: init crond crate

* feat: file deletion implementation

* chore: fix version references

* feat: add crond container definitions

* chore: clarify cargo deny / tomls

* feat: add company information to email footers

* chore: bump version to 0.8.3

* fix: don't bump the lockfile version

* fix: include `production` default value for config

* chore: ignore python venv and dev script for user generation

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* test: Add tests for invite/fetch

Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>

* test: add utilities for common setup

Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>

* test: add delete server channel test

Signed-off-by: phazeschift <51278042+phazeschift@users.noreply.github.com>

* feat: allow to set an icon when creating a group

Signed-off-by: Nils Ponsard <nilsponsard@gmail.com>

* fix: allow message pinning and unpinning for DM-like channels

Signed-off-by: Bradlee Barnes <69256931+StupidRepo@users.noreply.github.com>

* fix: remove SavedMessages and Group due to given permissions

Signed-off-by: Bradlee Barnes <69256931+StupidRepo@users.noreply.github.com>

* feat: allow bots to manage emojis (#407)

* Remove bot check on emoji create

Signed-off-by: Builderb <builderbgamer@gmail.com>

* Remove bot check on emoji delete

Signed-off-by: Builderb <builderbgamer@gmail.com>

---------

Signed-off-by: Builderb <builderbgamer@gmail.com>

* feat: add option to send message with missing replies

Signed-off-by: ShaksterNano <54268387+shaksternano@users.noreply.github.com>

* chore: bump version to 0.8.4
chore: enable webhooks in test builds

* ci: downgrade lockfile

* feat: Add Mass Mentions to the backend (#394)

* feat: create base of push daemon

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* Add outbound senders

* Make web_push send to rabbit instead (temp stuff)

* feat: stability and friend requests

* make vapid fr stuff not suck

* swap naming of queue

* move pushd into daemons folder

* fix cargo file for move into daemons folder

* feat: probably working fcm push notifs

* comment out fcm webpush stuff since the config keys dont exist

* fix fcm, name queues according to their prod status and configure routing keys

* add pushd to docker

* mix: Remove old code, add stuff to pushd

* fix: lockfile

* feat: update rocket to 5.0.1

* fix: fix queues and ack bugs

* Move rabbit messsage processing into ack queue

* chore: update readme

* chore: optimizations for ack database hits

* pushd flowchart

* misc: update flowchart

* exit dependancy hell

* add rocket_impl flag to authifier

* make the tests file of delta actually compile

* fix: don't silence every push message

* fix: don't silence all messages

* add debug logging for sending data to rabbit from message events

* validate mentions at a server membership level

* put back that import that was actually important

* minor fix to lockfile

* update delta authifier

* feat: proper permissions for push notifications

* add unit test for mention sanitization

* remove local file dependancy on authifier

* update ports to proper defaults

* fixTM the node bindings

* Theoretically configure docker releases for pushd

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* declare exchange in pushd and delta

* fix createbuckets script

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: reference db implementation

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: remove finally redundant code

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: changes

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: other changes

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: make channel name return result

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* Add role mention parsing

* feat: update to mongo 3.1, add member generator.

* integrate mass mentions into pushd

* patch redis-rs with updated versions

* feat: chunk role mentions

* move permission bits to 37/38 to avoid livekit conflict

* change role mention format to <%id>

* fix the lockfile from merge

* fix: PR change requests

* feat: add tests

* fix: i am a dumbass

* fix: tests, again

---------

Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>

* fix: show full branch name on github webhook messages

* fix: dont leak invisible presence to others

* fix: add back missing early adopter badge

* fix: update branch from main

* fix: update branch from main

* Merge remote-tracking branch 'origin/main' into livekit

* feat: private livekit nodes

* fix: remove node from channel voice state

* fix: get_voice_state has incorrect unique_key and allowed_sources should be lowercase

* chore: bump livekit dep

* feat: ability to force disconnect existing voice connection

* chore: cleanup code

* feat: - track call length
- move voice and video limits to config
- seperate VoiceInformation into model and db model
- fix build scripts

* fix: move call system message to daemon,
check max participants when creating a token to avoid giving tokens but erroring when attempting to join,
check if the channel actually supports voice

* fix: dont set max participants in livekit

* fix: remove VoiceChannel channel type
fix: calculate user overwrites correctly
fix: dont include personal info in livekit user metadata
fix: revoke video permissons on denied publish
fix: add video to default permissions

* fix: update migration

* feat: Send push notifications for dm call start/end

* chore: cleanup

* feat: allow users to specify who is notified when starting a call

* feat: track join time

* merge: branch 'main' into feat/livekit

* fix: include voice-ingress in build

* fix: dont presume channel has a node

* chore: add error logging to internal errors

* chore: add debug logging to voice ingress

* refactor: seperate out disconnect logic

* fix: return correct error if user is already in a voice channel

* fix: dont break on user not still being in channel and force disconnecting

* fix: add Speak and Listen to default permissions

* fix: include channel ids in UserMoveVoiceChannel

* chore: add temp sentry logging system

* fix: handle track_muted and track_unmuted

* fix: dont set notification recipients when empty vec is passed

* fix: only send UserMoveVoiceChannel to the user

* fix: preserve order of replies in message (#447)

Signed-off-by: Aeledfyr <aeledfyr@gmail.com>

* fix: prevent timing out members which have TimeoutMembers permission

* chore: update ci

* fix: temporarily disable call started system messages
See #457

* fix: update code from review
2025-11-12 21:27:04 +00:00
izzy
27ea7345ea ci: update api token; add validate PR title 2025-11-11 20:07:18 +00:00
Zomatree
5b534242c4 chore: update ci 2025-11-08 01:21:24 +00:00
Zomatree
e36fc9738b fix: prevent timing out members which have TimeoutMembers permission 2025-11-06 20:26:31 +00:00
Aeledfyr
657a3f08e5 fix: preserve order of replies in message (#447)
Signed-off-by: Aeledfyr <aeledfyr@gmail.com>
2025-10-27 00:10:07 +00:00
Zomatree
af78ac0586 chore(ci): move to stoatchat 2025-10-11 23:22:41 +01:00
Zomatree
d65c1a1ab3 fix(ci): publish images under stoatchat and remove docker hub 2025-10-11 23:01:35 +01:00
izzy
4fb99e3bd0 ci: also include gifbox src 2025-09-23 12:12:33 -05:00
izzy
f0a83abcfa ci: add missing src copies for Docker build 2025-09-23 12:09:19 -05:00
izzy
6f1c715b8c chore: bump version to 0.8.9 2025-09-23 12:02:35 -05:00
Zomatree
38dd4d1079 fix: swap to using reqwest for query building 2025-09-19 02:50:00 +01:00
Zomatree
154204742d feat: add ratelimits to gifbox 2025-09-19 02:50:00 +01:00
Zomatree
db55998546 docs: document revolt-coalesced 2025-09-19 02:50:00 +01:00
Zomatree
5885e067a6 feat: trending and categories routes 2025-09-19 02:50:00 +01:00
Zomatree
a92152d86d fix: use our own result types instead of tenors types
add user auth
2025-09-19 02:50:00 +01:00
Zomatree
b5cd5e30ef feat: require auth for search 2025-09-19 02:50:00 +01:00
Zomatree
b0c977b324 feat: initial work on tenor gif searching 2025-09-19 02:50:00 +01:00
Zomatree
bfe4018e43 fix: apple music to use original url instead of metadata url
Apple Music returns the url without the query parameter in the opengraph meta tags, causing the regex to not find the track id, meaning any embed links generated would only link to the album.
2025-09-19 02:41:53 +01:00
Zomatree
cc7a7962a8 fix: use trust_cloudflare config value instead of env var 2025-09-18 21:22:31 +01:00
Zomatree
fb4011084d refactor: move ratelimits to a generic system for all web servers 2025-09-18 21:22:31 +01:00
Zomatree
3a3415915f fix: relax settings name regex 2025-09-17 15:41:54 +01:00
Zomatree
964884a5de chore: add policy changes 2025-09-17 15:41:54 +01:00
Zomatree
db57706794 feat: ready payload field customisation 2025-09-17 15:41:54 +01:00
Paul Makles
a186732ca5 merge: pull request #428 from ElfFlu/bugfix/16byte-attachment-gain 2025-09-12 12:48:28 +01:00
Tom
e635bc23ec fix: don't remove timeouts when a member leaves a server (#409) 2025-09-06 18:41:23 -07:00
Zomatree
3cb7da95e3 chore: add missing traits implementations to PermissionValue 2025-09-05 15:22:55 +01:00
Zomatree
dea0f675dd fix: implement Serialize to ClientMessage 2025-09-05 13:07:52 +01:00
Zomatree
480f210ce8 feat: include member payload in ServerMemberJoin event 2025-08-24 06:32:38 +01:00
Zomatree
caa8607468 feat: include groups and dms in fetch mutuals 2025-08-24 03:40:10 +01:00
ElfFlu
32e6600272 fix: remove authentication tag bytes from attachment download
Signed-off-by: ElfFlu <228066338+ElfFlu@users.noreply.github.com>
2025-08-23 19:05:32 +02:00
Zomatree
5335124306 fix: add license to revolt-parser 2025-08-20 18:55:52 +01:00
Zomatree
1561481eb4 feat: make message lexer use unowned string 2025-08-20 18:55:39 +01:00
IAmTomahawkx
8f9015a6ff fix: apply uname fix to january and autumn 2025-08-18 16:59:08 -07:00
Zomatree
ed22b3a5ce chore: switch Reference to using an actual reference 2025-08-12 03:44:29 +01:00
Zomatree
65fbd36624 fix: allow for disabling default features 2025-08-12 02:35:09 +01:00
izzy
050ba16d4a fix: correct shebang for try-tag-and-release 2025-08-07 12:58:52 +02:00
izzy
65bc6c8fc6 chore: add just tool to nix shell 2025-08-07 12:58:15 +02:00
izzy
6ad3da5f35 fix: permit empty remove array in edit requests 2025-08-07 12:57:33 +02:00
izzy
947eb15771 fix: newly created roles should be ranked the lowest 2025-08-07 12:29:42 +02:00
IAmTomahawkx
f4ee35fb09 fix: don't update the same field while trying to remove it
fixes #392
2025-08-06 14:18:40 -07:00
IAmTomahawkx
6048587d34 fix: rename openapi operation ids
fixes #406
2025-08-06 12:28:58 -07:00
Builderb
80cf8fc4e8 Increase limit of embed icon url due to new cdn changes causing longer url (#426)
Signed-off-by: Builderb <builderbgamer@gmail.com>
2025-08-03 11:54:22 -07:00
higgs01
4f54227495 chore: use mc alias set instead of removed mc config (#423) 2025-07-18 09:28:31 +01:00
izzy
aab1734615 chore: bump version to 0.8.8 2025-06-08 11:57:57 +01:00
Paul Makles
40a41ffd64 merge: pull request #418 from revoltchat/feat/role-ranks-v2 2025-06-08 11:46:00 +01:00
izzy
d30ceea373 test: complete test for editing role positions 2025-06-08 11:32:42 +01:00
izzy
3e8a401077 test: begin writing test for role editing 2025-06-08 11:18:47 +01:00
izzy
99f400bc7b fix: logic error in initial data check 2025-06-08 11:18:47 +01:00
izzy
73b576a75f fix: ensure server ID is fanned out with role ranks update
fix: ensure original order is correctly sorted on edit route
2025-06-08 11:18:47 +01:00
izzy
4e4e598daf refactor: minor formatting changes 2025-06-08 11:18:47 +01:00
Zomatree
77daf82b94 chore: refactor new role rankings code 2025-06-08 11:18:47 +01:00
Zomatree
e00603f276 fix: route ranking 2025-06-08 11:18:47 +01:00
Zomatree
1b2c7b2fa1 feat: add roles migration 2025-06-08 11:18:47 +01:00
Zomatree
c526095d4f feat: initial bulk role reorder route 2025-06-08 11:18:47 +01:00
izzy
8cc4bbea4d refactor: clean up clippy warnings 2025-06-07 17:50:11 +01:00
izzy
911ffc767e merge: remote-tracking branch 'origin/feat/store-session-hello-new' 2025-06-07 17:31:15 +01:00
izzy
1690df998d fix: local tests with overrides were missing prerequisites 2025-06-07 17:30:16 +01:00
izzy
519d3c08a8 chore: correct dependency order in justfile 2025-06-07 16:51:09 +01:00
IAmTomahawkx
9846d8aac2 fix(tests): add policy change value to text fixtures 2025-06-06 21:06:18 -07:00
IAmTomahawkx
c74b6255dd don't use local authifier 2025-06-06 20:13:14 -07:00
IAmTomahawkx
df91b8c990 store last login time of session
Signed-off-by: IAmTomahawkx <iamtomahawkx@gmail.com>
2025-06-06 20:10:37 -07:00
izzy
c4728c696d feat: policy changes API
chore: bump version to 0.8.7
2025-05-30 13:12:57 +01:00
IAmTomahawkx
8153f5f17a Don't panic on empty message list
... even though it shouldn't be possible.
2025-05-15 03:20:32 -07:00
izzy
99a80f723d chore: increase verbosity for ack logging 2025-05-14 11:46:56 +01:00
izzy
4f13f5899b fix: authifier should not use transactions for migrations 2025-05-14 11:40:07 +01:00
izzy
5362e84730 fix: don't try to mount env logger twice 2025-05-13 11:16:28 +01:00
izzy
be89e62d54 fix: include pushd key in Revolt.toml 2025-05-13 11:04:43 +01:00
izzy
a72134f4ad chore: bump version to 0.8.6
chore: publish revolt-parser
2025-05-13 10:52:44 +01:00
izzy
69ae767175 refactor: some clippy recommendations 2025-05-13 10:48:56 +01:00
izzy
2aff76c369 refactor(pushd): add non-panic error handling to all queue consumers 2025-05-13 10:41:03 +01:00
izzy
01e0f9e558 fix: add Authifier migration for last_seen 2025-05-12 12:15:23 +01:00
izzy
8ea8f50fb8 chore: bump version to 0.8.5
closes #402
2025-05-10 16:51:38 +01:00
izzy
68bcfcb626 fix: ensure easypwned api is actually set before using 2025-05-10 16:43:30 +01:00
izzy
9632a0dc1b refactor: remove crate bindings/node 2025-05-10 16:40:02 +01:00
izzy
a4a0934475 feat: add use_starttls config option for SMTP 2025-05-10 16:38:26 +01:00
izzy
2061794305 docs: configure reset existing email template 2025-05-10 16:36:41 +01:00
izzy
3a7ebad883 merge: branch 'izzy/patch/easypwned'
fix: configure new authifier properties
2025-05-10 16:31:37 +01:00
izzy
6daa8223b2 ci: must login to ghcr before pushing base image 2025-05-09 23:10:35 +01:00
izzy
4a58d6eae0 chore: enable lto for production builds (#341) 2025-05-09 23:04:37 +01:00
510 changed files with 160592 additions and 9219 deletions

147
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@@ -0,0 +1,147 @@
name: Bug Report
description: Report a bug in Stoat — please fill out every required field to help us triage quickly.
title: "[Bug]: "
labels:
- bug
- unconfirmed
body:
# ── Thank you section ────────────────────────────────────────────────────────
- type: markdown
attributes:
value: |
**Thank you for taking the time to report a bug for Stoat!**
Please follow the instructions below and provide as much detail as possible to help us understand and reproduce the issue.
# ── Pre-flight checks ────────────────────────────────────────────────────────
- type: checkboxes
id: preflight
attributes:
label: Pre-flight checklist
description: Please confirm all of the following before submitting.
options:
- label: I am on the latest available version of Stoat.
required: true
- label: I searched existing issues and did not find a duplicate.
required: true
- label: This is a bug report, not a support question or feature request.
required: true
# ── Bug description ──────────────────────────────────────────────────────────
- type: textarea
id: description
attributes:
label: Bug description
description: >
What happened? What did you expect to happen instead?
Keep this brief — detailed steps go in the next field.
placeholder: "Example: When I open a DM thread, new messages from the other person do not appear unless I reload the page."
validations:
required: true
# ── Steps to reproduce ───────────────────────────────────────────────────────
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: >
Provide a numbered list of every step needed to trigger the bug.
The clearer this is, the faster we can fix it.
placeholder: |
1. Log in to Stoat on the web client.
2. Open a direct message conversation.
3. Ask the other person to send a message.
4. Observe that the message does not appear without reloading.
validations:
required: true
# ── Expected behavior ────────────────────────────────────────────────────────
- type: textarea
id: expected
attributes:
label: Expected behavior
description: What should have happened?
placeholder: "New messages should appear in real time without a page reload."
validations:
required: true
# ── Client / platform ────────────────────────────────────────────────────────
- type: dropdown
id: platform
attributes:
label: Client / platform
description: Which Stoat client are you using?
options:
- Web (browser)
- Stoat for Desktop
- Android
- iOS
- Self-hosted server
validations:
required: true
# ── Client version ───────────────────────────────────────────────────────────
- type: input
id: version
attributes:
label: Client version
description: >
Find this in **Settings**. For the web client, include the build
number shown in the footer or About page.
placeholder: "e.g. Stoat for Desktop 1.3.0 or 0.2.1 (2025-10-10)"
validations:
required: true
# ── OS / browser details ─────────────────────────────────────────────────────
- type: input
id: os
attributes:
label: OS / browser details
description: Your operating system and, for web bugs, your browser and its version.
placeholder: "e.g. Windows 11, Chrome 121 or macOS 14.3, Stoat for Desktop 1.3.0 or Android 14"
validations:
required: true
# ── Reproducibility ──────────────────────────────────────────────────────────
- type: dropdown
id: reproducibility
attributes:
label: Reproducibility
description: How consistently does this bug occur?
options:
- Always
- Usually
- Rarely
- Only once
- Unknown
validations:
required: true
# ── Screenshots or video ─────────────────────────────────────────────────────
- type: textarea
id: screenshots
attributes:
label: Screenshots or video
description: Paste images or drag-and-drop a screen recording here. GitHub accepts common image and video formats.
validations:
required: false
# ── Additional context ───────────────────────────────────────────────────────
- type: textarea
id: additional
attributes:
label: Additional context
description: >
Anything else that might help: workarounds you found, links to related issues,
self-hosted configuration details (Redis, MongoDB, LiveKit, SMTP), network environment, etc.
validations:
required: false
# ── Closing note ─────────────────────────────────────────────────────────────
- type: markdown
attributes:
value: |
---
A maintainer will review your report. Please watch for follow-up questions — issues
that go unanswered are harder to resolve and may be closed after 30 days of inactivity.

24
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,24 @@
<!-- Describe your changes -->
Fixes # (issue)
## How was this PR tested?
<!-- What did you do to test your changes? -->
- [ ] Test A
- [ ] Test B
## Checklist:
- [ ] I have carefully read [the contributing guidelines](https://developers.stoat.chat/developing/contrib/)
- [ ] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation if applicable
- [ ] I have no unrelated changes in the PR
- [ ] I have confirmed that any new dependencies are strictly necessary
- [ ] I have written tests for new code (if applicable)
- [ ] I have followed naming conventions/patterns in the surrounding code
## Please declare, if any, LLM usage involved in creating this PR
...

View File

@@ -1,37 +0,0 @@
name: Build documentation
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: write # To push a branch
pages: write # To push to a GitHub Pages site
id-token: write # To update the deployment status
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install latest mdbook
run: |
tag=$(curl 'https://api.github.com/repos/rust-lang/mdbook/releases/latest' | jq -r '.tag_name')
url="https://github.com/rust-lang/mdbook/releases/download/${tag}/mdbook-${tag}-x86_64-unknown-linux-gnu.tar.gz"
mkdir mdbook
curl -sSL $url | tar -xz --directory=./mdbook
echo `pwd`/mdbook >> $GITHUB_PATH
- name: Build Book
run: |
cd doc
mdbook build
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "doc/book"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

46
.github/workflows/docker-cleanup.yaml vendored Normal file
View File

@@ -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

View File

@@ -5,20 +5,21 @@ on:
tags:
- "*"
pull_request:
branches:
- "main"
paths:
- "Dockerfile"
workflow_dispatch:
permissions:
contents: read
packages: write
concurrency:
group: docker-test-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
base:
name: Test base image build
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
name: Test base image build (fork)
runs-on: arc-runner-set
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork
steps:
# Configure build environment
- name: Checkout
@@ -38,8 +39,8 @@ jobs:
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max
publish:
runs-on: self-hosted
if: github.event_name != 'pull_request'
runs-on: arc-runner-set
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }}
name: Publish Docker images
steps:
# Configure build environment
@@ -48,23 +49,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Build the image
- name: Build base image
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/${{ github.repository_owner }}/base:latest
# Authenticate with Docker Hub and GHCR
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
uses: docker/login-action@v2
with:
@@ -72,14 +57,33 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# revoltchat/server
- 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
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
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
id: meta-delta
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/server
ghcr.io/revoltchat/server
ghcr.io/stoatchat/api
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -89,17 +93,16 @@ 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 }}
# revoltchat/bonfire
# stoatchat/events
- name: Docker meta
id: meta-bonfire
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/bonfire
ghcr.io/revoltchat/bonfire
ghcr.io/stoatchat/events
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -109,17 +112,16 @@ 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 }}
# revoltchat/autumn
# stoatchat/file-server
- name: Docker meta
id: meta-autumn
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/autumn
ghcr.io/revoltchat/autumn
ghcr.io/stoatchat/file-server
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -129,17 +131,16 @@ 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 }}
# revoltchat/january
# stoatchat/proxy
- name: Docker meta
id: meta-january
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/january
ghcr.io/revoltchat/january
ghcr.io/stoatchat/proxy
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -149,17 +150,35 @@ 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 }}
# revoltchat/crond
# stoatchat/gifbox
- name: Docker meta
id: meta-gifbox
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/gifbox
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/services/gifbox/Dockerfile
tags: ${{ steps.meta-gifbox.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
labels: ${{ steps.meta-gifbox.outputs.labels }}
# stoatchat/crond
- name: Docker meta
id: meta-crond
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/crond
ghcr.io/revoltchat/crond
ghcr.io/stoatchat/crond
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -169,17 +188,16 @@ 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 }}
# revoltchat/pushd
# stoatchat/pushd
- name: Docker meta
id: meta-pushd
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/pushd
ghcr.io/revoltchat/pushd
ghcr.io/stoatchat/pushd
- name: Publish
uses: docker/build-push-action@v4
with:
@@ -189,5 +207,24 @@ 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
- name: Docker meta
id: meta-voice-ingress
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/stoatchat/voice-ingress
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/daemons/voice-ingress/Dockerfile
tags: ${{ steps.meta-voice-ingress.outputs.tags }}
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/base:${{ steps.base.outputs.tag }}
labels: ${{ steps.meta-voice-ingress.outputs.labels }}

23
.github/workflows/docs-test.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Documentation (test)
on:
pull_request:
jobs:
test-deploy:
name: Test deployment
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- run: mise docs:build

48
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Documentation
on:
push:
branches:
- main
jobs:
build:
name: Build Docusaurus
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- run: mise docs:build
- name: Upload Build Artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./docs/build
deploy:
name: Deploy to GitHub Pages
needs: build
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

23
.github/workflows/git-town.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
# DO NOT EDIT DIRECTLY IN REPOSITORY
# Managed in Terraform templates
name: Git Town
on:
pull_request:
jobs:
git-town:
name: Display the branch stack
runs-on: ubuntu-slim
if: ${{ github.event.pull_request.head.repo.full_name == github.repository && !startsWith(github.head_ref, 'release-please--') }}
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: stoatchat/action-git-town@4bc5c942e4603bffa0806b51d5fe5f0bc5deb0ac
continue-on-error: true

26
.github/workflows/publish-crates.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: Publish Crates
on:
workflow_dispatch:
release:
types: [published]
jobs:
publish:
name: Publish Crates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: mise publish --workspace

63
.github/workflows/release-please.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Release Please
on:
push:
branches: [main] # updates/opens the release PR when commits land on main
workflow_dispatch:
permissions:
contents: write
pull-requests: write
id-token: write
concurrency:
group: release-please
cancel-in-progress: true
jobs:
release-please:
name: Release Please
runs-on: ubuntu-latest
outputs:
release_created: ${{ steps.rp.outputs.release_created }}
tag_name: ${{ steps.rp.outputs.tag_name }}
steps:
- id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_STOAT_RELEASE_APP_ID }}
private-key: ${{ secrets.GH_STOAT_RELEASE_APP_PRIVATE_KEY }}
- id: rp
uses: googleapis/release-please-action@v4
with:
token: ${{ steps.app-token.outputs.token }}
config-file: release-please-config.json
- name: Install latest stable
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
token: ${{ steps.app-token.outputs.token }}
- name: Update Cargo.lock
if: ${{ steps.rp.outputs.prs_created == 'true' || steps.rp.outputs.prs_updated == 'true' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
RP_PR_OUTPUT_JSON: ${{ steps.rp.outputs.prs }}
run: |
PR_NUMBER=$(echo "$RP_PR_OUTPUT_JSON" | jq -r '.[0].number')
gh pr checkout "$PR_NUMBER"
cargo update -w
if git diff --quiet Cargo.lock; then
echo "No changes to Cargo.lock"
else
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Cargo.lock
git commit -s -m "chore: update Cargo.lock"
git push
fi

26
.github/workflows/release-webhook.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
# DO NOT EDIT DIRECTLY IN REPOSITORY
# Managed in Terraform templates
name: Release Webhook
on:
workflow_dispatch:
release:
types: [published]
jobs:
release-webhook:
name: Send Release Webhook
runs-on: ubuntu-latest
steps:
- name: Send release notification webhook
env:
TAG_NAME: ${{ github.event.release.tag_name }}
REPOSITORY: ${{ github.repository }}
WEBHOOK_URL: ${{ secrets.STOAT_WEBHOOK_UPDATES_URL }}
run: |
RELEASE_URL="https://github.com/${REPOSITORY}/releases/tag/${TAG_NAME}"
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"content\": \"$RELEASE_URL\"}"

30
.github/workflows/renovate.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
# DO NOT EDIT DIRECTLY IN REPOSITORY
# Managed in Terraform templates
name: Renovate
on:
workflow_dispatch:
schedule:
- cron: '0/15 * * * *'
jobs:
renovate:
runs-on: ubuntu-latest
steps:
- id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.GH_STOAT_RELEASE_APP_ID }}
private-key: ${{ secrets.GH_STOAT_RELEASE_APP_PRIVATE_KEY }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.app-token.outputs.token }}
- name: Self-hosted Renovate
uses: renovatebot/github-action@v46.1.14
with:
token: '${{ steps.app-token.outputs.token }}'
env:
RENOVATE_PLATFORM_COMMIT: 'enabled'
RENOVATE_REPOSITORIES: '${{ github.repository }}'

View File

@@ -2,16 +2,12 @@ name: Rust build, test, and generate specification
on:
push:
paths-ignore:
- ".github/**"
- "!.github/workflows/rust.yaml"
- ".vscode/**"
- "doc/**"
- ".gitignore"
- "LICENSE"
- "README"
pull_request:
branches: [main]
pull_request:
concurrency:
group: rust-build-test-${{ github.head_ref || github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
@@ -19,63 +15,60 @@ env:
jobs:
check:
name: Rust project
runs-on: ubuntu-latest
runs-on: arc-runner-set
steps:
- uses: actions/checkout@v2
- name: Install latest stable
uses: actions-rs/toolchain@v1
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Install cargo-nextest
uses: baptiste0928/cargo-install@v1
persist-credentials: false
# Using our own runners for now:
# - name: Free up disk space
# run: |
# sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@cd24790a7f5f6439ac32cc94f5523cb2de8bfa8c # use-mise-action-v1.1.0
with:
crate: cargo-nextest
locked: true
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Run cargo build
uses: actions-rs/cargo@v1
with:
command: build
- name: Run services in background
run: |
docker compose -f compose.yml up -d
- name: Run cargo test
- run: mise build
- run: mise docker:start
- name: Reference Test
env:
TEST_DB: REFERENCE
continue-on-error: ${{ github.ref_name == 'main' }}
run: |
cargo nextest run
mise test
- name: Run cargo test (with MongoDB)
- name: MongoDB Test
env:
TEST_DB: MONGODB
MONGODB: mongodb://localhost
continue-on-error: ${{ github.ref_name == 'main' }}
run: |
cargo nextest run
mise test
- name: Start API in background
if: github.event_name != 'pull_request' && github.ref_name == 'main'
env:
TEST_DB: REFERENCE
run: |
cargo build --bin revolt-delta && (cargo run --bin revolt-delta &)
mise build --bin revolt-delta && (mise service:api &)
- name: Wait for API to go up
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: nev7n/wait_for_response@v1
uses: nev7n/wait_for_response@7fef3c1a6e8939d0b09062f14fec50d3c5d15fa1 # v1.0.1
with:
url: "http://localhost:14702/"
- name: Checkout API repository
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: actions/checkout@v3
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
repository: revoltchat/api
repository: stoatchat/javascript-client-api
path: api
token: ${{ secrets.PAT }}
ssh-key: ${{ secrets.DEPLOY_KEY_JAVASCRIPT_CLIENT_API }}
- name: Download OpenAPI specification
if: github.event_name != 'pull_request' && github.ref_name == 'main'
@@ -83,10 +76,10 @@ jobs:
- name: Commit changes
if: github.event_name != 'pull_request' && github.ref_name == 'main'
uses: EndBug/add-and-commit@v4
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
with:
cwd: "api"
add: "*.json"
author_name: Revolt CI
author_email: revolt-ci@users.noreply.github.com
author_name: Stoat CI
author_email: stoat-ci@users.noreply.github.com
message: "chore: generate OpenAPI specification"

View File

@@ -1,54 +0,0 @@
name: Add Issue to Board
on:
issues:
types: [opened]
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectV2(number: 3) {
id
fields(first:20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'TODO_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="Todo") |.id' project_data.json) >> $GITHUB_ENV
- name: Add issue to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $issue:ID!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) {
item {
id
}
}
}' -f project=$PROJECT_ID -f issue=$ISSUE_ID --jq '.data.addProjectV2ItemById.item.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV

View File

@@ -1,79 +0,0 @@
name: Add PR to Board
on:
pull_request_target:
types: [opened, synchronize, ready_for_review, review_requested]
jobs:
track_pr:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
query {
organization(login: "revoltchat"){
projectV2(number: 5) {
id
fields(first:20) {
nodes {
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}
}
}
}
}
}
}' > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo 'INCOMING_OPTION_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name=="🆕 Untriaged") |.id' project_data.json) >> $GITHUB_ENV
- name: Add PR to project
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
PR_ID: ${{ github.event.pull_request.node_id }}
run: |
item_id="$( gh api graphql -f query='
mutation($project:ID!, $pr:ID!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) {
item {
id
}
}
}' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Set fields
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
gh api graphql -f query='
mutation (
$project: ID!
$item: ID!
$status_field: ID!
$status_value: String!
) {
set_status: updateProjectV2ItemFieldValue(input: {
projectId: $project
itemId: $item
fieldId: $status_field
value: {
singleSelectOptionId: $status_value
}
}) {
projectV2Item {
id
}
}
}' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=${{ env.INCOMING_OPTION_ID }} --silent

23
.github/workflows/validate-pr-title.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
# DO NOT EDIT DIRECTLY IN REPOSITORY
# Managed in Terraform templates
name: "Lint PR"
on:
pull_request_target:
types:
- opened
- reopened
- edited
- synchronize
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -11,4 +11,5 @@ venv/
.vercel
.DS_Store
livekit.yml
.idea

28
.mise/config.toml Normal file
View File

@@ -0,0 +1,28 @@
[tools]
node = "25.4.0"
pnpm = "10.28.1"
gh = "2.25.0"
rust = "1.92.0"
"cargo:cargo-nextest" = "0.9.122"
"github:git-town/git-town" = "22.7.1"
[settings]
experimental = true
idiomatic_version_file_enable_tools = ["rust"]
[tasks.start]
description = "Run all services"
depends = ["docker:start", "build"]
wait_for = ["docker:start", "build"]
run = [{ task = "service:*" }]
[env]
BUILDER = "cargo"
DOCKER_NETWORK_NAME = "stoatchat_default"
DATABASE_PORT = "27017"
RABBIT_PORT = "5672"
REDIS_PORT = "6379"
_.file = { path = ".env", tools = true }

5
.mise/tasks/build Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Build project"
set -e
${BUILDER} build "$@"

5
.mise/tasks/check Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Check project with clippy"
set -e
cargo clippy

13
.mise/tasks/docker/start Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
#MISE description="Start Docker containers"
set -e
docker compose up -d
docker run \
--network=${DOCKER_NETWORK_NAME} \
--name wait \
--rm dokku/wait -c \
rabbit:${RABBIT_PORT},\
database:${DATABASE_PORT},\
redis:${REDIS_PORT}

5
.mise/tasks/docker/stop Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Stop Docker containers"
set -e
docker compose down

7
.mise/tasks/docs/_default Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
#MISE description="Start the Stoat Developers website"
#MISE depends=["docs:install"]
#MISE dir="{{config_root}}/docs"
set -e
pnpm build

7
.mise/tasks/docs/build Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
#MISE description="Build the Stoat Developers website"
#MISE depends=["docs:install"]
#MISE dir="{{config_root}}/docs"
set -e
pnpm build

6
.mise/tasks/docs/install Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
#MISE description="Install dependencies for docs site"
#MISE dir="{{config_root}}/docs"
set -e
pnpm i --frozen-lockfile

5
.mise/tasks/publish Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Publish project"
set -e
cargo publish "$@"

5
.mise/tasks/service/api Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Run API server"
set -e
cargo run --bin revolt-delta

5
.mise/tasks/service/crond Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Run cron daemon"
set -e
cargo run --bin revolt-crond

5
.mise/tasks/service/events Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Run events server"
set -e
cargo run --bin revolt-bonfire

5
.mise/tasks/service/files Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Run file server"
set -e
cargo run --bin revolt-autumn

5
.mise/tasks/service/gifbox Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Run GIF proxy server"
set -e
cargo run --bin revolt-gifbox

5
.mise/tasks/service/proxy Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Run proxy server"
set -e
cargo run --bin revolt-january

5
.mise/tasks/service/pushd Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
#MISE description="Run push daemon"
set -e
cargo run --bin revolt-pushd

8
.mise/tasks/test Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
#MISE description="Test project"
set -e
: "${TEST_DB:=REFERENCE}"
export TEST_DB
cargo nextest run

View File

@@ -0,0 +1,3 @@
{
".": "0.13.7"
}

View File

@@ -1,6 +1,6 @@
{
"editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy",
"rust-analyzer.check.command": "clippy",
"nixEnvSelector.suggestion": false,
"nixEnvSelector.nixFile": "${workspaceFolder}/default.nix"
}
}

288
CHANGELOG.md Normal file
View File

@@ -0,0 +1,288 @@
# 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)
### Bug Fixes
* add migration to update existing files to be animated ([#705](https://github.com/stoatchat/stoatchat/issues/705)) ([f2c056a](https://github.com/stoatchat/stoatchat/commit/f2c056a1515be493b195f3f5db5886c2ddf36700))
* don't send self dm notifications ([#706](https://github.com/stoatchat/stoatchat/issues/706)) ([f30b729](https://github.com/stoatchat/stoatchat/commit/f30b729ca90d0be6853c57ab4935694e5e59ae56))
* mise start + missing docker image ([#564](https://github.com/stoatchat/stoatchat/issues/564)) ([fb8fe16](https://github.com/stoatchat/stoatchat/commit/fb8fe1655776791421284a6a093e86f0320c258a))
* test failure due to wrong assertion ([#707](https://github.com/stoatchat/stoatchat/issues/707)) ([f81e329](https://github.com/stoatchat/stoatchat/commit/f81e3291bdd57af9ceedb2987b111acc7051d69c))
## [0.12.0](https://github.com/stoatchat/stoatchat/compare/v0.11.5...v0.12.0) (2026-03-28)
### Features
* add bug report template for issue tracking ([#627](https://github.com/stoatchat/stoatchat/issues/627)) ([f777e28](https://github.com/stoatchat/stoatchat/commit/f777e2863c6ca50057c8b5d0a5be14915d287724))
* Add slowmode functionality to text channels ([#680](https://github.com/stoatchat/stoatchat/issues/680)) ([6107f24](https://github.com/stoatchat/stoatchat/commit/6107f242fd3ebaff71a15f9a16330ffbcb4f2d7b))
* Allow restricting server creation to specific users ([#685](https://github.com/stoatchat/stoatchat/issues/685)) ([edfa97d](https://github.com/stoatchat/stoatchat/commit/edfa97db108c9c81828547f98a1db5315cb5ba4a))
* compute thumbhash for images ([#596](https://github.com/stoatchat/stoatchat/issues/596)) ([c2d4369](https://github.com/stoatchat/stoatchat/commit/c2d4369e160f32d79bce0a0b0f14677f89de3669))
* Detect animation in image files for fetch_preview ([#574](https://github.com/stoatchat/stoatchat/issues/574)) ([3fa0abf](https://github.com/stoatchat/stoatchat/commit/3fa0abf47f5f42ddd8ee041fe4c44fbc5ba800c1))
* expose global and user limits in root API response ([#644](https://github.com/stoatchat/stoatchat/issues/644)) ([0b522eb](https://github.com/stoatchat/stoatchat/commit/0b522ebddc17f2e3f792ff5e2347793e9849fa23))
* implement time based message sweep on user ban ([#670](https://github.com/stoatchat/stoatchat/issues/670)) ([98c7b1b](https://github.com/stoatchat/stoatchat/commit/98c7b1b5a5b9fdac5c0ab83be10f0e23114dbfc9))
* load config from env vars ([#576](https://github.com/stoatchat/stoatchat/issues/576)) ([5191bd1](https://github.com/stoatchat/stoatchat/commit/5191bd16b2a905b8409838e34eb0baca96f08580))
* parse message push notification content and replace internal formatting ([#693](https://github.com/stoatchat/stoatchat/issues/693)) ([d1e72ce](https://github.com/stoatchat/stoatchat/commit/d1e72cee42c54e16f4e49af569897528b10a28ca))
* Transfer ownership ([#396](https://github.com/stoatchat/stoatchat/issues/396)) ([735d644](https://github.com/stoatchat/stoatchat/commit/735d644e043793cb86e74aab5b88bb4b8bc17ba2))
* update livekit ([#698](https://github.com/stoatchat/stoatchat/issues/698)) ([f181edc](https://github.com/stoatchat/stoatchat/commit/f181edc8f2ff3ce4b6d48938dfc73931ecfa2279))
### Bug Fixes
* add flag for disabling events instead of commenting them out ([#695](https://github.com/stoatchat/stoatchat/issues/695)) ([a5cd08a](https://github.com/stoatchat/stoatchat/commit/a5cd08a655dece4269f3ac84fa2387ae356709a5))
* add masquerade permission to default direct message settings ([#665](https://github.com/stoatchat/stoatchat/issues/665)) ([ab52569](https://github.com/stoatchat/stoatchat/commit/ab525699bd6663333f0e9fed6d2455e482e6a09f))
* Check for appropriate permission for removing other users avatar ([#657](https://github.com/stoatchat/stoatchat/issues/657)) ([d56135e](https://github.com/stoatchat/stoatchat/commit/d56135e0cbc713884c9378832952f7ad490fa315))
* default video resolution is a non-existent size ([#601](https://github.com/stoatchat/stoatchat/issues/601)) ([0698e11](https://github.com/stoatchat/stoatchat/commit/0698e115e8d003d615e468c4fb9654e6bbc9107f)), closes [#588](https://github.com/stoatchat/stoatchat/issues/588)
* **docs:** Update GitHub links ([#647](https://github.com/stoatchat/stoatchat/issues/647)) ([b830631](https://github.com/stoatchat/stoatchat/commit/b830631bd25a546844b7bdd30386084bb365e4de))
* don't use a bitop for OR ([#676](https://github.com/stoatchat/stoatchat/issues/676)) ([5701b5c](https://github.com/stoatchat/stoatchat/commit/5701b5c18c513f796af365169ceaea372a22638c))
* Fix typo for p256dh in vapid notification flow ([#622](https://github.com/stoatchat/stoatchat/issues/622)) ([a80ad1c](https://github.com/stoatchat/stoatchat/commit/a80ad1cbe58b8af5e45751e51d94d93c1cea1c9f))
* improve generated openapi.json ([#584](https://github.com/stoatchat/stoatchat/issues/584)) ([52ed510](https://github.com/stoatchat/stoatchat/commit/52ed5100c2446e0b261085639e123e7e124cab2c))
* no node state set on channel creation ([#653](https://github.com/stoatchat/stoatchat/issues/653)) ([24d0d2b](https://github.com/stoatchat/stoatchat/commit/24d0d2b7266f6f8a692d0a52704acfecf517674c))
* only show first line on commit messages ([#696](https://github.com/stoatchat/stoatchat/issues/696)) ([91783b9](https://github.com/stoatchat/stoatchat/commit/91783b906697fc85305dee683f7c15dda55f0c50))
* pass &str to Reference ([#697](https://github.com/stoatchat/stoatchat/issues/697)) ([ccda6f5](https://github.com/stoatchat/stoatchat/commit/ccda6f5c53ee043705f7ff6b5f6c393f020781de))
* redis_url vs redis_uri in config ([#666](https://github.com/stoatchat/stoatchat/issues/666)) ([b0b728f](https://github.com/stoatchat/stoatchat/commit/b0b728fb0dbc9ee28360301de1c3ea501bbbff1d))
* replace some links and Revolt mentions to current Stoat ([#515](https://github.com/stoatchat/stoatchat/issues/515)) ([d629e89](https://github.com/stoatchat/stoatchat/commit/d629e89304be2f0011e189293b278f07d346aa7d))
* send push notifications for DM and group messages ([#660](https://github.com/stoatchat/stoatchat/issues/660)) ([52c0d2f](https://github.com/stoatchat/stoatchat/commit/52c0d2f266b76d8975bba2d5e75c62bb30149c45))
* store server id in redis and in room metadata to be able to delete voice state in all scenarios ([#656](https://github.com/stoatchat/stoatchat/issues/656)) ([49c6289](https://github.com/stoatchat/stoatchat/commit/49c628958070e4f0a5edc764d3b48158589219d9))
* uname is missing from crond ([#675](https://github.com/stoatchat/stoatchat/issues/675)) ([dc4438b](https://github.com/stoatchat/stoatchat/commit/dc4438bc3c7b2cad8d442b3cd438afb9ed566a5e))
## [0.11.5](https://github.com/stoatchat/stoatchat/compare/v0.11.4...v0.11.5) (2026-02-17)
### Reverts
* disable user update events ([#593](https://github.com/stoatchat/stoatchat/issues/593)) ([1c98ead](https://github.com/stoatchat/stoatchat/commit/1c98ead69579b4700be0b51c9020bb8402336cc6))
## [0.11.4](https://github.com/stoatchat/stoatchat/compare/v0.11.3...v0.11.4) (2026-02-16)
### Bug Fixes
* add separate config option for redis events replica url ([#590](https://github.com/stoatchat/stoatchat/issues/590)) ([a75e4ea](https://github.com/stoatchat/stoatchat/commit/a75e4eabfc4b34aba7620c82ba77558a32d9e10a))
## [0.11.3](https://github.com/stoatchat/stoatchat/compare/v0.11.2...v0.11.3) (2026-02-13)
### Bug Fixes
* cut presence traffic too while we engineer a new events architecture ([#561](https://github.com/stoatchat/stoatchat/issues/561)) ([1f8ea96](https://github.com/stoatchat/stoatchat/commit/1f8ea963ad742f693f405e6438f1c343c81e6579))
## [0.11.2](https://github.com/stoatchat/stoatchat/compare/v0.11.1...v0.11.2) (2026-02-13)
### Bug Fixes
* cut events traffic while we engineer a new events architecture ([#559](https://github.com/stoatchat/stoatchat/issues/559)) ([a11986b](https://github.com/stoatchat/stoatchat/commit/a11986ba1ad16b672ff1080913a684567d88adbb))
## [0.11.1](https://github.com/stoatchat/stoatchat/compare/v0.11.0...v0.11.1) (2026-02-13)
### Bug Fixes
* bots in multiple voice channel logic ([#544](https://github.com/stoatchat/stoatchat/issues/544)) ([94cb916](https://github.com/stoatchat/stoatchat/commit/94cb916231b9b8befb2e94065917ff40815bec52))
## [0.11.0](https://github.com/stoatchat/stoatchat/compare/v0.10.3...v0.11.0) (2026-02-10)
### Features
* appeal to the almighty Spamhaus ([#524](https://github.com/stoatchat/stoatchat/issues/524)) ([5132270](https://github.com/stoatchat/stoatchat/commit/5132270f2edd6df25ce414daa42ed1b2aa6fa7a9))
## [0.10.3](https://github.com/stoatchat/stoatchat/compare/v0.10.2...v0.10.3) (2026-02-07)
### Bug Fixes
* update `Revolt` -&gt; `Stoat` in email titles/desc. ([#508](https://github.com/stoatchat/stoatchat/issues/508)) ([84483ce](https://github.com/stoatchat/stoatchat/commit/84483cee7af3e5dfa16f7fe13e334c4d9f5abd60))
## [0.10.2](https://github.com/stoatchat/stoatchat/compare/v0.10.1...v0.10.2) (2026-01-25)
### Bug Fixes
* thumbnailification requires rgb8/rgba8 ([#505](https://github.com/stoatchat/stoatchat/issues/505)) ([413aa04](https://github.com/stoatchat/stoatchat/commit/413aa04dcaf8bff3935ed1e5f31432e11a03ce6f))
## [0.10.1](https://github.com/stoatchat/stoatchat/compare/v0.10.0...v0.10.1) (2026-01-25)
### Bug Fixes
* use Rust 1.92.0 for Docker build ([#503](https://github.com/stoatchat/stoatchat/issues/503)) ([98da8a2](https://github.com/stoatchat/stoatchat/commit/98da8a28a0aa2fee4e8eee1d86bd7c49e3187477))
## [0.10.0](https://github.com/stoatchat/stoatchat/compare/v0.9.4...v0.10.0) (2026-01-25)
### Features
* allow kicking members from voice channels ([#495](https://github.com/stoatchat/stoatchat/issues/495)) ([0dc5442](https://github.com/stoatchat/stoatchat/commit/0dc544249825a49c793309edee5ec1838458a6da))
* repository architecture for files crate w. added tests ([#498](https://github.com/stoatchat/stoatchat/issues/498)) ([01ded20](https://github.com/stoatchat/stoatchat/commit/01ded209c62208fc906d6aab9b08c04e860e10ef))
### Bug Fixes
* expose ratelimit headers via cors ([#496](https://github.com/stoatchat/stoatchat/issues/496)) ([a1a2125](https://github.com/stoatchat/stoatchat/commit/a1a21252d0ad58937e41f16e5fb86f96bebd2a51))
## [0.9.4](https://github.com/stoatchat/stoatchat/compare/v0.9.3...v0.9.4) (2026-01-10)
### Bug Fixes
* checkout repo. before bumping lock ([#490](https://github.com/stoatchat/stoatchat/issues/490)) ([b2da2a8](https://github.com/stoatchat/stoatchat/commit/b2da2a858787853be43136fd526a0bd72baf78ef))
* persist credentials for git repo ([#492](https://github.com/stoatchat/stoatchat/issues/492)) ([c674a9f](https://github.com/stoatchat/stoatchat/commit/c674a9fd4e0abbd51569870e4b38074d4a1de03c))
## [0.9.3](https://github.com/stoatchat/stoatchat/compare/v0.9.2...v0.9.3) (2026-01-10)
### Bug Fixes
* pipeline fixes ([#487](https://github.com/stoatchat/stoatchat/issues/487)) ([aeeafeb](https://github.com/stoatchat/stoatchat/commit/aeeafebefc36a43a656cf797c9251ca50292733c))
## [0.9.2](https://github.com/stoatchat/stoatchat/compare/v0.9.1...v0.9.2) (2026-01-10)
### Bug Fixes
* disable publish for services ([#485](https://github.com/stoatchat/stoatchat/issues/485)) ([d13609c](https://github.com/stoatchat/stoatchat/commit/d13609c37279d6a40445dcd99564e5c3dd03bac1))
## [0.9.1](https://github.com/stoatchat/stoatchat/compare/v0.9.0...v0.9.1) (2026-01-10)
### Bug Fixes
* **ci:** pipeline fixes (marked as fix to force release) ([#483](https://github.com/stoatchat/stoatchat/issues/483)) ([303e52b](https://github.com/stoatchat/stoatchat/commit/303e52b476585eea81c33837f1b01506ce387684))
## [0.9.0](https://github.com/stoatchat/stoatchat/compare/v0.8.8...v0.9.0) (2026-01-10)
### Features
* add id field to role ([#470](https://github.com/stoatchat/stoatchat/issues/470)) ([2afea56](https://github.com/stoatchat/stoatchat/commit/2afea56e56017f02de98e67316b4457568ad5b26))
* add ratelimits to gifbox ([1542047](https://github.com/stoatchat/stoatchat/commit/154204742d21cbeff6e2577b00f50b495ea44631))
* include groups and dms in fetch mutuals ([caa8607](https://github.com/stoatchat/stoatchat/commit/caa86074680d46223cebc20f41e9c91c41ec825d))
* include member payload in ServerMemberJoin event ([480f210](https://github.com/stoatchat/stoatchat/commit/480f210ce85271e13d1dac58a5dae08de108579d))
* initial work on tenor gif searching ([b0c977b](https://github.com/stoatchat/stoatchat/commit/b0c977b324b8144c1152589546eb8fec5954c3e7))
* make message lexer use unowned string ([1561481](https://github.com/stoatchat/stoatchat/commit/1561481eb4cdc0f385fbf0a81e4950408050e11f))
* ready payload field customisation ([db57706](https://github.com/stoatchat/stoatchat/commit/db577067948f13e830b5fb773034e9713a1abaff))
* require auth for search ([b5cd5e3](https://github.com/stoatchat/stoatchat/commit/b5cd5e30ef7d5e56e8964fb7c543965fa6bf5a4a))
* trending and categories routes ([5885e06](https://github.com/stoatchat/stoatchat/commit/5885e067a627b8fff1c8ce2bf9e852ff8cf3f07a))
* voice chats v2 ([#414](https://github.com/stoatchat/stoatchat/issues/414)) ([d567155](https://github.com/stoatchat/stoatchat/commit/d567155f124e4da74115b1a8f810062f7c6559d9))
### Bug Fixes
* add license to revolt-parser ([5335124](https://github.com/stoatchat/stoatchat/commit/53351243064cac8d499dd74284be73928fa78a43))
* allow for disabling default features ([65fbd36](https://github.com/stoatchat/stoatchat/commit/65fbd3662462aed1333b79e59155fa6377e83fcc))
* apple music to use original url instead of metadata url ([bfe4018](https://github.com/stoatchat/stoatchat/commit/bfe4018e436a4075bae780dd4d35a9b58315e12f))
* apply uname fix to january and autumn ([8f9015a](https://github.com/stoatchat/stoatchat/commit/8f9015a6ff181d208d9269ab8691bd417d39811a))
* **ci:** publish images under stoatchat and remove docker hub ([d65c1a1](https://github.com/stoatchat/stoatchat/commit/d65c1a1ab3bdc7e5684b03f280af77d881661a3d))
* correct miniz_oxide in lockfile ([#478](https://github.com/stoatchat/stoatchat/issues/478)) ([5d27a91](https://github.com/stoatchat/stoatchat/commit/5d27a91e901dd2ea3e860aeaed8468db6c5f3214))
* correct shebang for try-tag-and-release ([050ba16](https://github.com/stoatchat/stoatchat/commit/050ba16d4adad5d0fb247867aa3e94e3d42bd12d))
* correct string_cache in lockfile ([#479](https://github.com/stoatchat/stoatchat/issues/479)) ([0b178fc](https://github.com/stoatchat/stoatchat/commit/0b178fc791583064bf9ca94b1d39b42d021e1d79))
* don't remove timeouts when a member leaves a server ([#409](https://github.com/stoatchat/stoatchat/issues/409)) ([e635bc2](https://github.com/stoatchat/stoatchat/commit/e635bc23ec857d648d5705e1a3875d7bc3402b0d))
* don't update the same field while trying to remove it ([f4ee35f](https://github.com/stoatchat/stoatchat/commit/f4ee35fb093ca49f0a64ff4b17fd61587df28145)), closes [#392](https://github.com/stoatchat/stoatchat/issues/392)
* github webhook incorrect payload and formatting ([#468](https://github.com/stoatchat/stoatchat/issues/468)) ([dc9c82a](https://github.com/stoatchat/stoatchat/commit/dc9c82aa4e9667ea6639256c65ac8de37a24d1f7))
* implement Serialize to ClientMessage ([dea0f67](https://github.com/stoatchat/stoatchat/commit/dea0f675dde7a63c7a59b38d469f878b7a8a3af4))
* newly created roles should be ranked the lowest ([947eb15](https://github.com/stoatchat/stoatchat/commit/947eb15771ed6785b3dcd16c354c03ded5e4cbe0))
* permit empty `remove` array in edit requests ([6ad3da5](https://github.com/stoatchat/stoatchat/commit/6ad3da5f35f989a2e7d8e29718b98374248e76af))
* preserve order of replies in message ([#447](https://github.com/stoatchat/stoatchat/issues/447)) ([657a3f0](https://github.com/stoatchat/stoatchat/commit/657a3f08e5d652814bbf0647e089ed9ebb139bbf))
* prevent timing out members which have TimeoutMembers permission ([e36fc97](https://github.com/stoatchat/stoatchat/commit/e36fc9738bac0de4f3fcbccba521f1e3754f7ae7))
* relax settings name regex ([3a34159](https://github.com/stoatchat/stoatchat/commit/3a3415915f0d0fdce1499d47a2b7fa097f5946ea))
* remove authentication tag bytes from attachment download ([32e6600](https://github.com/stoatchat/stoatchat/commit/32e6600272b885c595c094f0bc69459250220dcb))
* rename openapi operation ids ([6048587](https://github.com/stoatchat/stoatchat/commit/6048587d348fbca0dc3a9b47690c56df8fece576)), closes [#406](https://github.com/stoatchat/stoatchat/issues/406)
* respond with 201 if no body in requests ([#465](https://github.com/stoatchat/stoatchat/issues/465)) ([24fedf8](https://github.com/stoatchat/stoatchat/commit/24fedf8c4d9cd3160bdec97aa451520f8beaa739))
* swap to using reqwest for query building ([38dd4d1](https://github.com/stoatchat/stoatchat/commit/38dd4d10797b3e6e397fc219e818f379bdff19f2))
* use `trust_cloudflare` config value instead of env var ([cc7a796](https://github.com/stoatchat/stoatchat/commit/cc7a7962a882e1627fcd0bc75858a017415e8cfc))
* use our own result types instead of tenors types ([a92152d](https://github.com/stoatchat/stoatchat/commit/a92152d86da136997817e797c7af8e38731cdde8))

6549
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,205 @@
[workspace]
resolver = "2"
resolver = "3"
members = [
"crates/delta",
"crates/bonfire",
"crates/core/*",
"crates/services/*",
"crates/bindings/*",
"crates/daemons/*",
]
[patch.crates-io]
redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" }
#authifier = { package = "authifier", version = "1.0.10", path = "../authifier/crates/authifier" }
#rocket_authifier = { package = "rocket_authifier", version = "1.0.10", path = "../authifier/crates/rocket_authifier" }
# I'm 99% sure this is overloading the GitHub worker
# hence builds have been failing since, let's just
# disable it for now. In the future, we could use this
# if we were rolling our own CI.
# [profile.release]
# lto = true
[profile.release]
lto = true
[workspace.dependencies]
# Async
async-trait = "0.1.89"
tokio = "1.49.0"
async-channel = "2.3.1"
futures = "0.3.32"
async-tungstenite = "0.17.0"
futures-locks = "0.7.1"
async-lock = "2.8.0"
async-recursion = "1.0.4"
tokio-util = { version = "0.7.18" }
# Error Handling
anyhow = "1.0.100"
thiserror = "2.0.18"
sentry = "0.31.5"
sentry-anyhow = "0.38.1"
# Data Validation
regex = "1.12.3"
validator = "0.16"
# Data Types
uuid = "1.19.0"
ulid = "1.2.1"
nanoid = "0.4.0"
typenum = "1.17.0"
num_enum = "0.6.1"
bitfield = "0.13.2"
# Time
chrono = "0.4.15"
iso8601-timestamp = "0.2.10"
# Data Collections
lru = "0.16.3"
indexmap = "2.13.1"
dashmap = "5.2.0"
moka = "0.12.8"
lru_time_cache = "0.11.11"
deadqueue = "0.2.4"
# Web scraping
scraper = "0.20.0"
encoding_rs = "0.8.34"
# Mail
lettre = "0.10.0-alpha.4"
handlebars = "4.3.0"
# HTTP Requests
reqwest = "0.13.2"
isahc = "1.7"
# Notifications
fcm_v1 = "0.3.0"
web-push = "0.10.0"
revolt_a2 = "0.10"
# Parsing
logos = "0.15"
# SVG rendering
usvg = "0.44.0"
resvg = "0.44.0"
tiny-skia = "0.11.4"
# Logging
log = "0.4.29"
pretty_env_logger = "0.4.0"
# Redis
redis-kiss = { version = "0.1.4", default-features = false }
fred = "8.0.1"
# Serialisation
bincode = "1.3.3"
serde_json = "1.0.79"
rmp-serde = "1.0.0"
serde = { version = "1", features = ["derive"] }
strum_macros = "0.26.4"
# MongoDB
bson = { version = "2.1.0" }
mongodb = { version = "3.1.0" }
# S3
aws-config = "1.5.5"
aws-sdk-s3 = "1.46.0"
# Axum (HTTP server)
axum-macros = "0.4.1"
axum_typed_multipart = "0.12.1"
axum = "0.7.5"
axum-extra = "0.9"
tower-http = "0.5.2"
# Rocket (HTTP server)
rocket = "0.5.1"
rocket_empty = "0.1.1"
revolt_rocket_okapi = "0.10.0"
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" }
rocket_authifier = "1.0.16"
rocket_prometheus = "0.10.0-rc.3"
# Spec Generation
utoipa = "4.2.3"
revolt_okapi = "0.9.1"
schemars = "0.8.8"
utoipa-scalar = "0.1.0"
# Image Processing
jxl-oxide = "0.12.5"
sha2 = "0.10.8"
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"
simdutf8 = "0.1.4"
# Content type processing
infer = "0.16.0"
ffprobe = "0.4.0"
imagesize = "0.13.0"
# OpenTelemetry
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.22", features = [
"env-filter",
] } # consider https://crates.io/crates/better-tracing
opentelemetry = { version = "0.31.0", features = ["logs"] }
opentelemetry_sdk = { version = "0.31.0", features = ["logs"] }
opentelemetry-otlp = { version = "0.31.0", features = ["logs"] }
opentelemetry-appender-tracing = "0.31.1"
# RabbitMQ
lapin = "4.7.1"
# Voice
livekit-api = "=0.4.23"
livekit-protocol = "=0.7.7"
livekit-runtime = "0.4.0"
# Other Utilities
once_cell = "1.9.0"
config = "0.13.3"
cached = "0.44.0"
rand = "0.8.5"
base64 = "0.21.3"
decancer = "3.3.3"
linkify = "0.8.1"
url-escape = "0.1.1"
revolt_optional_struct = "0.2.0"
unicode-segmentation = "1.10.1"
querystring = "1.1.0"
tempfile = "3.12.0"
aes-gcm = "0.10.3"
auto_ops = "0.3.0"
url = "2.2.2"
impl_ops = "0.1.1"
lazy_static = "1.5.0"
mime = "0.3.17"
totp-lite = "2.0.0"
rust-argon2 = "1.0.0"
base32 = "0.4.0"
sha1 = "0.10.6"
futures-lite = "2.6.1"
# Build Dependencies
vergen = "7.5.0"
# Local packages
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" }

View File

@@ -1,5 +1,5 @@
# Build Stage
FROM --platform="${BUILDPLATFORM}" rust:1.86.0-slim-bookworm
FROM --platform="${BUILDPLATFORM}" rust:1.92.0-slim-bookworm
USER 0:0
WORKDIR /home/rust/src
@@ -17,7 +17,6 @@ RUN sh /tmp/build-image-layer.sh tools
# Build all dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/
@@ -28,10 +27,14 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
COPY crates/daemons/voice-ingress/Cargo.toml ./crates/daemons/voice-ingress/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps

View File

@@ -1,5 +1,5 @@
# Build Stage
FROM rust:1.86.0-slim-bookworm
FROM rust:1.92.0-slim-bookworm
USER 0:0
WORKDIR /home/rust/src
@@ -13,7 +13,6 @@ COPY scripts/build-image-layer.sh /tmp/
# Build all dependencies
COPY Cargo.toml Cargo.lock ./
COPY crates/bindings/node/Cargo.toml ./crates/bindings/node/
COPY crates/bonfire/Cargo.toml ./crates/bonfire/
COPY crates/delta/Cargo.toml ./crates/delta/
COPY crates/core/config/Cargo.toml ./crates/core/config/
@@ -24,10 +23,14 @@ COPY crates/core/parser/Cargo.toml ./crates/core/parser/
COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/core/coalesced/Cargo.toml ./crates/core/coalesced/
COPY crates/core/ratelimits/Cargo.toml ./crates/core/ratelimits/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
COPY crates/services/gifbox/Cargo.toml ./crates/services/gifbox/
COPY crates/daemons/crond/Cargo.toml ./crates/daemons/crond/
COPY crates/daemons/pushd/Cargo.toml ./crates/daemons/pushd/
COPY crates/daemons/voice-ingress/Cargo.toml ./crates/daemons/voice-ingress/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps

131
README.md
View File

@@ -1,33 +1,34 @@
<div align="center">
<h1>
Revolt Backend
Stoat Backend
[![Stars](https://img.shields.io/github/stars/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/stargazers)
[![Forks](https://img.shields.io/github/forks/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/network/members)
[![Pull Requests](https://img.shields.io/github/issues-pr/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/pulls)
[![Issues](https://img.shields.io/github/issues/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/issues)
[![Contributors](https://img.shields.io/github/contributors/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/graphs/contributors)
[![License](https://img.shields.io/github/license/revoltchat/backend?style=flat-square&logoColor=white)](https://github.com/revoltchat/backend/blob/main/LICENSE)
[![Stars](https://img.shields.io/github/stars/stoatchat/stoatchat?style=flat-square&logoColor=white)](https://github.com/stoatchat/stoatchat/stargazers)
[![Forks](https://img.shields.io/github/forks/stoatchat/stoatchat?style=flat-square&logoColor=white)](https://github.com/stoatchat/stoatchat/network/members)
[![Pull Requests](https://img.shields.io/github/issues-pr/stoatchat/stoatchat?style=flat-square&logoColor=white)](https://github.com/stoatchat/stoatchat/pulls)
[![Issues](https://img.shields.io/github/issues/stoatchat/stoatchat?style=flat-square&logoColor=white)](https://github.com/stoatchat/stoatchat/issues)
[![Contributors](https://img.shields.io/github/contributors/stoatchat/stoatchat?style=flat-square&logoColor=white)](https://github.com/stoatchat/stoatchat/graphs/contributors)
[![License](https://img.shields.io/github/license/stoatchat/stoatchat?style=flat-square&logoColor=white)](https://github.com/stoatchat/stoatchat/blob/main/LICENSE)
</h1>
The services and libraries that power the Revolt service.<br/>
The services and libraries that power the Stoat service.<br/>
<br/>
| Crate | Path | Description | |
| ------------------ | -------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration | ![Crates.io Version](https://img.shields.io/crates/v/revolt-config) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-config) ![Crates.io Version](https://img.shields.io/crates/size/revolt-config) ![Crates.io License](https://img.shields.io/crates/l/revolt-config) |
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation | ![Crates.io Version](https://img.shields.io/crates/v/revolt-database) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-database) ![Crates.io Version](https://img.shields.io/crates/size/revolt-database) ![Crates.io License](https://img.shields.io/crates/l/revolt-database) |
| `core/files` | [crates/core/files](crates/core/files) | Core: S3 and encryption subroutines | ![Crates.io Version](https://img.shields.io/crates/v/revolt-files) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-files) ![Crates.io Version](https://img.shields.io/crates/size/revolt-files) ![Crates.io License](https://img.shields.io/crates/l/revolt-files) |
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models | ![Crates.io Version](https://img.shields.io/crates/v/revolt-models) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-models) ![Crates.io Version](https://img.shields.io/crates/size/revolt-models) ![Crates.io License](https://img.shields.io/crates/l/revolt-models) |
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) |
| `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bindings/node` | [crates/bindings/node](crates/bindings/node) | Node.js bindings for the Revolt software | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| Crate | Path | Description | |
| ------------------ | -------------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core/config` | [crates/core/config](crates/core/config) | Core: Configuration | ![Crates.io Version](https://img.shields.io/crates/v/revolt-config) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-config) ![Crates.io Version](https://img.shields.io/crates/size/revolt-config) ![Crates.io License](https://img.shields.io/crates/l/revolt-config) |
| `core/database` | [crates/core/database](crates/core/database) | Core: Database Implementation | ![Crates.io Version](https://img.shields.io/crates/v/revolt-database) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-database) ![Crates.io Version](https://img.shields.io/crates/size/revolt-database) ![Crates.io License](https://img.shields.io/crates/l/revolt-database) |
| `core/files` | [crates/core/files](crates/core/files) | Core: S3 and encryption subroutines | ![Crates.io Version](https://img.shields.io/crates/v/revolt-files) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-files) ![Crates.io Version](https://img.shields.io/crates/size/revolt-files) ![Crates.io License](https://img.shields.io/crates/l/revolt-files) |
| `core/models` | [crates/core/models](crates/core/models) | Core: API Models | ![Crates.io Version](https://img.shields.io/crates/v/revolt-models) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-models) ![Crates.io Version](https://img.shields.io/crates/size/revolt-models) ![Crates.io License](https://img.shields.io/crates/l/revolt-models) |
| `core/permissions` | [crates/core/permissions](crates/core/permissions) | Core: Permission Logic | ![Crates.io Version](https://img.shields.io/crates/v/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-permissions) ![Crates.io Version](https://img.shields.io/crates/size/revolt-permissions) ![Crates.io License](https://img.shields.io/crates/l/revolt-permissions) |
| `core/presence` | [crates/core/presence](crates/core/presence) | Core: User Presence | ![Crates.io Version](https://img.shields.io/crates/v/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-presence) ![Crates.io Version](https://img.shields.io/crates/size/revolt-presence) ![Crates.io License](https://img.shields.io/crates/l/revolt-presence) |
| `core/result` | [crates/core/result](crates/core/result) | Core: Result and Error types | ![Crates.io Version](https://img.shields.io/crates/v/revolt-result) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-result) ![Crates.io Version](https://img.shields.io/crates/size/revolt-result) ![Crates.io License](https://img.shields.io/crates/l/revolt-result) |
| `core/coalesced` | [crates/core/coalesced](crates/core/coalesced) | Core: Coalescion service | ![Crates.io Version](https://img.shields.io/crates/v/revolt-coalesced) ![Crates.io Version](https://img.shields.io/crates/msrv/revolt-coalesced) ![Crates.io Version](https://img.shields.io/crates/size/revolt-coalesced) ![Crates.io License](https://img.shields.io/crates/l/revolt-coalesced) |
| `delta` | [crates/delta](crates/delta) | REST API server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `bonfire` | [crates/bonfire](crates/bonfire) | WebSocket events server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/january` | [crates/services/january](crates/services/january) | Proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/gifbox` | [crates/services/gifbox](crates/services/gifbox) | Tenor proxy server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `services/autumn` | [crates/services/autumn](crates/services/autumn) | File server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/crond` | [crates/daemons/crond](crates/daemons/crond) | Timed data clean up daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
| `daemons/pushd` | [crates/daemons/pushd](crates/daemons/pushd) | Push notification daemon server | ![License](https://img.shields.io/badge/license-AGPL--3.0--or--later-blue) |
</div>
<br/>
@@ -36,22 +37,19 @@ The services and libraries that power the Revolt service.<br/>
Rust 1.86.0 or higher.
> [!CAUTION]
> The events server has a significant performance regression between Rust 1.77.2 and 1.78.0 onwards, see [issue #341](https://github.com/revoltchat/backend/issues/341). This is currently solved by build time options but we are looking for a proper fix.
## Development Guide
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.stoat.chat/developing/contrib/) and the [technical documentation for this project](https://developers.stoat.chat/).
Before getting started, you'll want to install:
- Rust toolchain (rustup recommended)
- mise
- Docker
- Git
- mold (optional, faster compilation)
> A **default.nix** is available for Nix users!
> Just run `nix-shell` and continue.
> Run `nix-shell` to activate mise.
As a heads-up, the development environment uses the following ports:
@@ -67,15 +65,25 @@ As a heads-up, the development environment uses the following ports:
| `crates/bonfire` | 14703 |
| `crates/services/autumn` | 14704 |
| `crates/services/january` | 14705 |
| `crates/services/gifbox` | 14706 |
Now you can clone and build the project:
```bash
git clone https://github.com/revoltchat/backend revolt-backend
cd revolt-backend
cargo build
git clone https://github.com/stoatchat/stoatchat stoat-backend
cd stoat-backend
mise install
mise build
```
> [!TIP]
> You can override `BUILDER` in your `.env` file to run cargo with mold if you installed it:
>
> ```bash
> # .env
> BUILDER = "mold --run cargo"
> ```
A default configuration `Revolt.toml` is present in this project that is suited for development.
If you'd like to change anything, create a `Revolt.overrides.toml` file and specify relevant variables.
@@ -112,10 +120,11 @@ If you'd like to change anything, create a `Revolt.overrides.toml` file and spec
> - "14672:15672"
> ```
>
> And corresponding Revolt configuration:
> With the corresponding Revolt configuration:
>
> ```toml
> # Revolt.overrides.toml
> # Revolt.overrides.toml
> # and Revolt.test-overrides.toml
> [database]
> mongodb = "mongodb://127.0.0.1:14017"
> redis = "redis://127.0.0.1:14079/"
@@ -123,51 +132,41 @@ If you'd like to change anything, create a `Revolt.overrides.toml` file and spec
> [rabbit]
> port = 14072
> ```
>
> And mise configuration
>
> ```bash
> #.env
> DATABASE_PORT = "14017"
> RABBIT_PORT = "14072"
> REDIS_PORT = "14079"
> ```
Then continue:
```bash
# start other necessary services
docker compose up -d
cp livekit.example.yml livekit.yml
# run everything together
./scripts/start.sh
# .. or individually
# run the API server
cargo run --bin revolt-delta
# run the events server
cargo run --bin revolt-bonfire
# run the file server
cargo run --bin revolt-autumn
# run the proxy server
cargo run --bin revolt-january
# run the push daemon (not usually needed in regular development)
cargo run --bin revolt-pushd
# hint:
# mold -run <cargo build, cargo run, etc...>
# mold -run ./scripts/start.sh
mise start
```
You can start a web client by doing the following:
You can start a web client by doing the following in another terminal:
```bash
# if you do not have yarn yet and have a modern Node.js:
corepack enable
# clone the web client and run it:
git clone --recursive https://github.com/revoltchat/revite
cd revite
yarn
yarn build:deps
echo "VITE_API_URL=http://local.revolt.chat:14702" > .env.local
yarn dev --port 14701
git clone --recursive https://github.com/stoatchat/for-web stoat-web
cd stoat-web
# refer to stoat-web/README.md for startup, creating an account and loging in
```
Then go to http://local.revolt.chat:14701 to create an account/login.
When signing up, go to http://localhost:14080 to find confirmation/password reset emails.
To stop all services, hit (CTRL + c) in the terminal you ran `mise start` and run `mise docker:stop`
## Deployment Guide
### Cutting new crate releases
@@ -196,14 +195,14 @@ Tag and push a new release by running:
just release
```
If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/revoltchat/backend/releases/new) to create a changelog.
If you have bumped the crate versions, proceed to [GitHub releases](https://github.com/stoatchat/stoatchat/releases/new) to create a changelog.
## Testing
First, start the required services:
```sh
docker compose -f docker-compose.db.yml up -d
docker compose up -d
```
Now run tests for whichever database:
@@ -215,6 +214,6 @@ TEST_DB=MONGODB cargo nextest run
## License
The Revolt backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/revoltchat/backend/blob/master/LICENSE).
The Stoat backend is generally licensed under the [GNU Affero General Public License v3.0](https://github.com/stoatchat/stoatchat/blob/main/LICENSE).
**Individual crates may supply their own licenses!**

View File

@@ -15,7 +15,7 @@ host = "127.0.0.1"
[hosts]
# Web locations of various services
# Defaults assume all services are reverse-proxied
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
# See https://github.com/stoatchat/self-hosted/blob/main/Caddyfile
#
# Remember to change these to https/wss where appropriate in production!
app = "http://local.revolt.chat:14701"
@@ -26,6 +26,11 @@ january = "http://local.revolt.chat:14705"
voso_legacy = ""
voso_legacy_ws = ""
# Public urls for livekit nodes
# each entry here should have a corresponding entry under `api.livekit.nodes`
[hosts.livekit]
worldwide = "ws://local.revolt.chat:14706"
[api]
[api.smtp]
@@ -38,6 +43,19 @@ from_address = "development@revolt.chat"
reply_to = "support@revolt.chat"
port = 14025
use_tls = false
use_starttls = false
[api.livekit]
# Config for livekit nodes
# Make sure to change the secret when deploying
# The key and secret should match the values livekit is using
[api.livekit.nodes.worldwide]
url = "http://livekit"
lat = 0.0
lon = 0.0
key = "worldwide"
secret = "ZjCofRlfm6GGtjlifmNpCDkcQbEIIVC0"
[files.s3]
# S3 protocol endpoint

View File

@@ -1,73 +0,0 @@
# Code Style Guide
Beyond using Cargo format and Clippy, there are some specific code style guidelines laid out in this document for different parts of the project.
## Writing Style
- Shorten "identifier" to "Id" with that exact casing, i.e. Server Id.
## `core/database` crate
w.r.t. `model.rs` files
- All struct definitions must be commented.
```rust
/// Server
pub struct Server {
/// Name of the server
pub name: String,
```
- Struct definitions should not include derives unless necessary (if additional traits such as Hash are required) and instead use `auto_derived!` and `auto_derived_partial!`.
```rust
auto_derived_partial!(
/// Server
pub struct Server { .. },
"PartialServer"
);
```
- `auto_derived!` macro accepts multiple entries and should be used as such:
```rust
auto_derived!(
/// Optional fields on server object
pub enum FieldsServer { .. }
/// Optional fields on server object
pub enum FieldsRole { .. }
);
```
- If special serialisation conditions are required, such as checking if a boolean is false, use the existing definitions for these functions from the crate root:
```rust
#[serde(skip_serializing_if = "crate::if_false", default)]
```
- `impl` blocks may be defined below the struct definitions and should be ordered in the same order of definition. Methods in the block must follow the same guidelines as traits where-in: methods are ordered in terms of CRUD, there are empty line breaks, and methods are commented.
w.r.t. `ops` module for models
- All traits must use a the name format `AbstractPlural` where Plural is the plural form of the collection. e.g. Servers
- Traits defined must follow these guidelines:
- Methods are ordered in terms of CRUD, create-read-update-delete ordering.
```rust
#[async_trait]
pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database
async fn insert_member(&self, member: &Member) -> Result<()>;
/// Fetch a server member by their id
async fn fetch_member(&self, server_id: &str, user_id: &str) -> Result<Member>;
/// Update information for a server member
async fn update_member(&self, .. ) -> Result<()>;
/// Delete a server member by their id
async fn delete_member(&self, id: &MemberCompositeKey) -> Result<()>;
}
```
- There should be an empty line break between each method declaration.
- All methods must have an appropriate comment.
- When implementing the trait defined in `ops.rs` with each driver, the method declaration style should be the same for ease of searching: same ordering, same comments, same line breaks.

View File

@@ -8,18 +8,33 @@ 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
hard: 65536
# 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:
@@ -34,13 +49,13 @@ services:
- minio
entrypoint: >
/bin/sh -c "while ! /usr/bin/mc ready minio; do
/usr/bin/mc config host add minio http://minio:9000 minioautumn minioautumn;
/usr/bin/mc alias set minio http://minio:9000 minioautumn minioautumn;
echo 'Waiting minio...' && sleep 1;
done; /usr/bin/mc mb minio/revolt-uploads; exit 0;"
# Rabbit
rabbit:
image: rabbitmq:3-management
image: rabbitmq:4-management
environment:
RABBITMQ_DEFAULT_USER: rabbituser
RABBITMQ_DEFAULT_PASS: rabbitpass
@@ -55,7 +70,7 @@ services:
# Mock SMTP server
maildev:
image: soulteary/maildev
image: maildev/maildev
ports:
- "14025:25"
- "14080:8080"
@@ -64,3 +79,10 @@ services:
MAILDEV_WEB_PORT: 8080
MAILDEV_INCOMING_USER: smtp
MAILDEV_INCOMING_PASS: smtp
livekit:
image: ghcr.io/stoatchat/livekit-server:v1.9.13
command: --config /etc/livekit.yml
network_mode: "host"
volumes:
- ./livekit.yml:/etc/livekit.yml

View File

@@ -1,7 +0,0 @@
target
index.node
**/node_modules
**/.DS_Store
npm-debug.log*
cargo.log
cross.log

View File

@@ -1,25 +0,0 @@
[package]
name = "revolt-nodejs-bindings"
version = "0.8.4"
description = "Node.js bindings for the Revolt software"
authors = ["Paul Makles <me@insrt.uk>"]
license = "MIT"
edition = "2021"
exclude = ["index.node"]
[lib]
crate-type = ["cdylib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
neon = "1.0.0"
neon-serde4 = "1.0.0"
serde = { version = "1", features = ["derive"] }
async-std = "1.12.0"
revolt-config = { version = "0.8.4", path = "../../core/config" }
revolt-result = { version = "0.8.4", path = "../../core/result" }
revolt-database = { version = "0.8.4", path = "../../core/database" }

View File

@@ -1,92 +0,0 @@
# revolt.node
**revolt.node:** Node.js bindings for the Revolt software
This project was bootstrapped by [create-neon](https://www.npmjs.com/package/create-neon).
## Building revolt.node
Building revolt.node requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
To run the build, run:
```sh
$ npm run build
```
This command uses the [@neon-rs/cli](https://www.npmjs.com/package/@neon-rs/cli) utility to assemble the binary Node addon from the output of `cargo`.
## Exploring revolt.node
After building revolt.node, you can explore its exports at the Node console:
```sh
$ npm i
$ npm run build
$ node
> require('.').hello()
'hello node'
```
## Available Scripts
In the project directory, you can run:
#### `npm install`
Installs the project, including running `npm run build`.
#### `npm run build`
Builds the Node addon (`index.node`) from source, generating a release build with `cargo --release`.
Additional [`cargo build`](https://doc.rust-lang.org/cargo/commands/cargo-build.html) arguments may be passed to `npm run build` and similar commands. For example, to enable a [cargo feature](https://doc.rust-lang.org/cargo/reference/features.html):
```
npm run build -- --feature=beetle
```
#### `npm run debug`
Similar to `npm run build` but generates a debug build with `cargo`.
#### `npm run cross`
Similar to `npm run build` but uses [cross-rs](https://github.com/cross-rs/cross) to cross-compile for another platform. Use the [`CARGO_BUILD_TARGET`](https://doc.rust-lang.org/cargo/reference/config.html#buildtarget) environment variable to select the build target.
#### `npm test`
Runs the unit tests by calling `cargo test`. You can learn more about [adding tests to your Rust code](https://doc.rust-lang.org/book/ch11-01-writing-tests.html) from the [Rust book](https://doc.rust-lang.org/book/).
## Project Layout
The directory structure of this project is:
```
revolt.node/
├── Cargo.toml
├── README.md
├── src/
| └── lib.rs
├── index.node
├── package.json
└── target/
```
| Entry | Purpose |
|----------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `Cargo.toml` | The Cargo [manifest file](https://doc.rust-lang.org/cargo/reference/manifest.html), which informs the `cargo` command. |
| `README.md` | This file. |
| `src/` | The directory tree containing the Rust source code for the project. |
| `lib.rs` | Entry point for the Rust source code. |
| `index.node` | The main module, a [Node addon](https://nodejs.org/api/addons.html) generated by the build and pointed to by `"main"` in `package.json`. |
| `package.json` | The npm [manifest file](https://docs.npmjs.com/cli/v7/configuring-npm/package-json), which informs the `npm` command. |
| `target/` | Binary artifacts generated by the Rust build. |
## Learn More
Learn more about:
- [Neon](https://neon-bindings.com).
- [Rust](https://www.rust-lang.org).
- [Node](https://nodejs.org).

View File

@@ -1,84 +0,0 @@
import { Channel, User } from "revolt-api";
/**
* Opaque type for Revolt database
*/
export declare interface Database {}
/**
* Opaque type for Revolt database
*/
export declare interface OpaqueUser {}
/**
* Error type from Revolt backend
*/
export declare interface Err {
type: string;
location: string;
}
/**
* Initialises background tasks and logging, must be called before anything else!
* Can be called multiple times!
*/
export declare function init();
/**
* Gets a new handle to the Revolt database
* @returns {Database} Handle
*/
export declare function database(): Database;
/**
* Fetch user from database
* @param {string} userId User's ID
* @this {Database}
*/
export declare function database_fetch_user(userId: string): OpaqueUser;
/**
* Fetch user from database
* @param {string} username Username
* @param {string} discriminator Discriminator
* @this {Database}
*/
export declare function database_fetch_user_by_username(
username: string,
discriminator: string
): OpaqueUser;
/**
* Gets model data as JSON
* @this {OpaqueUser}
*/
export declare function model_data(): User;
/**
* Gets error if the model failed to fetch
* @this {OpaqueUser}
*/
export declare function model_error(): Err;
/**
* Open a direct message channel between two users
* @param {string} userA User A ID
* @param {string} userB User B ID
* @returns Existing or newly created channel
*/
export declare function proc_channels_create_dm(
userA: string,
userB: string
): Promise<Channel & { error: Err }>;
/**
* Suspend a user
* @param {string} user User
* @param {number} duration Duration (in days), set to 0 for indefinite
* @param {string} reason Pipe-separated list of reasons (e.g. reason1|reason2|reason3)
*/
export declare function proc_users_suspend(
user: OpaqueUser,
duration: number,
reason: string
): Promise<{ error: Err }>;

View File

@@ -1,36 +0,0 @@
{
"name": "revolt-nodejs-bindings",
"version": "0.7.15-rev0.0.3",
"description": "Node.js bindings for the Revolt software",
"main": "index.node",
"scripts": {
"test": "cargo test",
"cargo-build": "cargo build --message-format=json > cargo.log",
"cross-build": "cross build --message-format=json > cross.log",
"postcargo-build": "neon dist < cargo.log",
"postcross-build": "neon dist -m /target < cross.log",
"debug": "npm run cargo-build --",
"build": "npm run cargo-build -- --release",
"cross": "npm run cross-build -- --release"
},
"author": "Paul Makles",
"license": "AGPL-3.0",
"devDependencies": {
"@neon-rs/cli": "0.1.73"
},
"repository": {
"type": "git",
"url": "git+https://github.com/revoltchat/backend"
},
"keywords": [
"revolt",
"chat"
],
"bugs": {
"url": "https://github.com/revoltchat/backend/issues"
},
"homepage": "https://github.com/revoltchat/backend#readme",
"dependencies": {
"revolt-api": "^0.7.15"
}
}

View File

@@ -1,117 +0,0 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
revolt-api:
specifier: ^0.7.15
version: 0.7.15
devDependencies:
'@neon-rs/cli':
specifier: 0.1.73
version: 0.1.73
packages:
/@cargo-messages/android-arm-eabi@0.1.72:
resolution: {integrity: sha512-gGZxIM1mj+Y5x+ULND6ZCNr7f70OJi9wDlycSK8hGONy9wrChN6JAIHryddC5cqcwlYAoQ6IDcDFElnhAYbybA==}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/darwin-arm64@0.1.72:
resolution: {integrity: sha512-EAzN5MLaXPljZKZDO5qR+aBs44eSq2ZbEnS7AI/FziE3MzeXbrGOS3fLba5+7yWPFXJyZolXzePm8N1EBv8ovg==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/darwin-x64@0.1.72:
resolution: {integrity: sha512-RLo6j8s3nYbjdd1LDct4wamfChyRit7zokUuxtIYCu9XOlltkN5vnj1vwnrPvoqCMZ/7CbbuHFwSTn9A71de/w==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/linux-arm-gnueabihf@0.1.72:
resolution: {integrity: sha512-tHsRshuzfjrX6SDW3jg6al8vMNLTMgczGnVYl5RuBZf/yrAUuwe30KxA9ge6w6mW6Ox797DyBchzAc9OLgTgmQ==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/linux-x64-gnu@0.1.72:
resolution: {integrity: sha512-VGtL6CCnUbhsP4aYuBNT5kfrAL7o0qjrxw97a+ax13t+nJd26tVEEIKHMu5drvvS/Nm/hn7sLT8zMnnCv0pvHg==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/win32-arm64-msvc@0.1.72:
resolution: {integrity: sha512-V93Cgz39K+yqa3MveNbhh29pYCp8izK5uEavjPoxlNxAbsMCWH+s0verGDdUcfGxjR1H2V7oZ4FszPqR2SqMRQ==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@cargo-messages/win32-x64-msvc@0.1.72:
resolution: {integrity: sha512-knz3uSrO0OSbq3U5VWfCY8FB4NsM43BOWLZ7x4sfaMOC1XWv+IyvDdkLe6DhJx8KUw46KIAimYs9YROrp6l46Q==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@neon-rs/cli@0.1.73:
resolution: {integrity: sha512-1kv8S/feB6UQWQQwsnGfkSkEBOtlFDNExnioL81E2BwvUWgjQPaseHgpi2EpWVgsPUgur5eBm4QowmlpWkD4/w==}
hasBin: true
optionalDependencies:
'@cargo-messages/android-arm-eabi': 0.1.72
'@cargo-messages/darwin-arm64': 0.1.72
'@cargo-messages/darwin-x64': 0.1.72
'@cargo-messages/linux-arm-gnueabihf': 0.1.72
'@cargo-messages/linux-x64-gnu': 0.1.72
'@cargo-messages/win32-arm64-msvc': 0.1.72
'@cargo-messages/win32-x64-msvc': 0.1.72
dev: true
/axios@0.26.1:
resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==}
dependencies:
follow-redirects: 1.15.6
transitivePeerDependencies:
- debug
dev: false
/follow-redirects@1.15.6:
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/lodash.defaultsdeep@4.6.1:
resolution: {integrity: sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA==}
dev: false
/revolt-api@0.7.15:
resolution: {integrity: sha512-rWRnjBFvoXt/RHRxHg9KuP+x1Jk61KdsOpKOczVJg5tIqGDmqp/6QZHw5rJTtG+wCH3Ah3YKwY+MM5NHfg1W3Q==}
dependencies:
axios: 0.26.1
lodash.defaultsdeep: 4.6.1
transitivePeerDependencies:
- debug
dev: false

View File

@@ -1,221 +0,0 @@
#[macro_use]
extern crate serde;
use std::sync::OnceLock;
use neon::prelude::*;
use revolt_database::{Database, DatabaseInfo};
fn js_init(mut cx: FunctionContext) -> JsResult<JsUndefined> {
// static INIT: OnceLock<()> = OnceLock::new();
// if INIT.get().is_none() {
// INIT.get_or_init(|| {
// async_std::task::block_on(async {
// revolt_config::configure!(api);
// match DatabaseInfo::Auto.connect().await {
// Ok(db) => {
// let authifier_db = db.clone().to_authifier().await.database;
// revolt_database::tasks::start_workers(db, authifier_db);
// Ok(())
// }
// Err(err) => Err(err),
// }
// })
// .or_else(|err| cx.throw_error(err))
// .unwrap();
// });
// }
Ok(cx.undefined())
}
struct DatabaseBinding(Database, Channel);
impl Finalize for DatabaseBinding {}
impl DatabaseBinding {
fn take(&self) -> (Database, Channel) {
(self.0.clone(), self.1.clone())
}
}
fn js_database(mut cx: FunctionContext) -> JsResult<JsBox<DatabaseBinding>> {
let db = async_std::task::block_on(DatabaseInfo::Auto.connect())
.or_else(|err| cx.throw_error(err))?;
let channel = cx.channel();
Ok(cx.boxed(DatabaseBinding(db, channel)))
}
// Implementations for models
#[derive(Clone)]
#[allow(clippy::large_enum_variant)]
enum Model {
User(revolt_database::User),
Error(revolt_result::Error),
}
impl Model {
fn give(&self) -> Model {
self.clone()
}
}
impl Finalize for Model {}
macro_rules! shim_boxed {
($cx: ident, $name: ident, $model: ident, $( $variable: ident $type: ident $id: expr )+, $cmd: ident, $( $arg: expr, )+) => {
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
$(
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
)+
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
let (deferred, promise) = cx.promise();
async_std::task::spawn(async move {
let result = db.$cmd($($arg,)+).await;
deferred.settle_with(&channel, move |mut cx| {
Ok(cx.boxed(match result {
Ok(value) => Model::$model(value),
Err(error) => Model::Error(error)
}))
})
});
Ok(promise)
}
$cx.export_function(stringify!($name), $name)?;
};
}
fn js_data(mut cx: FunctionContext) -> JsResult<JsValue> {
match cx.this::<JsBox<Model>>()?.give() {
Model::Error(_) => neon_serde4::to_value(&mut cx, &None::<()>),
Model::User(user) => neon_serde4::to_value(&mut cx, &user),
}
.or_else(|e| cx.throw_error(e.to_string()))
}
fn js_error(mut cx: FunctionContext) -> JsResult<JsValue> {
let value = match cx.this::<JsBox<Model>>()?.give() {
Model::Error(err) => Some(err),
_ => None,
};
neon_serde4::to_value(&mut cx, &value).or_else(|e| cx.throw_error(e.to_string()))
}
// Basic data implementation
#[derive(Serialize, Deserialize)]
struct ResultBinding<T> {
#[serde(flatten)]
value: Option<T>,
error: Option<revolt_result::Error>,
}
macro_rules! shim {
($cx: ident, $name: ident, $( $variable: ident $type: ident $id: expr )*, $( $model: ident $modelType: ident $modelId: expr )*, | $db: ident | $closure: expr, $( $arg: expr, )+) => {
fn $name(mut cx: FunctionContext) -> JsResult<JsPromise> {
$(
let $variable = cx.argument::<$type>($id)?.value(&mut cx);
)*
$(
let mut $model = if let Model::$modelType(value) = cx.argument::<JsBox<Model>>($modelId)?.give() {
value
} else {
unreachable!()
};
)*
let (db, channel) = cx.this::<JsBox<DatabaseBinding>>()?.take();
let (deferred, promise) = cx.promise();
async_std::task::spawn(async move {
#[allow(clippy::redundant_closure_call)]
let result = (|$db: $crate::Database| $closure)(db.clone()).await;
deferred.settle_with(&channel, move |mut cx| {
neon_serde4::to_value(
&mut cx,
&match result {
Ok(value) => ResultBinding {
value: Some(value),
error: None,
},
Err(error) => ResultBinding {
value: None,
error: Some(error),
},
},
)
.or_else(|e| cx.throw_error(e.to_string()))
})
});
Ok(promise)
}
$cx.export_function(stringify!($name), $name)?;
};
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
// initialise required background stuff
cx.export_function("init", js_init)?;
// database & model stuff
cx.export_function("database", js_database)?;
cx.export_function("model_data", js_data)?;
cx.export_function("model_error", js_error)?;
shim_boxed!(
cx,
database_fetch_user,
User,
user_id JsString 0,
fetch_user,
&user_id,
);
shim_boxed!(
cx,
database_fetch_user_by_username,
User,
username JsString 0
discriminator JsString 1,
fetch_user_by_username,
&username, &discriminator,
);
// procedure calls
shim!(
cx,
proc_channels_create_dm,
user_a JsString 0
user_b JsString 1,
,
|db| async move {
let user_a = db.fetch_user(&user_a).await?;
let user_b = db.fetch_user(&user_b).await?;
revolt_database::Channel::create_dm(&db, &user_a, &user_b).await
},
&userA, &userB,
);
shim!(
cx,
proc_users_suspend,
duration JsNumber 1
reason JsString 2,
user User 0,
|db| async move {
let duration = duration as usize;
user.suspend(&db, if duration == 0 { None } else { Some(duration) }, Some(reason.split('|').map(|x| x.to_owned()).collect())).await
},
&user,
);
Ok(())
}

View File

@@ -1,53 +0,0 @@
const Internal = require(".");
// playing around with class wrapper, not practical
class Model {
constructor(model) {
this.model = model;
}
data() {
return Internal.model_data.bind(this.model)();
}
error() {
return Internal.model_error.bind(this.model)();
}
}
class User extends Model {
constructor(db, user) {
super(user);
this.db = db;
}
}
class Database {
constructor() {
this.db = Internal.database();
}
async fetchUser(userId) {
return new User(
this,
await Internal.database_fetch_user.bind(this.db)(userId)
);
}
async fetchUserByUsername(username, discriminator) {
return new User(
this,
await Internal.database_fetch_user_by_username.bind(this.db)(
username,
discriminator
)
);
}
}
const db = new Database();
db.fetchUserByUsername("dos", "7624").then((user) => console.info(user.data()));
db.fetchUserByUsername("dos", "1111").then((user) => console.info(user.data()));
db.fetchUserByUsername("dos", "1111").then((user) =>
console.info(user.error())
);

View File

@@ -1,6 +0,0 @@
{
"name": "bindings",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@@ -1,48 +1,46 @@
[package]
name = "revolt-bonfire"
version = "0.8.4"
version = "0.13.7"
license = "AGPL-3.0-or-later"
edition = "2021"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# util
log = "*"
sentry = "0.31.5"
lru = "0.7.6"
ulid = "0.5.0"
once_cell = "1.9.0"
redis-kiss = "0.1.4"
lru_time_cache = "0.11.11"
async-channel = "2.3.1"
log = { workspace = true }
sentry = { workspace = true }
lru = { workspace = true }
ulid = { workspace = true }
once_cell = { workspace = true }
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
lru_time_cache = { workspace = true }
async-channel = { workspace = true }
# parsing
querystring = "1.1.0"
querystring = { workspace = true }
regex = { workspace = true }
# serde
bincode = "1.3.3"
serde_json = "1.0.79"
rmp-serde = "1.0.0"
serde = "1.0.136"
bincode = { workspace = true }
serde_json = { workspace = true }
rmp-serde = { workspace = true }
serde = { workspace = true }
# async
futures = "0.3.21"
async-tungstenite = { version = "0.17.0", features = ["async-std-runtime"] }
async-std = { version = "1.8.0", features = [
"tokio1",
"tokio02",
"attributes",
] }
futures = { workspace = true }
async-tungstenite = { workspace = true, features = ["tokio-runtime"] }
tokio = { workspace = true }
tokio-util = { workspace = true, features = ["compat"] }
# core
authifier = { version = "1.0.10" }
revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.8.4", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
revolt-result = { workspace = true }
revolt-models = { workspace = true }
revolt-config = { workspace = true }
revolt-database = { workspace = true, features = ["voice"] }
revolt-permissions = { workspace = true }
revolt-presence = { workspace = true, features = ["redis-is-patched"] }
# redis
fred = { version = "8.0.1", features = ["subscriber-client"] }
fred = { workspace = true, features = ["subscriber-client"] }

View File

@@ -1,5 +1,5 @@
# Build Stage
FROM ghcr.io/revoltchat/base:latest AS builder
FROM ghcr.io/stoatchat/base:latest AS builder
FROM debian:12 AS debian
# Bundle Stage

View File

@@ -1,9 +1,15 @@
use async_tungstenite::tungstenite::{handshake, Message};
use futures::channel::oneshot::Sender;
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_database::events::client::ReadyPayloadFields;
use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize};
/// matches either a single word ie "users" or a key and value ie "settings[notifications]"
static READY_PAYLOAD_FIELD_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^(\w+)(?:\[(\S+)\])?$"#).unwrap());
/// Enumeration of supported protocol formats
#[derive(Debug)]
pub enum ProtocolFormat {
@@ -17,6 +23,7 @@ pub struct ProtocolConfiguration {
protocol_version: i32,
format: ProtocolFormat,
session_token: Option<String>,
ready_payload_fields: ReadyPayloadFields,
}
impl ProtocolConfiguration {
@@ -25,11 +32,13 @@ impl ProtocolConfiguration {
protocol_version: i32,
format: ProtocolFormat,
session_token: Option<String>,
ready_payload_fields: ReadyPayloadFields,
) -> Self {
Self {
protocol_version,
format,
session_token,
ready_payload_fields,
}
}
@@ -86,14 +95,8 @@ impl ProtocolConfiguration {
}
/// Get ready payload fields
pub fn get_ready_payload_fields(&self) -> Vec<ReadyPayloadFields> {
vec![
ReadyPayloadFields::Users,
ReadyPayloadFields::Servers,
ReadyPayloadFields::Channels,
ReadyPayloadFields::Members,
ReadyPayloadFields::Emoji,
]
pub fn get_ready_payload_fields(&self) -> &ReadyPayloadFields {
&self.ready_payload_fields
}
}
@@ -124,6 +127,23 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
let mut protocol_version = 1;
let mut format = ProtocolFormat::Json;
let mut session_token = None;
let mut ready_payload_fields = if params.iter().any(|(k, _)| *k == "ready") {
// If they pass the ready field, set all fields to false
ReadyPayloadFields {
users: false,
servers: false,
channels: false,
members: false,
emojis: false,
voice_states: false,
user_settings: Vec::new(),
channel_unreads: false,
policy_changes: false,
}
} else {
ReadyPayloadFields::default()
};
// Parse and map parameters from key-value to known variables.
for (key, value) in params {
@@ -139,6 +159,31 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
_ => {}
},
"token" => session_token = Some(value.into()),
"ready" => {
// Re-enable all the fields the client specifies
if let Some(captures) = READY_PAYLOAD_FIELD_REGEX.captures(value) {
if let Some(field) = captures.get(0) {
match field.as_str() {
"users" => ready_payload_fields.users = true,
"servers" => ready_payload_fields.servers = true,
"channels" => ready_payload_fields.channels = true,
"members" => ready_payload_fields.members = true,
"emojis" => ready_payload_fields.emojis = true,
"voice_states" => ready_payload_fields.voice_states = true,
"channel_unreads" => ready_payload_fields.channel_unreads = true,
"user_settings" => {
if let Some(subkey) = captures.get(1) {
ready_payload_fields
.user_settings
.push(subkey.as_str().to_string());
}
}
"policy_changes" => ready_payload_fields.policy_changes = true,
_ => {}
}
}
}
}
_ => {}
}
}
@@ -151,6 +196,7 @@ impl handshake::server::Callback for WebsocketHandshakeCallback {
protocol_version,
format,
session_token,
ready_payload_fields,
})
.is_ok()
{

View File

@@ -1,9 +1,11 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use futures::future::join_all;
use redis_kiss::AsyncCommands;
use revolt_database::{
events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery,
voice::{get_channel_voice_state, UserVoiceChannel},
Channel, Database, Member, MemberCompositeKey, Presence, RelationshipStatus,
};
use revolt_models::v0;
@@ -17,8 +19,9 @@ use super::state::{Cache, State};
impl Cache {
/// Check whether the current user can view a channel
pub async fn can_view_channel(&self, db: &Database, channel: &Channel) -> bool {
#[allow(deprecated)]
match &channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
Channel::TextChannel { server, .. } => {
let member = self.members.get(server);
let server = self.servers.get(server);
let mut query =
@@ -95,11 +98,25 @@ impl State {
pub async fn generate_ready_payload(
&mut self,
db: &Database,
fields: Vec<ReadyPayloadFields>,
fields: &ReadyPayloadFields,
) -> Result<EventV1> {
let user = self.clone_user();
self.cache.is_bot = user.bot.is_some();
// Fetch pending policy changes.
let policy_changes = if user.bot.is_some() || !fields.policy_changes {
None
} else {
Some(
db.fetch_policy_changes()
.await?
.into_iter()
.filter(|policy| policy.created_time > user.last_acknowledged_policy_change)
.map(Into::into)
.collect(),
)
};
// Find all relationships to the user.
let mut user_ids: HashSet<String> = user
.relations
@@ -108,12 +125,7 @@ impl State {
.unwrap_or_default();
// Fetch all memberships with their corresponding servers.
let members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
let mut members: Vec<Member> = db.fetch_all_memberships(&user.id).await?;
let server_ids: Vec<String> = members.iter().map(|x| x.id.server.clone()).collect();
let servers = db.fetch_servers(&server_ids).await?;
@@ -142,6 +154,55 @@ impl State {
}
}
let voice_states = if fields.voice_states {
let mut voice_state_server_members: HashMap<String, HashSet<String>> = HashMap::new();
// fetch voice states for all the channels we can see
let mut voice_states = Vec::new();
for channel in channels.iter().filter(|c| {
matches!(
c,
Channel::DirectMessage { .. }
| Channel::Group { .. }
| Channel::TextChannel { voice: Some(_), .. }
)
}) {
if let Ok(Some(voice_state)) =
get_channel_voice_state(&UserVoiceChannel::from_channel(channel)).await
{
if let Some(server) = channel.server() {
let set = voice_state_server_members
.entry(server.to_string())
.or_default();
for participant in &voice_state.participants {
user_ids.insert(participant.id.clone());
set.insert(participant.id.clone());
}
} else {
for participant in &voice_state.participants {
user_ids.insert(participant.id.clone());
}
}
voice_states.push(voice_state);
}
}
// Fetch all the members for for the participants who are in a server
for (server, user_ids) in voice_state_server_members {
let user_ids = user_ids.into_iter().collect::<Vec<_>>();
let voice_members = db.fetch_members(&server, &user_ids).await?;
members.extend(voice_members);
}
Some(voice_states)
} else {
None
};
// Fetch presence data for known users.
let online_ids = filter_online(&user_ids.iter().cloned().collect::<Vec<String>>()).await;
@@ -155,8 +216,14 @@ impl State {
)
.await?;
self.cache.members = members
.iter()
.cloned()
.map(|x| (x.id.server.clone(), x))
.collect();
// Fetch customisations.
let emojis = if fields.contains(&ReadyPayloadFields::Emoji) {
let emojis = if fields.emojis {
Some(
db.fetch_emoji_by_parent_ids(
&servers
@@ -164,25 +231,34 @@ impl State {
.map(|x| x.id.to_string())
.collect::<Vec<String>>(),
)
.await?,
.await?
.into_iter()
.map(|emoji| emoji.into())
.collect(),
)
} else {
None
};
// Fetch user settings
let user_settings = if let Some(ReadyPayloadFields::UserSettings(keys)) = fields
.iter()
.find(|e| matches!(e, ReadyPayloadFields::UserSettings(_)))
{
Some(db.fetch_user_settings(&user.id, &keys).await?)
let user_settings = if !fields.user_settings.is_empty() {
Some(
db.fetch_user_settings(&user.id, &fields.user_settings)
.await?,
)
} else {
None
};
// Fetch channel unreads
let channel_unreads = if fields.contains(&ReadyPayloadFields::ChannelUnreads) {
Some(db.fetch_unreads(&user.id).await?)
let channel_unreads = if fields.channel_unreads {
Some(
db.fetch_unreads(&user.id)
.await?
.into_iter()
.map(|unread| unread.into())
.collect(),
)
} else {
None
};
@@ -199,12 +275,11 @@ impl State {
.collect();
// Make all users appear from our perspective.
let mut users: Vec<v0::User> = join_all(users
.into_iter()
.map(|other_user| async {
let is_online = online_ids.contains(&other_user.id);
other_user.into_known(&user, is_online).await
})).await;
let mut users: Vec<v0::User> = join_all(users.into_iter().map(|other_user| async {
let is_online = online_ids.contains(&other_user.id);
other_user.into_known(&user, is_online).await
}))
.await;
// Make sure we see our own user correctly.
users.push(user.into_self(true).await);
@@ -228,31 +303,31 @@ impl State {
for channel in &channels {
self.insert_subscription(channel.id().to_string()).await;
}
Ok(EventV1::Ready {
users: if fields.contains(&ReadyPayloadFields::Users) {
Some(users)
} else {
None
},
servers: if fields.contains(&ReadyPayloadFields::Servers) {
users: if fields.users { Some(users) } else { None },
servers: if fields.servers {
Some(servers.into_iter().map(Into::into).collect())
} else {
None
},
channels: if fields.contains(&ReadyPayloadFields::Channels) {
channels: if fields.channels {
Some(channels.into_iter().map(Into::into).collect())
} else {
None
},
members: if fields.contains(&ReadyPayloadFields::Members) {
members: if fields.members {
Some(members.into_iter().map(Into::into).collect())
} else {
None
},
emojis: emojis.map(|vec| vec.into_iter().map(Into::into).collect()),
voice_states,
emojis,
user_settings,
channel_unreads: channel_unreads.map(|vec| vec.into_iter().map(Into::into).collect()),
channel_unreads,
policy_changes,
})
}
@@ -265,19 +340,14 @@ impl State {
let id = &id.to_string();
for (channel_id, channel) in &self.cache.channels {
match channel {
Channel::TextChannel { server, .. } | Channel::VoiceChannel { server, .. } => {
if server == id {
channel_ids.insert(channel_id.clone());
if channel.server() == Some(id) {
channel_ids.insert(channel_id.clone());
if self.cache.can_view_channel(db, channel).await {
added_channels.push(channel_id.clone());
} else {
removed_channels.push(channel_id.clone());
}
}
if self.cache.can_view_channel(db, channel).await {
added_channels.push(channel_id.clone());
} else {
removed_channels.push(channel_id.clone());
}
_ => {}
}
}
@@ -332,6 +402,11 @@ impl State {
/// Push presence change to the user and all associated server topics
pub async fn broadcast_presence_change(&self, target: bool) {
let config = revolt_config::config().await;
if config.disable_events_dont_use {
return;
}
if if let Some(status) = &self.cache.users.get(&self.cache.user_id).unwrap().status {
status.presence != Some(Presence::Invisible)
} else {
@@ -445,6 +520,7 @@ impl State {
server,
channels,
emojis: _,
voice_states: _,
} => {
self.insert_subscription(id.clone()).await;

View File

@@ -1,10 +1,8 @@
use std::{
collections::{HashMap, HashSet},
sync::Arc,
time::Duration,
collections::{HashMap, HashSet}, num::NonZeroUsize, sync::Arc, time::Duration
};
use async_std::sync::{Mutex, RwLock};
use tokio::sync::{Mutex, RwLock};
use lru::LruCache;
use lru_time_cache::{LruCache as LruTimeCache, TimedEntry};
use revolt_database::{Channel, Member, Server, User};
@@ -57,7 +55,7 @@ impl Default for Cache {
members: Default::default(),
servers: Default::default(),
seen_events: LruCache::new(20),
seen_events: LruCache::new(NonZeroUsize::new(20).unwrap()),
}
}
}

View File

@@ -1,6 +1,6 @@
use std::env;
use async_std::net::TcpListener;
use tokio::net::TcpListener;
use revolt_presence::clear_region;
#[macro_use]
@@ -12,7 +12,7 @@ pub mod events;
mod database;
mod websocket;
#[async_std::main]
#[tokio::main]
async fn main() {
// Configure requirements for Bonfire.
revolt_config::configure!(events);
@@ -33,7 +33,7 @@ async fn main() {
// Start accepting new connections and spawn a client for each connection.
while let Ok((stream, addr)) = listener.accept().await {
async_std::task::spawn(async move {
tokio::task::spawn(async move {
info!("User connected from {addr:?}");
websocket::client(database::get_db(), stream, addr).await;
info!("User disconnected from {addr:?}");

View File

@@ -1,11 +1,10 @@
use std::{collections::HashSet, net::SocketAddr, sync::Arc};
use async_tungstenite::WebSocketStream;
use authifier::AuthifierEvent;
use fred::{
error::{RedisError, RedisErrorKind},
error::RedisErrorKind,
interfaces::{ClientLike, EventInterface, PubsubInterface},
types::RedisConfig,
types::{ReconnectPolicy, RedisConfig},
};
use futures::{
channel::oneshot,
@@ -13,27 +12,30 @@ 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},
iso8601_timestamp::Timestamp,
Database, User, UserHint,
};
use revolt_presence::{create_session, delete_session};
use async_std::{
use tokio::{
net::TcpStream,
sync::{Mutex, RwLock},
task::spawn,
};
use tokio_util::compat::{TokioAsyncReadCompatExt, Compat};
use revolt_result::create_error;
use sentry::Level;
use crate::config::{ProtocolConfiguration, WebsocketHandshakeCallback};
use crate::events::state::{State, SubscriptionStateChange};
use revolt_models::v0;
type WsReader = SplitStream<WebSocketStream<TcpStream>>;
type WsWriter = SplitSink<WebSocketStream<TcpStream>, async_tungstenite::tungstenite::Message>;
type WsReader = SplitStream<WebSocketStream<Compat<TcpStream>>>;
type WsWriter = SplitSink<WebSocketStream<Compat<TcpStream>>, async_tungstenite::tungstenite::Message>;
/// Start a new WebSocket client worker given access to the database,
/// the relevant TCP stream and the remote address of the client.
@@ -43,7 +45,7 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
// e.g. wss://example.com?format=json&version=1
let (sender, receiver) = oneshot::channel();
let Ok(ws) = async_tungstenite::accept_hdr_async_with_config(
stream,
stream.compat(),
WebsocketHandshakeCallback::from(sender),
None,
)
@@ -100,6 +102,10 @@ pub async fn client(db: &'static Database, stream: TcpStream, addr: SocketAddr)
info!("User {addr:?} authenticated as @{}", user.username);
db.update_session_last_seen(&session_id, Timestamp::now_utc())
.await
.ok();
// Create local state.
let mut state = State::from(user, session_id);
let user_id = state.cache.user_id.clone();
@@ -123,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;
@@ -213,10 +227,16 @@ async fn listener(
kill_signal_r: async_channel::Receiver<()>,
write: &Mutex<WsWriter>,
) {
let redis_config = RedisConfig::from_url(&REDIS_URI).unwrap();
let subscriber = match report_internal_error!(
fred::types::Builder::from_config(redis_config).build_subscriber_client()
) {
let stoat_config = revolt_config::config().await;
let url = stoat_config
.database
.redis_pubsub
.unwrap_or(REDIS_URI.to_string());
let redis_config = RedisConfig::from_url(&url).unwrap();
let mut builder = fred::types::Builder::from_config(redis_config);
builder.set_policy(ReconnectPolicy::new_exponential(8, 100, 30_000, 2));
let subscriber = match report_internal_error!(builder.build_subscriber_client()) {
Ok(subscriber) => subscriber,
Err(_) => return,
};
@@ -225,16 +245,21 @@ async fn listener(
return;
}
// Let Fred automatically re-subscribe to tracked channels on reconnect.
subscriber.manage_subscriptions();
// Handle Redis connection dropping
let (clean_up_s, clean_up_r) = async_channel::bounded(1);
let clean_up_s = Arc::new(Mutex::new(clean_up_s));
subscriber.on_error(move |err| {
warn!("Redis subscriber error: {:?}", err);
if let RedisErrorKind::Canceled = err.kind() {
let clean_up_s = clean_up_s.clone();
spawn(async move {
clean_up_s.lock().await.send(()).await.ok();
});
}
// Transient errors (IO, timeout) are handled by the reconnect policy.
Ok(())
});
@@ -330,22 +355,20 @@ async fn listener(
break 'out;
};
if let EventV1::Auth(auth) = &event {
if let AuthifierEvent::DeleteSession { session_id, .. } = auth {
if &state.session_id == session_id {
event = EventV1::Logout;
}
} else if let AuthifierEvent::DeleteAllSessions {
exclude_session_id, ..
} = auth
{
if let Some(excluded) = exclude_session_id {
if &state.session_id != excluded {
event = EventV1::Logout;
}
} else {
if let EventV1::DeleteSession { session_id, .. } = &event {
if &state.session_id == session_id {
event = EventV1::Logout;
}
} else if let EventV1::DeleteAllSessions {
exclude_session_id, ..
} = &event
{
if let Some(excluded) = exclude_session_id {
if &state.session_id != excluded {
event = EventV1::Logout;
}
} else {
event = EventV1::Logout;
}
} else {
let should_send = state.handle_incoming_event_v1(db, &mut event).await;
@@ -417,6 +440,8 @@ async fn worker(
mut read: WsReader,
write: &Mutex<WsWriter>,
) {
let revolt_config = revolt_config::config().await;
loop {
let t1 = read.try_next().fuse();
let t2 = kill_signal_r.recv().fuse();
@@ -453,6 +478,10 @@ async fn worker(
match payload {
ClientMessage::BeginTyping { channel } => {
if revolt_config.disable_events_dont_use {
continue;
}
if !subscribed.read().await.contains(&channel) {
continue;
}
@@ -465,6 +494,10 @@ async fn worker(
.await;
}
ClientMessage::EndTyping { channel } => {
if revolt_config.disable_events_dont_use {
continue;
}
if !subscribed.read().await.contains(&channel) {
continue;
}
@@ -502,3 +535,42 @@ async fn worker(
}
}
}
async fn fetch_user_slowmodes(user_id: &str) -> Option<Vec<v0::ChannelSlowmode>> {
let mut conn = get_connection().await.ok()?.into_inner();
let idx_key = format!("slowmode_idx:{}", user_id);
let channel_ids: Vec<String> = 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<i64> = 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)
}

View File

@@ -0,0 +1,28 @@
[package]
name = "revolt-coalesced"
version = "0.13.7"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>", "Zomatree <me@zomatree.live>"]
description = "Revolt Backend: Coalescion service"
repository = "https://github.com/stoatchat/stoatchat"
[features]
tokio = ["dep:tokio"]
queue = ["dep:indexmap"]
cache = ["dep:lru"]
default = ["tokio"]
[dependencies]
tokio = { workspace = true, features = ["sync"], optional = true }
indexmap = { workspace = true, optional = true }
lru = { workspace = true, optional = true }
[dev-dependencies]
tokio = { workspace = true, features = [
"rt",
"rt-multi-thread",
"macros",
"time",
] }

View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024 Pawel Makles
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,24 @@
#[derive(Clone, PartialEq, Eq, Debug)]
/// Config values for [`CoalescionService`].
pub struct CoalescionServiceConfig {
/// How many tasks are running at once
pub max_concurrent: Option<usize>,
/// Whether to queue tasks once `max_concurrent` is reached
#[cfg(feature = "queue")]
pub queue_requests: bool,
/// Max amount of tasks in the buffer queue
#[cfg(feature = "queue")]
pub max_queue: Option<usize>,
}
impl Default for CoalescionServiceConfig {
fn default() -> Self {
Self {
max_concurrent: Some(100),
#[cfg(feature = "queue")]
queue_requests: true,
#[cfg(feature = "queue")]
max_queue: Some(100)
}
}
}

View File

@@ -0,0 +1,27 @@
use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
/// Coalescion service error.
pub enum Error {
/// Failed to receive the actions return from the channel for unknown reason
RecvError,
/// Reached the `max_concurrent` amount of actions running at once and could not queue the action
MaxConcurrent,
/// Reached the `max_queue` amount of actions in the queue
MaxQueue,
/// Failed to downcast the type to the current type being returned, this will be most likely an ID collision
DowncastError,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::RecvError => write!(f, "Unable to receive data from the channel"),
Error::MaxConcurrent => write!(f, "Max number of tasks running at once"),
Error::MaxQueue => write!(f, "Max number of tasks in queue"),
Error::DowncastError => write!(f, "Failed to downcast type, possible key collision with different types")
}
}
}
impl std::error::Error for Error {}

View File

@@ -0,0 +1,39 @@
//! # Coalesced
//!
//! Coalescion service to group, caching and queue duplicate actions.
//! useful for deduplicating web requests, database lookups and other similar resource
//! intensive or rate-limited actions.
//!
//! ## Features
//! - `tokio`: Uses tokio for the async backend, this is currently the only backend.
//! - `queue`: Whether to support queueing requests to only allow X amount of actions running at once.
//! - `cache`: Whether to cache the actions results for future actions with the same id, uses an LRU cache internally.
//!
//! [`CoalescionService`] uses both [`Arc`] and [`RwLock`] internally and can be cheaply cloned to
//! use in your codebase.
//!
//! It is common practice to wrap the service and in your own which delegates the executions to ensure all ids are tracked in one location across your codebase.
//!
//! All values are stored using [`Any`] and must be [`'static`] + [`Send`] + [`Sync`], if there is an id mismatch
//! and a type is wrong the library will return an error, values returned from the service are also
//! wrapped in an [`Arc`] as they are shared to each duplicate action.
//!
//! ## Example:
//! ```rs
//! use revolt_coalesced::CoalescionService;
//!
//! let service = CoalescionService::new();
//!
//! let user_id = "my_user_id";
//! let user = service.execute(user_id, || async move {
//! database.fetch_user(user_id).await.unwrap()
//! }).await;
//! ```
mod config;
mod error;
mod service;
pub use config::CoalescionServiceConfig;
pub use error::Error;
pub use service::CoalescionService;

View File

@@ -0,0 +1,208 @@
use std::{any::Any, collections::HashMap, fmt::Debug, future::Future, hash::Hash, sync::Arc};
use tokio::sync::{
watch::{channel as watch_channel, Receiver},
RwLock,
};
#[cfg(feature = "cache")]
use lru::LruCache;
#[cfg(feature = "queue")]
use indexmap::IndexMap;
use crate::{CoalescionServiceConfig, Error};
#[derive(Debug, Clone)]
#[allow(clippy::type_complexity)]
/// # Coalescion service
///
/// See module description for example usage.
pub struct CoalescionService<Id: Hash + Clone + Eq> {
config: Arc<CoalescionServiceConfig>,
watchers: Arc<RwLock<HashMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
#[cfg(feature = "queue")]
queue: Arc<RwLock<IndexMap<Id, Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>>>>,
#[cfg(feature = "cache")]
cache: Option<Arc<tokio::sync::Mutex<LruCache<Id, Arc<dyn Any + Send + Sync>>>>>,
}
impl<Id: Hash + Clone + Eq> CoalescionService<Id> {
pub fn new() -> Self {
Default::default()
}
pub fn from_config(config: CoalescionServiceConfig) -> Self {
Self {
config: Arc::new(config),
watchers: Arc::new(RwLock::new(HashMap::new())),
#[cfg(feature = "queue")]
queue: Arc::new(RwLock::new(IndexMap::new())),
#[cfg(feature = "cache")]
cache: None,
}
}
#[cfg(feature = "cache")]
pub fn from_cache(
config: CoalescionServiceConfig,
cache: LruCache<Id, Arc<dyn Any + Send + Sync>>,
) -> Self {
Self {
cache: Some(Arc::new(Mutex::new(cache))),
..Self::from_config(config)
}
}
async fn wait_for<Value: Any + Send + Sync>(
&self,
mut receiver: Receiver<Option<Result<Arc<dyn Any + Send + Sync>, Error>>>,
) -> Result<Arc<Value>, Error> {
receiver
.wait_for(|v| v.is_some())
.await
.map_err(|_| Error::RecvError)
.and_then(|r| r.clone().unwrap())
.and_then(|arc| Arc::downcast(arc).map_err(|_| Error::DowncastError))
}
async fn insert_and_execute<
Value: Send + Sync + 'static,
F: FnOnce() -> Fut,
Fut: Future<Output = Value>,
>(
&self,
id: Id,
func: F,
) -> Result<Arc<Value>, Error> {
let (send, recv) = watch_channel(None);
self.watchers.write().await.insert(id.clone(), recv);
let value = Ok(Arc::new(func().await));
send.send_modify(|opt| {
opt.replace(value.clone().map(|v| v as Arc<dyn Any + Send + Sync>));
});
#[cfg(feature = "cache")]
if let Some(cache) = self.cache.as_ref() {
if let Ok(value) = &value {
cache.lock().await.push(id.clone(), value.clone());
}
};
self.watchers.write().await.remove(&id);
value
}
/// Coalesces an function, the actual function may not run if one with the same id is already running,
/// queued to be ran, or cached, the id should be globally unique for this specific action.
pub async fn execute<
Value: Send + Sync + 'static,
F: FnOnce() -> Fut,
Fut: Future<Output = Value>,
>(
&self,
id: Id,
func: F,
) -> Result<Arc<Value>, Error> {
#[cfg(feature = "cache")]
if let Some(cache) = self.cache.as_ref() {
if let Some(value) = cache.lock().await.get(&id) {
return Arc::downcast::<Value>(value.clone()).map_err(|_| Error::DowncastError);
}
};
let (receiver, length) = {
let watchers = self.watchers.read().await;
let length = watchers.len();
(watchers.get(&id).cloned(), length)
};
if let Some(receiver) = receiver {
self.wait_for(receiver).await
} else {
match self.config.max_concurrent {
Some(max_concurrent) if length >= max_concurrent => {
#[cfg(feature = "queue")]
if self.config.queue_requests {
let (receiver, length) = {
let queue = self.queue.read().await;
(queue.get(&id).cloned(), queue.len())
};
if let Some(receiver) = receiver {
return self.wait_for(receiver).await;
} else {
if self
.config
.max_queue
.is_some_and(|max_queue| max_queue >= length)
{
return Err(Error::MaxQueue);
};
let (send, recv) = watch_channel(None);
self.queue.write().await.insert(id.clone(), recv);
loop {
let length = self.watchers.read().await.len();
if length < max_concurrent {
let first_key = {
let queue = self.queue.read().await;
queue.first().map(|v| v.0).cloned()
};
if first_key == Some(id.clone()) {
self.queue.write().await.shift_remove(&id);
let response = self.insert_and_execute(id, func).await;
send.send_modify(|opt| {
opt.replace(
response
.clone()
.map(|v| v as Arc<dyn Any + Send + Sync>),
);
});
return response;
}
}
}
}
} else {
Err(Error::MaxConcurrent)
}
#[cfg(not(feature = "queue"))]
Err(Error::MaxConcurrent)
}
_ => self.insert_and_execute(id, func).await,
}
}
}
/// Fetches the amount of currently running tasks
pub async fn current_task_count(&self) -> usize {
self.watchers.read().await.len()
}
#[cfg(feature = "queue")]
/// Fetches the current length of the queue
pub async fn current_queue_len(&self) -> usize {
self.queue.read().await.len()
}
}
impl<Id: Hash + Clone + Eq> Default for CoalescionService<Id> {
fn default() -> Self {
Self::from_config(CoalescionServiceConfig::default())
}
}

View File

@@ -1,37 +1,40 @@
[package]
name = "revolt-config"
version = "0.8.4"
version = "0.13.7"
edition = "2021"
license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Configuration"
repository = "https://github.com/stoatchat/stoatchat"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
anyhow = ["dep:sentry-anyhow"]
report-macros = ["revolt-result"]
test = ["async-std"]
default = ["test"]
sentry = ["dep:sentry"]
test = ["tokio"]
default = ["sentry"]
[dependencies]
# Utility
config = "0.13.3"
cached = "0.44.0"
once_cell = "1.18.0"
config = { workspace = true }
cached = { workspace = true }
# Serde
serde = { version = "1", features = ["derive"] }
serde = { workspace = true }
# Async
futures-locks = "0.7.1"
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
futures-locks = { workspace = true }
tokio = { workspace = true, optional = true }
# Logging
log = "0.4.14"
pretty_env_logger = "0.4.0"
log = { workspace = true }
pretty_env_logger = { workspace = true }
# Sentry
sentry = "0.31.5"
sentry = { workspace = true, optional = true }
sentry-anyhow = { workspace = true, optional = true }
# Core
revolt-result = { version = "0.8.4", path = "../result", optional = true }
revolt-result = { workspace = true, optional = true }

View File

@@ -1,3 +1,5 @@
environment = "test"
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost/"
@@ -10,3 +12,12 @@ password = "rabbitpass"
[features]
webhooks_enabled = true
[api.smtp]
host = "localhost"
username = "smtp"
password = "smtp"
from_address = "development@stoat.chat"
reply_to = "support@stoat.chat"
port = 14025
use_tls = false

View File

@@ -1,4 +1,6 @@
production = false
disable_events_dont_use = false
environment = "dev"
[database]
# MongoDB connection URL
@@ -11,7 +13,7 @@ redis = "redis://redis/"
[hosts]
# Web locations of various services
# Defaults assume all services are reverse-proxied
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
# See https://github.com/stoatchat/self-hosted/blob/main/Caddyfile
#
# Remember to change these to https/wss where appropriate in production!
app = "http://local.revolt.chat"
@@ -22,17 +24,23 @@ january = "http://local.revolt.chat/january"
voso_legacy = ""
voso_legacy_ws = ""
[hosts.livekit]
[rabbit]
host = "rabbit"
port = 5672
username = "rabbituser"
password = "rabbitpass"
default_exchange = "revolt.default"
[rabbit.queues]
acks = "internal.ack"
[api]
[api.registration]
# Whether an invite should be required for registration
# See https://github.com/revoltchat/self-hosted#making-your-instance-invite-only
# See https://github.com/stoatchat/self-hosted#making-your-instance-invite-only
invite_only = false
[api.smtp]
@@ -46,6 +54,10 @@ from_address = "noreply@example.com"
# port = 587
# use_tls = true
[api.smtp.expiry]
expire_verification = 604800 # 3600 * 24 * 7
expire_password_reset = 86400 # 3600 * 24
expire_account_deletion = 86400 # 3600 * 24
[api.security]
# Authifier Shield API key
@@ -54,18 +66,33 @@ authifier_shield_key = ""
voso_legacy_token = ""
# Whether services are behind the Cloudflare network
trust_cloudflare = false
# easypwned endpoint
easypwned = ""
# Tenor API Key
tenor_key = ""
[api.security.captcha]
# hCaptcha configuration
hcaptcha_key = ""
hcaptcha_sitekey = ""
[api.security.shield]
host = ""
key = ""
[api.workers]
# Maximum concurrent connections (to proxy server)
max_concurrent_connections = 50
[api.users]
[api.livekit]
# How long to ring devices for when calling in dms/groups, in seconds
call_ring_duration = 30
[api.livekit.nodes]
[api.users]
# Minimum allowed length of usernames
min_username_length = 2
[pushd]
# this changes the names of the queues to not overlap
@@ -77,12 +104,18 @@ production = true
# Increasing this will resolve mentions faster, but will consume more memory while resolving.
mass_mention_chunk_size = 200
# How long pushd will cache resolved names for rendered message notifications.
# Increasing this will result in lower database usage, but may result in a situation where a user/channel/role name changes
# and the notifications still resolve to the old name.
render_cache_time = 60
# none of these should need changing
exchange = "revolt.notifications"
message_queue = "notifications.origin.message"
mass_mention_queue = "notifications.origin.mass_mention" # handles messages that contain role or everyone mentions
fr_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted
fr_received_queue = "notifications.ingest.fr_received" # friend request received
dm_call_queue = "notifications.ingest.dm_call" # direct message voice call
generic_queue = "notifications.ingest.generic" # generic messages (title + body)
ack_queue = "notifications.process.ack" # updates badges for apple devices
@@ -112,6 +145,8 @@ pkcs8 = ""
key_id = ""
team_id = ""
[january]
blocked_domains = []
[files]
# Encryption key for stored files
@@ -208,6 +243,10 @@ new_user_hours = 72
# (should be greater than any one file upload limit)
body_limit_size = 20_000_000
# If any userids are entered here, only those users will be able to create servers.
# Leave empty to allow all users to create servers
restrict_server_creation = []
[features.limits.new_user]
# Limits imposed on new users
@@ -226,6 +265,18 @@ message_attachments = 5
# Maximum number of servers the user can create/join
servers = 50
# Maximum audio frequency (Hz) in voice calls
voice_quality = 16000
# Whether the user can use video streams in voice calls
video = true
# Maximum resolution (width, height) of video streams in voice calls
video_resolution = [1080, 720]
# Minimum and maximum aspect ratio of video streams in voice calls
video_aspect_ratio = [0.3, 2.5]
[features.limits.new_user.file_upload_size_limit]
# Maximum file size limits (in bytes)
attachments = 20_000_000
@@ -253,6 +304,18 @@ message_attachments = 5
# Maximum number of servers the user can create/join
servers = 100
# Maximum audio frequency (Hz) in voice calls
voice_quality = 16000
# Whether the user can use video streams in voice calls
video = true
# Maximum resolution (width, height) of video streams in voice calls
video_resolution = [1280, 720]
# Minimum and maximum aspect ratio of video streams in voice calls
video_aspect_ratio = [0.3, 2.5]
[features.limits.default.file_upload_size_limit]
# Maximum file size limits (in bytes)
attachments = 20_000_000
@@ -267,10 +330,19 @@ 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 = ""
events = ""
voice_ingress = ""
files = ""
proxy = ""
pushd = ""
crond = ""
gifbox = ""

View File

@@ -1,14 +1,18 @@
use std::collections::HashMap;
#[cfg(feature = "test")]
use std::sync::OnceLock;
use std::{collections::HashMap, path::Path, sync::LazyLock};
use cached::proc_macro::cached;
use config::{Config, File, FileFormat};
use config::{Config, Environment, File, FileFormat};
use futures_locks::RwLock;
use once_cell::sync::Lazy;
use serde::Deserialize;
#[cfg(feature = "sentry")]
pub use sentry::{capture_error, capture_message, Level};
#[cfg(feature = "anyhow")]
pub use sentry_anyhow::capture_anyhow;
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! report_error {
( $expr: expr, $error: ident $( $tt:tt )? ) => {
@@ -23,7 +27,7 @@ macro_rules! report_error {
};
}
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! capture_internal_error {
( $expr: expr ) => {
@@ -34,7 +38,7 @@ macro_rules! capture_internal_error {
};
}
#[cfg(feature = "report-macros")]
#[cfg(all(feature = "report-macros", feature = "sentry"))]
#[macro_export]
macro_rules! report_internal_error {
( $expr: expr ) => {
@@ -59,27 +63,55 @@ static CONFIG_SEARCH_PATHS: [&str; 3] = [
"/Revolt.toml",
];
/// Path to search for test overrides
static TEST_OVERRIDE_PATH: &str = "Revolt.test-overrides.toml";
/// Configuration builder
static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
static CONFIG_BUILDER: LazyLock<RwLock<Config>> = LazyLock::new(|| {
RwLock::new({
let mut builder = Config::builder().add_source(File::from_str(
include_str!("../Revolt.toml"),
FileFormat::Toml,
));
let cwd = std::env::current_dir().unwrap();
let mut cwd: Option<&Path> = Some(&cwd);
while let Some(path) = cwd {
for config_path in CONFIG_SEARCH_PATHS {
let config_path = path.join(config_path);
if config_path.exists() {
builder = builder
.add_source(File::new(config_path.to_str().unwrap(), FileFormat::Toml));
}
}
cwd = path.parent();
}
if std::env::var("TEST_DB").is_ok() {
builder = builder.add_source(File::from_str(
include_str!("../Revolt.test.toml"),
FileFormat::Toml,
));
}
for path in CONFIG_SEARCH_PATHS {
if std::path::Path::new(path).exists() {
builder = builder.add_source(File::new(path, FileFormat::Toml));
// recursively search upwards for an overrides file (if there is one)
if let Ok(cwd) = std::env::current_dir() {
let mut path = Some(cwd.as_path());
while let Some(current_path) = path {
let target_path = current_path.join(TEST_OVERRIDE_PATH);
if target_path.exists() {
builder = builder
.add_source(File::new(target_path.to_str().unwrap(), FileFormat::Toml));
}
path = current_path.parent();
}
}
}
builder = builder.add_source(Environment::with_prefix("REVOLT").separator("__"));
builder.build().unwrap()
})
});
@@ -88,6 +120,12 @@ static CONFIG_BUILDER: Lazy<RwLock<Config>> = Lazy::new(|| {
pub struct Database {
pub mongodb: String,
pub redis: String,
pub redis_pubsub: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct RabbitQueues {
pub acks: String,
}
#[derive(Deserialize, Debug, Clone)]
@@ -96,6 +134,8 @@ pub struct Rabbit {
pub port: u16,
pub username: String,
pub password: String,
pub default_exchange: String,
pub queues: RabbitQueues,
}
#[derive(Deserialize, Debug, Clone)]
@@ -105,8 +145,7 @@ pub struct Hosts {
pub events: String,
pub autumn: String,
pub january: String,
pub voso_legacy: String,
pub voso_legacy_ws: String,
pub livekit: HashMap<String, String>,
}
#[derive(Deserialize, Debug, Clone)]
@@ -123,6 +162,19 @@ pub struct ApiSmtp {
pub reply_to: Option<String>,
pub port: Option<i32>,
pub use_tls: Option<bool>,
pub use_starttls: Option<bool>,
pub expiry: EmailExpiry,
}
/// Email expiration config
#[derive(Deserialize, Debug, Clone)]
pub struct EmailExpiry {
/// How long email verification codes should last for (in seconds)
pub expire_verification: i64,
/// How long password reset codes should last for (in seconds)
pub expire_password_reset: i64,
/// How long account deletion codes should last for (in seconds)
pub expire_account_deletion: i64,
}
#[derive(Deserialize, Debug, Clone)]
@@ -162,12 +214,20 @@ pub struct ApiSecurityCaptcha {
pub hcaptcha_sitekey: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSecurityShield {
pub host: String,
pub key: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiSecurity {
pub authifier_shield_key: String,
pub shield: ApiSecurityShield,
pub voso_legacy_token: String,
pub captcha: ApiSecurityCaptcha,
pub trust_cloudflare: bool,
pub easypwned: String,
pub tenor_key: String,
}
#[derive(Deserialize, Debug, Clone)]
@@ -175,9 +235,29 @@ pub struct ApiWorkers {
pub max_concurrent_connections: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiLiveKit {
pub call_ring_duration: usize,
pub nodes: HashMap<String, LiveKitNode>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct LiveKitNode {
pub url: String,
pub lat: f64,
pub lon: f64,
pub key: String,
pub secret: String,
// whether to hide the node in the nodes list
#[serde(default)]
pub private: bool,
}
#[derive(Deserialize, Debug, Clone)]
pub struct ApiUsers {
pub early_adopter_cutoff: Option<u64>
pub early_adopter_cutoff: Option<u64>,
pub min_username_length: usize,
}
#[derive(Deserialize, Debug, Clone)]
@@ -186,6 +266,7 @@ pub struct Api {
pub smtp: ApiSmtp,
pub security: ApiSecurity,
pub workers: ApiWorkers,
pub livekit: ApiLiveKit,
pub users: ApiUsers,
}
@@ -194,10 +275,12 @@ pub struct Pushd {
pub production: bool,
pub exchange: String,
pub mass_mention_chunk_size: usize,
pub render_cache_time: usize,
// Queues
pub message_queue: String,
pub mass_mention_queue: String,
pub dm_call_queue: String,
pub fr_accepted_queue: String,
pub fr_received_queue: String,
pub generic_queue: String,
@@ -228,6 +311,10 @@ impl Pushd {
self.get_routing_key(self.mass_mention_queue.clone())
}
pub fn get_dm_call_routing_key(&self) -> String {
self.get_routing_key(self.dm_call_queue.clone())
}
pub fn get_fr_accepted_routing_key(&self) -> String {
self.get_routing_key(self.fr_accepted_queue.clone())
}
@@ -241,6 +328,11 @@ impl Pushd {
}
}
#[derive(Deserialize, Debug, Clone)]
pub struct January {
pub blocked_domains: Vec<String>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FilesLimit {
pub min_file_size: usize,
@@ -285,6 +377,8 @@ pub struct GlobalLimits {
pub new_user_hours: usize,
pub body_limit_size: usize,
pub restrict_server_creation: Vec<String>,
}
#[derive(Deserialize, Debug, Clone)]
@@ -295,6 +389,10 @@ pub struct FeaturesLimits {
pub message_length: usize,
pub message_attachments: usize,
pub servers: usize,
pub voice_quality: u32,
pub video: bool,
pub video_resolution: [u32; 2],
pub video_aspect_ratio: [f32; 2],
pub file_upload_size_limit: HashMap<String, usize>,
}
@@ -310,6 +408,16 @@ pub struct FeaturesLimitsCollection {
pub roles: HashMap<String, FeaturesLimits>,
}
#[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)]
@@ -327,6 +435,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,
@@ -339,9 +448,12 @@ pub struct Features {
pub struct Sentry {
pub api: String,
pub events: String,
pub voice_ingress: String,
pub files: String,
pub proxy: String,
pub pushd: String,
pub crond: String,
pub gifbox: String,
}
#[derive(Deserialize, Debug, Clone)]
@@ -351,10 +463,13 @@ pub struct Settings {
pub hosts: Hosts,
pub api: Api,
pub pushd: Pushd,
pub january: January,
pub files: Files,
pub features: Features,
pub sentry: Sentry,
pub production: bool,
pub environment: String,
pub disable_events_dont_use: bool,
}
impl Settings {
@@ -380,19 +495,54 @@ pub async fn read() -> Config {
CONFIG_BUILDER.read().await.clone()
}
#[cached(time = 30)]
pub async fn config() -> Settings {
pub async fn config_no_cache() -> Settings {
let mut config = read().await.try_deserialize::<Settings>().unwrap();
// inject REDIS_URI for redis-kiss library
if std::env::var("REDIS_URI").is_err() {
std::env::set_var("REDIS_URI", config.database.redis.clone());
}
// auto-detect production nodes
if config.hosts.api.contains("https") && config.hosts.api.contains("revolt.chat") {
if config.hosts.api.contains("https")
&& (config.hosts.api.contains("revolt.chat") || config.hosts.api.contains("stoat.chat"))
{
config.production = true;
}
config
}
#[cached(time = 30)]
pub async fn config() -> Settings {
#[cfg(feature = "test")]
if let Some(overwrites) = CONFIG_OVERWRITES.get() {
return overwrites.clone();
}
config_no_cache().await
}
#[cfg(feature = "test")]
static CONFIG_OVERWRITES: OnceLock<Settings> = OnceLock::new();
/// Modify the config values for a test, this can only be called once
///
/// This will also fail if two or more tests are running in the same process and both try to modify the config,
/// This could happen if tests where run under `cargo test` instead of `nextest`.
#[cfg(feature = "test")]
pub async fn overwrite_config(f: impl FnOnce(&mut Settings)) {
let mut config = config_no_cache().await;
f(&mut config);
CONFIG_OVERWRITES.set(config).expect(
"Cannot overwrite config multiple times, make sure you are running tests through nextest.",
);
}
/// Configure logging and common Rust variables
#[cfg(feature = "sentry")]
pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry::ClientInitGuard> {
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info");
@@ -402,12 +552,6 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
std::env::set_var("ROCKET_ADDRESS", "0.0.0.0");
}
if std::env::var("REDIS_URL").is_err() {
// Configure redis-kiss library
let config = config().await;
std::env::set_var("REDIS_URI", config.database.redis);
}
pretty_env_logger::init();
log::info!("Starting {release}");
@@ -424,6 +568,7 @@ pub async fn setup_logging(release: &'static str, dsn: String) -> Option<sentry:
}
}
#[cfg(feature = "sentry")]
#[macro_export]
macro_rules! configure {
($application: ident) => {
@@ -441,7 +586,7 @@ macro_rules! configure {
mod tests {
use crate::init;
#[async_std::test]
#[tokio::test]
async fn it_works() {
init().await;
}

View File

@@ -1,10 +1,11 @@
[package]
name = "revolt-database"
version = "0.8.4"
version = "0.13.7"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: Database Implementation"
repository = "https://github.com/stoatchat/stoatchat"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -14,90 +15,99 @@ mongodb = ["dep:mongodb", "bson"]
# ... Other
tasks = ["isahc", "linkify", "url-escape"]
async-std-runtime = ["async-std"]
rocket-impl = ["rocket", "schemars", "revolt_okapi", "revolt_rocket_okapi"]
axum-impl = ["axum"]
tokio-runtime = ["tokio"]
rocket-impl = [
"rocket",
"schemars",
"revolt_okapi",
"revolt_rocket_okapi",
]
axum-impl = ["axum", "revolt-result/axum", "utoipa"]
redis-is-patched = ["revolt-presence/redis-is-patched"]
voice = ["livekit-api", "livekit-protocol", "livekit-runtime"]
# Default Features
default = ["mongodb", "async-std-runtime", "tasks"]
default = ["mongodb", "tokio-runtime", "tasks"]
[dependencies]
# Core
revolt-config = { version = "0.8.4", path = "../config", features = [
"report-macros",
] }
revolt-result = { version = "0.8.4", path = "../result" }
revolt-models = { version = "0.8.4", path = "../models", features = [
"validator",
] }
revolt-presence = { version = "0.8.4", path = "../presence" }
revolt-permissions = { version = "0.8.4", path = "../permissions", features = [
"serde",
"bson",
] }
revolt-parser = { version = "0.1.0", path = "../parser" }
revolt-config = { workspace = true, features = ["report-macros"] }
revolt-result = { workspace = true }
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 = "0.4"
lru = "0.11.0"
rand = "0.8.5"
ulid = "1.0.0"
nanoid = "0.4.0"
base64 = "0.21.3"
once_cell = "1.17"
indexmap = "1.9.1"
decancer = "1.6.2"
deadqueue = "0.2.4"
linkify = { optional = true, version = "0.8.1" }
url-escape = { optional = true, version = "0.1.1" }
validator = { version = "0.16", features = ["derive"] }
isahc = { optional = true, version = "1.7", features = ["json"] }
log = { workspace = true }
lru = { workspace = true }
rand = { workspace = true }
ulid = { workspace = true }
nanoid = { workspace = true }
base64 = { workspace = true }
once_cell = { workspace = true }
indexmap = { workspace = true }
decancer = { workspace = true }
deadqueue = { workspace = true }
linkify = { workspace = true, optional = true }
url-escape = { workspace = true, optional = true }
validator = { workspace = true, features = ["derive"] }
isahc = { workspace = true, features = ["json"], optional = true }
base32 = { workspace = true }
sha1 = { workspace = true }
# Serialisation
serde_json = "1"
revolt_optional_struct = "0.2.0"
serde = { version = "1", features = ["derive"] }
iso8601-timestamp = { version = "0.2.10", features = ["serde", "bson"] }
serde_json = { workspace = true }
revolt_optional_struct = { workspace = true }
serde = { workspace = true }
iso8601-timestamp = { workspace = true, features = ["serde", "bson"] }
# Events
redis-kiss = { version = "0.1.4" }
redis-kiss = { workspace = true, default-features = false, features = ["tokio-runtime"] }
# Database
bson = { optional = true, version = "2.1.0" }
mongodb = { optional = true, version = "3.1.0" }
bson = { workspace = true, optional = true }
mongodb = { workspace = true, optional = true }
# Database Migration
unicode-segmentation = "1.10.1"
regex = "1"
unicode-segmentation = { workspace = true }
regex = { workspace = true }
# Async Language Features
futures = "0.3.19"
async-lock = "2.8.0"
async-trait = "0.1.51"
async-recursion = "1.0.4"
futures = { workspace = true }
async-lock = { workspace = true }
async-trait = { workspace = true }
async-recursion = { workspace = true }
# Async
async-std = { version = "1.8.0", features = ["attributes"], optional = true }
tokio = { workspace = true, optional = true }
# Axum Impl
axum = { version = "0.7.5", optional = true }
axum = { workspace = true, optional = true }
utoipa = { workspace = true, features = ["axum_extras"], optional = true }
# Rocket Impl
schemars = { version = "0.8.8", optional = true }
rocket = { version = "0.5.1", default-features = false, features = [
"json",
], optional = true }
revolt_okapi = { version = "0.9.1", optional = true }
revolt_rocket_okapi = { version = "0.10.0", optional = true }
# Notifications
fcm_v1 = "0.3.0"
web-push = "0.10.0"
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier
authifier = { version = "1.0.10", features = ["rocket_impl"] }
schemars = { workspace = true, optional = true }
rocket = { workspace = true, features = ["json"], optional = true }
revolt_okapi = { workspace = true, optional = true }
revolt_rocket_okapi = { workspace = true, optional = true }
# RabbitMQ
amqprs = { version = "1.7.0" }
lapin = { workspace = true, features = ["tokio"] }
# Voice
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 }
# Security
totp-lite = { workspace = true }
rust-argon2 = { workspace = true }
# Email
lettre = { workspace = true }
handlebars = { workspace = true }
# Web Requests
reqwest = { workspace = true, features = ["json", "form"] }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,555 @@
this is an assorted list of known disposable email providers
#region nobody will ever have an email @example.com so we can safely block it
----
example.com
#region list provided by michenriksen at https://gist.github.com/michenriksen/8710649
----
0815.ru
0wnd.net
0wnd.org
10minutemail.co.za
10minutemail.com
123-m.com
1fsdfdsfsdf.tk
1pad.de
20minutemail.com
21cn.com
2fdgdfgdfgdf.tk
2prong.com
30minutemail.com
33mail.com
3trtretgfrfe.tk
4gfdsgfdgfd.tk
4warding.com
5ghgfhfghfgh.tk
6hjgjhgkilkj.tk
6paq.com
7tags.com
9ox.net
a-bc.net
agedmail.com
ama-trade.de
amilegit.com
amiri.net
amiriindustries.com
anonmails.de
anonymbox.com
antichef.com
antichef.net
antireg.ru
antispam.de
antispammail.de
armyspy.com
artman-conception.com
azmeil.tk
baxomale.ht.cx
beefmilk.com
bigstring.com
binkmail.com
bio-muesli.net
bobmail.info
bodhi.lawlita.com
bofthew.com
bootybay.de
boun.cr
bouncr.com
breakthru.com
brefmail.com
bsnow.net
bspamfree.org
bugmenot.com
bund.us
burstmail.info
buymoreplays.com
byom.de
c2.hu
casualdx.com
cek.pm
centermail.com
centermail.net
chammy.info
childsavetrust.org
chogmail.com
choicemail1.com
clixser.com
cmail.net
cmail.org
coldemail.info
cool.fr.nf
courriel.fr.nf
courrieltemporaire.com
crapmail.org
cust.in
cuvox.de
d3p.dk
dacoolest.com
dandikmail.com
dayrep.com
dcemail.com
deadaddress.com
deadspam.com
delikkt.de
despam.it
despammed.com
devnullmail.com
dfgh.net
digitalsanctuary.com
dingbone.com
disposableaddress.com
disposableemailaddresses.com
disposableinbox.com
dispose.it
dispostable.com
dodgeit.com
dodgit.com
donemail.ru
dontreg.com
dontsendmespam.de
drdrb.net
dump-email.info
dumpandjunk.com
dumpyemail.com
e-mail.com
e-mail.org
e4ward.com
easytrashmail.com
einmalmail.de
einrot.com
eintagsmail.de
emailgo.de
emailias.com
emaillime.com
emailsensei.com
emailtemporanea.com
emailtemporanea.net
emailtemporar.ro
emailtemporario.com.br
emailthe.net
emailtmp.com
emailwarden.com
emailx.at.hm
emailxfer.com
emeil.in
emeil.ir
emz.net
ero-tube.org
evopo.com
explodemail.com
eyepaste.com
fakeinbox.com
fakeinformation.com
fansworldwide.de
fantasymail.de
fightallspam.com
filzmail.com
fivemail.de
fleckens.hu
frapmail.com
friendlymail.co.uk
fuckingduh.com
fudgerub.com
fyii.de
garliclife.com
gehensiemirnichtaufdensack.de
get2mail.fr
getairmail.com
getmails.eu
getonemail.com
giantmail.de
girlsundertheinfluence.com
gishpuppy.com
gmial.com
goemailgo.com
gotmail.net
gotmail.org
gotti.otherinbox.com
great-host.in
greensloth.com
grr.la
gsrv.co.uk
guerillamail.biz
guerillamail.com
guerrillamail.biz
guerrillamail.com
guerrillamail.de
guerrillamail.info
guerrillamail.net
guerrillamail.org
guerrillamailblock.com
gustr.com
harakirimail.com
hat-geld.de
hatespam.org
herp.in
hidemail.de
hidzz.com
hmamail.com
hopemail.biz
ieh-mail.de
ikbenspamvrij.nl
imails.info
inbax.tk
inbox.si
inboxalias.com
inboxclean.com
inboxclean.org
instant-mail.de
ip6.li
irish2me.com
iwi.net
jetable.com
jetable.fr.nf
jetable.net
jetable.org
jnxjn.com
jourrapide.com
jsrsolutions.com
kasmail.com
kaspop.com
killmail.com
killmail.net
klassmaster.com
klzlk.com
koszmail.pl
kurzepost.de
lawlita.com
letthemeatspam.com
lhsdv.com
lifebyfood.com
link2mail.net
litedrop.com
lol.ovpn.to
lolfreak.net
lookugly.com
lortemail.dk
lr78.com
lroid.com
lukop.dk
m21.cc
mail-filter.com
mail-temporaire.fr
mail.mezimages.net
mail1a.de
mail21.cc
mail2rss.org
mail333.com
mailbidon.com
mailbiz.biz
mailblocks.com
mailbucket.org
mailcat.biz
mailcatch.com
mailde.de
mailde.info
maildrop.cc
maileimer.de
mailexpire.com
mailfa.tk
mailforspam.com
mailfreeonline.com
mailguard.me
mailin8r.com
mailinater.com
mailinator.com
mailinator.net
mailinator.org
mailinator2.com
mailincubator.com
mailismagic.com
mailme.lv
mailme24.com
mailmetrash.com
mailmoat.com
mailms.com
mailnesia.com
mailnull.com
mailorg.org
mailpick.biz
mailrock.biz
mailscrap.com
mailshell.com
mailsiphon.com
mailtemp.info
mailtome.de
mailtothis.com
mailtrash.net
mailtv.net
mailtv.tv
mailzilla.com
makemetheking.com
manybrain.com
mbx.cc
mega.zik.dj
meinspamschutz.de
meltmail.com
messagebeamer.de
mezimages.net
ministry-of-silly-walks.de
mintemail.com
misterpinball.de
moncourrier.fr.nf
monemail.fr.nf
monmail.fr.nf
monumentmail.com
mt2009.com
mt2014.com
mycleaninbox.net
mymail-in.net
mypacks.net
mypartyclip.de
myphantomemail.com
mysamp.de
mytempemail.com
mytempmail.com
mytrashmail.com
nabuma.com
neomailbox.com
nepwk.com
nervmich.net
nervtmich.net
netmails.com
netmails.net
neverbox.com
nice-4u.com
nincsmail.hu
nnh.com
no-spam.ws
noblepioneer.com
nomail.pw
nomail.xl.cx
nomail2me.com
nomorespamemails.com
nospam.ze.tc
nospam4.us
nospamfor.us
nospammail.net
notmailinator.com
nowhere.org
nowmymail.com
nurfuerspam.de
nus.edu.sg
objectmail.com
obobbo.com
odnorazovoe.ru
oneoffemail.com
onewaymail.com
onlatedotcom.info
online.ms
ordinaryamerican.net
otherinbox.com
ovpn.to
owlpic.com
pancakemail.com
pcusers.otherinbox.com
pjjkp.com
plexolan.de
poczta.onet.pl
politikerclub.de
poofy.org
pookmail.com
privacy.net
privatdemail.net
proxymail.eu
prtnx.com
putthisinyourspamdatabase.com
putthisinyourspamdatabase.com
quickinbox.com
rcpt.at
reallymymail.com
realtyalerts.ca
recode.me
recursor.net
reliable-mail.com
rhyta.com
rmqkr.net
royal.net
rtrtr.com
s0ny.net
safersignup.de
safetymail.info
safetypost.de
saynotospams.com
schafmail.de
schrott-email.de
secretemail.de
secure-mail.biz
senseless-entertainment.com
services391.com
sharklasers.com
shieldemail.com
shiftmail.com
shitmail.me
shitware.nl
shmeriously.com
shortmail.net
sinnlos-mail.de
slapsfromlastnight.com
slaskpost.se
smashmail.de
smellfear.com
snakemail.com
sneakemail.com
sneakmail.de
snkmail.com
sofimail.com
solvemail.info
sogetthis.com
soodonims.com
spam4.me
spamail.de
spamarrest.com
spambob.net
spambog.ru
spambox.us
spamcannon.com
spamcannon.net
spamcon.org
spamcorptastic.com
spamcowboy.com
spamcowboy.net
spamcowboy.org
spamday.com
spamex.com
spamfree.eu
spamfree24.com
spamfree24.de
spamfree24.org
spamgoes.in
spamgourmet.com
spamgourmet.net
spamgourmet.org
spamherelots.com
spamherelots.com
spamhereplease.com
spamhereplease.com
spamhole.com
spamify.com
spaml.de
spammotel.com
spamobox.com
spamslicer.com
spamspot.com
spamthis.co.uk
spamtroll.net
speed.1s.fr
spoofmail.de
stuffmail.de
super-auswahl.de
supergreatmail.com
supermailer.jp
superrito.com
superstachel.de
suremail.info
talkinator.com
teewars.org
teleworm.com
teleworm.us
temp-mail.org
temp-mail.ru
tempe-mail.com
tempemail.co.za
tempemail.com
tempemail.net
tempemail.net
tempinbox.co.uk
tempinbox.com
tempmail.eu
tempmaildemo.com
tempmailer.com
tempmailer.de
tempomail.fr
temporaryemail.net
temporaryforwarding.com
temporaryinbox.com
temporarymailaddress.com
tempthe.net
thankyou2010.com
thc.st
thelimestones.com
thisisnotmyrealemail.com
thismail.net
throwawayemailaddress.com
tilien.com
tittbit.in
tizi.com
tmailinator.com
toomail.biz
topranklist.de
tradermail.info
trash-mail.at
trash-mail.com
trash-mail.de
trash2009.com
trashdevil.com
trashemail.de
trashmail.at
trashmail.com
trashmail.de
trashmail.me
trashmail.net
trashmail.org
trashymail.com
trialmail.de
trillianpro.com
twinmail.de
tyldd.com
uggsrock.com
umail.net
uroid.com
us.af
venompen.com
veryrealemail.com
viditag.com
viralplays.com
vpn.st
vsimcard.com
vubby.com
wasteland.rfc822.org
webemail.me
weg-werf-email.de
wegwerf-emails.de
wegwerfadresse.de
wegwerfemail.com
wegwerfemail.de
wegwerfmail.de
wegwerfmail.info
wegwerfmail.net
wegwerfmail.org
wh4f.org
whyspam.me
willhackforfood.biz
willselfdestruct.com
winemaven.info
wronghead.com
www.e4ward.com
www.mailinator.com
wwwnew.eu
x.ip6.li
xagloo.com
xemaps.com
xents.com
xmaily.com
xoxy.net
yep.it
yogamaven.com
yopmail.com
yopmail.fr
yopmail.net
yourdomain.com
yuurok.com
z1p.biz
za.com
zehnminuten.de
zehnminutenmail.de
zippymail.info
zoemail.net
zomg.info
#region public emails provided by mail.tm
----
trythe.net
leadwizzer.com
metalunits.com
scpulse.com

View File

@@ -3,18 +3,21 @@
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Member",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "Member",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0002"
},
{
@@ -23,6 +26,9 @@
"channel_type": "Group",
"name": "My Group",
"owner": "__ID:0__",
"recipients": ["__ID:0__", "__ID:1__"]
"recipients": [
"__ID:0__",
"__ID:1__"
]
}
]
]

View File

@@ -3,18 +3,21 @@
"_object_type": "User",
"_id": "__ID:0__",
"username": "Owner",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:1__",
"username": "Moderator",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
"_object_type": "User",
"_id": "__ID:2__",
"username": "User",
"last_acknowledged_policy_change": "2025-06-07T04:04:48+0000",
"discriminator": "0001"
},
{
@@ -39,17 +42,21 @@
"_id": "__ID:4__",
"owner": "__ID:0__",
"name": "Server",
"channels": ["__ID:3__"],
"channels": [
"__ID:3__"
],
"roles": {
"__ID:5__": {
"_id": "__ID:5__",
"name": "Moderator",
"permissions": {
"a": 545270208,
"d": 0
},
"rank": 3
"rank": 1
},
"__ID:6__": {
"_id": "__ID:6__",
"name": "Owner",
"permissions": {
"a": 0,
@@ -66,7 +73,9 @@
"user": "__ID:0__",
"server": "__ID:4__"
},
"roles": ["__ID:6__"],
"roles": [
"__ID:6__"
],
"joined_at": 1698318340195
},
{
@@ -75,7 +84,9 @@
"user": "__ID:1__",
"server": "__ID:4__"
},
"roles": ["__ID:5__"],
"roles": [
"__ID:5__"
],
"joined_at": 1698318340195
},
{
@@ -86,4 +97,4 @@
},
"joined_at": 1698318340195
}
]
]

View File

@@ -1,30 +1,79 @@
use std::collections::HashSet;
use std::sync::Arc;
use crate::events::rabbit::*;
use crate::User;
use amqprs::channel::BasicPublishArguments;
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<Channel>,
friend_request_received: Arc<Channel>,
generic_message: Arc<Channel>,
message_sent: Arc<Channel>,
mass_mention_message_sent: Arc<Channel>,
ack_notification_message: Arc<Channel>,
dm_call_updated: Arc<Channel>,
process_ack: Arc<Channel>,
#[allow(unused)]
connection: Connection,
channel: Channel,
connection: Arc<Connection>,
}
impl AMQP {
pub fn new(connection: Connection, channel: Channel) -> AMQP {
AMQP {
pub async fn new(connection: Arc<Connection>) -> 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() -> Self {
let config = revolt_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"),
);
Self::new(connection).await
}
async fn create_channel(connection: &Connection) -> Arc<Channel> {
Arc::new(
connection
.create_channel()
.await
.expect("Failed to create channel"),
)
}
pub async fn friend_request_accepted(
&self,
accepted_request_user: &User,
@@ -42,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(
@@ -75,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(
@@ -112,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(
@@ -155,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(
@@ -190,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,
@@ -222,22 +277,106 @@ 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
/// Used to send an update about a DM call, eg. start or end of a call.
/// Recipients can be used to narrow the scope of recipients, otherwise all recipients will be notified.
/// `ended` refers to the ringing period, not necessarily the call itself.
pub async fn dm_call_updated(
&self,
initiator_id: &str,
channel_id: &str,
started_at: Option<&str>,
ended: bool,
recipients: Option<Vec<String>>,
) -> Result<(), AMQPError> {
let config = revolt_config::config().await;
let payload = InternalDmCallPayload {
payload: DmCallPayload {
initiator_id: initiator_id.to_string(),
channel_id: channel_id.to_string(),
started_at: started_at.map(|f| f.to_string()),
ended,
},
recipients,
};
let payload = to_string(&payload).unwrap();
debug!(
"Sending dm call update payload on channel {}: {}",
config.pushd.get_dm_call_routing_key(),
payload
);
self.dm_call_updated
.basic_publish(
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?;
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(())
}
}

View File

@@ -1,17 +1,12 @@
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
use authifier::config::Captcha;
use authifier::config::EmailVerificationConfig;
use authifier::config::ResolveIp;
use authifier::config::SMTPSettings;
use authifier::config::Shield;
use authifier::config::Template;
use authifier::config::Templates;
use authifier::Authifier;
use rand::Rng;
use revolt_config::config;
#[cfg(feature = "mongodb")]
pub use self::mongodb::*;
pub use self::reference::*;
@@ -24,17 +19,20 @@ pub enum DatabaseInfo {
/// Use the mock database
Reference,
/// Connect to MongoDB
#[cfg(feature = "mongodb")]
MongoDb { uri: String, database_name: String },
/// Use existing MongoDB connection
#[cfg(feature = "mongodb")]
MongoDbFromClient(::mongodb::Client, String),
}
/// Database
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum Database {
/// Mock database
Reference(ReferenceDb),
/// MongoDB database
#[cfg(feature = "mongodb")]
MongoDb(MongoDb),
}
@@ -44,7 +42,7 @@ impl DatabaseInfo {
pub async fn connect(self) -> Result<Database, String> {
let config = config().await;
Ok(match self {
match self {
DatabaseInfo::Auto => {
if std::env::var("TEST_DB").is_ok() {
DatabaseInfo::Test(format!(
@@ -52,16 +50,20 @@ impl DatabaseInfo {
rand::thread_rng().gen_range(1_000_000..10_000_000)
))
.connect()
.await?
.await
} else if !config.database.mongodb.is_empty() {
DatabaseInfo::MongoDb {
#[cfg(feature = "mongodb")]
return DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name: "revolt".to_string(),
}
.connect()
.await?
.await;
#[cfg(not(feature = "mongodb"))]
return Err("MongoDB not enabled.".to_string());
} else {
DatabaseInfo::Reference.connect().await?
DatabaseInfo::Reference.connect().await
}
}
DatabaseInfo::Test(database_name) => {
@@ -69,137 +71,35 @@ impl DatabaseInfo {
.expect("`TEST_DB` environment variable should be set to REFERENCE or MONGODB")
.as_str()
{
"REFERENCE" => DatabaseInfo::Reference.connect().await?,
"REFERENCE" => DatabaseInfo::Reference.connect().await,
"MONGODB" => {
DatabaseInfo::MongoDb {
#[cfg(feature = "mongodb")]
return DatabaseInfo::MongoDb {
uri: config.database.mongodb,
database_name,
}
.connect()
.await?
.await;
#[cfg(not(feature = "mongodb"))]
return Err("MongoDB not enabled.".to_string());
}
_ => unreachable!("must specify REFERENCE or MONGODB"),
}
}
DatabaseInfo::Reference => Database::Reference(Default::default()),
DatabaseInfo::Reference => Ok(Database::Reference(Default::default())),
#[cfg(feature = "mongodb")]
DatabaseInfo::MongoDb { uri, database_name } => {
let client = ::mongodb::Client::with_uri_str(uri)
.await
.map_err(|_| "Failed to init db connection.".to_string())?;
Database::MongoDb(MongoDb(client, database_name))
Ok(Database::MongoDb(MongoDb(client, database_name)))
}
#[cfg(feature = "mongodb")]
DatabaseInfo::MongoDbFromClient(client, database_name) => {
Database::MongoDb(MongoDb(client, database_name))
Ok(Database::MongoDb(MongoDb(client, database_name)))
}
})
}
}
impl Database {
/// Create an Authifier reference
pub async fn to_authifier(self) -> Authifier {
let config = config().await;
let mut auth_config = authifier::Config {
email_verification: if !config.api.smtp.host.is_empty() {
EmailVerificationConfig::Enabled {
smtp: SMTPSettings {
from: config.api.smtp.from_address,
host: config.api.smtp.host,
username: config.api.smtp.username,
password: config.api.smtp.password,
reply_to: Some(
config
.api
.smtp
.reply_to
.unwrap_or("support@revolt.chat".into()),
),
port: config.api.smtp.port,
use_tls: config.api.smtp.use_tls,
},
expiry: Default::default(),
templates: if config.production {
Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("../../templates/verify.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: Some(include_str!("../../templates/verify.html").into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("../../templates/reset.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: Some(include_str!("../../templates/reset.html").into()),
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: Some(include_str!("../../templates/deletion.html").into()),
},
welcome: None,
}
} else {
Templates {
verify: Template {
title: "Verify your account.".into(),
text: include_str!("../../templates/verify.whitelabel.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: None,
},
reset: Template {
title: "Reset your password.".into(),
text: include_str!("../../templates/reset.whitelabel.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: None,
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.whitelabel.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: None,
},
welcome: None,
}
},
}
} else {
EmailVerificationConfig::Disabled
},
..Default::default()
};
auth_config.invite_only = config.api.registration.invite_only;
if !config.api.security.captcha.hcaptcha_key.is_empty() {
auth_config.captcha = Captcha::HCaptcha {
secret: config.api.security.captcha.hcaptcha_key,
};
}
if !config.api.security.authifier_shield_key.is_empty() {
auth_config.shield = Shield::Enabled {
api_key: config.api.security.authifier_shield_key,
strict: false,
};
}
if config.api.security.trust_cloudflare {
auth_config.resolve_ip = ResolveIp::Cloudflare;
}
Authifier {
database: match self {
Database::Reference(_) => Default::default(),
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
},
config: auth_config,
event_channel: Some(crate::tasks::authifier_relay::sender()),
}
}
}

View File

@@ -10,8 +10,8 @@ use serde::de::DeserializeOwned;
use serde::Serialize;
database_derived!(
#[cfg(feature = "mongodb")]
/// MongoDB implementation
#[derive(Debug)]
pub struct MongoDb(pub ::mongodb::Client, pub String);
);

View File

@@ -4,13 +4,13 @@ use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
UserSettings, Webhook,
MemberCompositeKey, Message, PolicyChange, RatelimitEvent, Report, Server, ServerBan, Snapshot,
User, UserSettings, Webhook, Account, AccountInvite, Session, MFATicket
};
database_derived!(
/// Reference implementation
#[derive(Default)]
#[derive(Default, Debug)]
pub struct ReferenceDb {
pub bots: Arc<Mutex<HashMap<String, Bot>>>,
pub channels: Arc<Mutex<HashMap<String, Channel>>>,
@@ -21,6 +21,7 @@ database_derived!(
pub file_hashes: Arc<Mutex<HashMap<String, FileHash>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub messages: Arc<Mutex<HashMap<String, Message>>>,
pub policy_changes: Arc<Mutex<HashMap<String, PolicyChange>>>,
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
pub user_settings: Arc<Mutex<HashMap<String, UserSettings>>>,
pub users: Arc<Mutex<HashMap<String, User>>>,
@@ -29,5 +30,9 @@ database_derived!(
pub servers: Arc<Mutex<HashMap<String, Server>>>,
pub safety_reports: Arc<Mutex<HashMap<String, Report>>>,
pub safety_snapshots: Arc<Mutex<HashMap<String, Snapshot>>>,
pub accounts: Arc<Mutex<HashMap<String, Account>>>,
pub account_invites: Arc<Mutex<HashMap<String, AccountInvite>>>,
pub sessions: Arc<Mutex<HashMap<String, Session>>>,
pub tickets: Arc<Mutex<HashMap<String, MFATicket>>>,
}
);

View File

@@ -1,15 +1,16 @@
use authifier::AuthifierEvent;
use revolt_result::Error;
use serde::{Deserialize, Serialize};
use revolt_models::v0::{
AppendMessage, Channel, ChannelUnread, Emoji, FieldsChannel, FieldsMember, FieldsMessage,
FieldsRole, FieldsServer, FieldsUser, FieldsWebhook, Member, MemberCompositeKey, Message,
PartialChannel, PartialMember, PartialMessage, PartialRole, PartialServer, PartialUser,
PartialWebhook, RemovalIntention, Report, Server, User, UserSettings, 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;
use crate::{Account, Database, Session};
/// Ping Packet
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -20,16 +21,33 @@ pub enum Ping {
}
/// Fields provided in Ready payload
#[derive(PartialEq)]
pub enum ReadyPayloadFields {
Users,
Servers,
Channels,
Members,
Emoji,
#[derive(PartialEq, Debug, Clone, Deserialize)]
pub struct ReadyPayloadFields {
pub users: bool,
pub servers: bool,
pub channels: bool,
pub members: bool,
pub emojis: bool,
pub voice_states: bool,
pub user_settings: Vec<String>,
pub channel_unreads: bool,
pub policy_changes: bool,
}
UserSettings(Vec<String>),
ChannelUnreads,
impl Default for ReadyPayloadFields {
fn default() -> Self {
Self {
users: true,
servers: true,
channels: true,
members: true,
emojis: true,
voice_states: true,
user_settings: Vec::new(),
channel_unreads: false,
policy_changes: true,
}
}
}
/// Protocol Events
@@ -37,9 +55,13 @@ pub enum ReadyPayloadFields {
#[serde(tag = "type")]
pub enum EventV1 {
/// Multiple events
Bulk { v: Vec<EventV1> },
Bulk {
v: Vec<EventV1>,
},
/// Error event
Error { data: Error },
Error {
data: Error,
},
/// Successfully authenticated
Authenticated,
@@ -57,15 +79,22 @@ pub enum EventV1 {
members: Option<Vec<Member>>,
#[serde(skip_serializing_if = "Option::is_none")]
emojis: Option<Vec<Emoji>>,
#[serde(skip_serializing_if = "Option::is_none")]
voice_states: Option<Vec<ChannelVoiceState>>,
#[serde(skip_serializing_if = "Option::is_none")]
user_settings: Option<UserSettings>,
#[serde(skip_serializing_if = "Option::is_none")]
channel_unreads: Option<Vec<ChannelUnread>>,
#[serde(skip_serializing_if = "Option::is_none")]
policy_changes: Option<Vec<PolicyChange>>,
},
/// Ping response
Pong { data: Ping },
Pong {
data: Ping,
},
/// New message
Message(Message),
@@ -86,7 +115,10 @@ pub enum EventV1 {
},
/// Delete message
MessageDelete { id: String, channel: String },
MessageDelete {
id: String,
channel: String,
},
/// New reaction to a message
MessageReact {
@@ -112,7 +144,10 @@ pub enum EventV1 {
},
/// Bulk delete messages
BulkMessageDelete { channel: String, ids: Vec<String> },
BulkMessageDelete {
channel: String,
ids: Vec<String>,
},
/// New server
ServerCreate {
@@ -120,6 +155,7 @@ pub enum EventV1 {
server: Server,
channels: Vec<Channel>,
emojis: Vec<Emoji>,
voice_states: Vec<ChannelVoiceState>,
},
/// Update existing server
@@ -131,7 +167,9 @@ pub enum EventV1 {
},
/// Delete server
ServerDelete { id: String },
ServerDelete {
id: String,
},
/// Update existing server member
ServerMemberUpdate {
@@ -142,7 +180,13 @@ pub enum EventV1 {
},
/// User joins server
ServerMemberJoin { id: String, user: String },
ServerMemberJoin {
id: String,
// Deprecated: use member.id.user
#[deprecated = "Use member.id.user instead"]
user: String,
member: Member,
},
/// User left server
ServerMemberLeave {
@@ -161,7 +205,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<String>,
},
/// Update existing user
UserUpdate {
@@ -173,9 +226,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
///
@@ -186,12 +245,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),
@@ -207,19 +277,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 {
@@ -239,10 +323,56 @@ pub enum EventV1 {
},
/// Delete webhook
WebhookDelete { id: String },
WebhookDelete {
id: String,
},
/// Auth events
Auth(AuthifierEvent),
CreateAccount {
account: Account,
},
CreateSession {
session: Session,
},
DeleteSession {
user_id: String,
session_id: String,
},
DeleteAllSessions {
user_id: String,
exclude_session_id: Option<String>,
},
/// Voice events
VoiceChannelJoin {
id: String,
state: UserVoiceState,
},
VoiceChannelLeave {
id: String,
user: String,
},
VoiceChannelMove {
user: String,
from: String,
to: String,
state: UserVoiceState,
},
UserVoiceStateUpdate {
id: String,
channel_id: String,
data: PartialUserVoiceState,
},
UserMoveVoiceChannel {
node: String,
from: String,
to: String,
token: String,
},
/// User's active slowmodes
UserSlowmodes {
slowmodes: Vec<ChannelSlowmode>,
},
}
impl EventV1 {

View File

@@ -37,6 +37,20 @@ pub struct GenericPayload {
pub user: User,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct DmCallPayload {
pub initiator_id: String,
pub channel_id: String,
pub started_at: Option<String>,
pub ended: bool,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct InternalDmCallPayload {
pub payload: DmCallPayload,
pub recipients: Option<Vec<String>>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data")]
#[allow(clippy::large_enum_variant)]
@@ -46,6 +60,7 @@ pub enum PayloadKind {
FRReceived(FRReceivedPayload),
BadgeUpdate(usize),
Generic(GenericPayload),
DmCallStartEnd(DmCallPayload),
}
#[derive(Serialize, Deserialize)]
@@ -63,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<String>,
pub server_id: Option<String>,
}

View File

@@ -1,8 +1,8 @@
use serde::Deserialize;
use serde::{Serialize, Deserialize};
use super::client::Ping;
#[derive(Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum ClientMessage {
Authenticate { token: String },

View File

@@ -25,6 +25,9 @@ pub use mongodb;
#[macro_use]
extern crate bson;
#[cfg(not(feature = "tokio-runtime"))]
compile_error!("tokio-runtime feature must be enabled.");
#[macro_export]
#[cfg(debug_assertions)]
macro_rules! query {
@@ -103,11 +106,16 @@ pub mod util;
pub use models::*;
pub mod events;
#[cfg(feature = "tasks")]
pub mod tasks;
mod amqp;
pub use amqp::amqp::AMQP;
#[cfg(feature = "voice")]
pub mod voice;
/// Utility function to check if a boolean value is false
pub fn if_false(t: &bool) -> bool {
!t

View File

@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,25 @@
use crate::{if_false, Database};
use revolt_result::Result;
auto_derived_partial!(
/// Account invite ticket
pub struct AccountInvite {
/// Invite code
#[serde(rename = "_id")]
pub id: String,
/// Whether this invite ticket has been used
#[serde(skip_serializing_if = "if_false", default)]
pub used: bool,
/// User ID that this invite was claimed by
#[serde(skip_serializing_if = "Option::is_none")]
pub claimed_by: Option<String>,
},
"PartialAccountInvite"
);
impl AccountInvite {
/// Save model
pub async fn save(&self, db: &Database) -> Result<()> {
db.save_account_invite(self).await
}
}

View File

@@ -0,0 +1,16 @@
use revolt_result::Result;
use crate::AccountInvite;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAccountInvites: Sync + Send {
/// Find invite by id
async fn fetch_account_invite(&self, id: &str) -> Result<AccountInvite>;
/// Save invite
async fn save_account_invite(&self, invite: &AccountInvite) -> Result<()>;
}

View File

@@ -0,0 +1,31 @@
use crate::{AbstractAccountInvites, AccountInvite, MongoDb};
use bson::to_document;
use mongodb::options::UpdateOptions;
use revolt_result::Result;
const COL: &str = "account_invites";
#[async_trait]
impl AbstractAccountInvites for MongoDb {
/// Find invite by id
async fn fetch_account_invite(&self, id: &str) -> Result<AccountInvite> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(InvalidInvite))
}
/// Save invite
async fn save_account_invite(&self, invite: &AccountInvite) -> Result<()> {
self.col::<AccountInvite>(COL)
.update_one(
doc! {
"_id": &invite.id
},
doc! {
"$set": to_document(invite).map_err(|_| create_database_error!("to_document", COL))?,
},
)
.with_options(UpdateOptions::builder().upsert(true).build())
.await
.map_err(|_| create_database_error!("upsert_one", COL))
.map(|_| ())
}
}

View File

@@ -0,0 +1,21 @@
use crate::{AbstractAccountInvites, AccountInvite, ReferenceDb};
use revolt_result::Result;
#[async_trait]
impl AbstractAccountInvites for ReferenceDb {
/// Find invite by id
async fn fetch_account_invite(&self, id: &str) -> Result<AccountInvite> {
let invites = self.account_invites.lock().await;
invites
.get(id)
.cloned()
.ok_or_else(|| create_error!(InvalidInvite))
}
/// Save invite
async fn save_account_invite(&self, invite: &AccountInvite) -> Result<()> {
let mut invites = self.account_invites.lock().await;
invites.insert(invite.id.to_string(), invite.clone());
Ok(())
}
}

View File

@@ -0,0 +1,22 @@
use axum::{extract::{FromRef, FromRequestParts}, http::request::Parts};
use revolt_result::{Error, Result};
use crate::{Account, Database, Session};
#[async_trait]
impl<S> FromRequestParts<S> for Account
where
Database: FromRef<S>,
S: Send + Sync
{
type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self> {
let db = Database::from_ref(state);
let session = Session::from_request_parts(parts, state).await?;
db.fetch_account(&session.user_id).await
}
}

View File

@@ -0,0 +1,11 @@
#[cfg(feature = "axum-impl")]
mod axum;
mod model;
mod ops;
#[cfg(feature = "rocket-impl")]
mod rocket;
#[cfg(feature = "rocket-impl")]
mod schema;
pub use model::*;
pub use ops::*;

View File

@@ -0,0 +1,656 @@
use iso8601_timestamp::{Duration, Timestamp};
use nanoid::nanoid;
use revolt_config::config;
use revolt_result::Result;
use serde_json::json;
use crate::{
events::client::EventV1,
util::{
email::{email_templates, normalise_email, send_email},
password::hash_password,
},
Database, MFATicket, Session,
};
use revolt_models::v0;
auto_derived_partial!(
/// Account model
pub struct Account {
/// Unique Id
#[serde(rename = "_id")]
pub id: String,
/// User's email
pub email: String,
/// Normalised email
///
/// (see https://github.com/insertish/authifier/#how-does-authifier-work)
pub email_normalised: String,
/// Argon2 hashed password
pub password: String,
/// Whether the account is disabled
#[serde(default)]
pub disabled: bool,
/// Email verification status
pub verification: EmailVerification,
/// Password reset information
pub password_reset: Option<PasswordReset>,
/// Account deletion information
pub deletion: Option<DeletionInfo>,
/// Account lockout
pub lockout: Option<Lockout>,
/// Multi-factor authentication information
pub mfa: MultiFactorAuthentication,
},
"PartialAccount"
);
auto_derived!(
/// Email verification status
#[serde(tag = "status")]
pub enum EmailVerification {
/// Account is verified
Verified,
/// Pending email verification
Pending { token: String, expiry: Timestamp },
/// Moving to a new email
Moving {
new_email: String,
token: String,
expiry: Timestamp,
},
}
/// Password reset information
pub struct PasswordReset {
/// Token required to change password
pub token: String,
/// Time at which this token expires
pub expiry: Timestamp,
}
/// Account deletion information
#[serde(tag = "status")]
pub enum DeletionInfo {
/// The user must confirm deletion by email
WaitingForVerification { token: String, expiry: Timestamp },
/// The account is scheduled for deletion
Scheduled { after: Timestamp },
/// This account was deleted
Deleted,
}
/// Lockout information
pub struct Lockout {
/// Attempt counter
pub attempts: i32,
/// Time at which this lockout expires
pub expiry: Option<Timestamp>,
}
/// MFA configuration
#[derive(Default)]
pub struct MultiFactorAuthentication {
/// Allow password-less email OTP login
/// (1-Factor)
// #[serde(skip_serializing_if = "is_false", default)]
// pub enable_email_otp: bool,
/// Allow trusted handover
/// (1-Factor)
// #[serde(skip_serializing_if = "is_false", default)]
// pub enable_trusted_handover: bool,
/// Allow email MFA
/// (2-Factor)
// #[serde(skip_serializing_if = "is_false", default)]
// pub enable_email_mfa: bool,
/// TOTP MFA token, enabled if present
/// (2-Factor)
#[serde(skip_serializing_if = "Totp::is_empty", default)]
pub totp_token: Totp,
/// Security Key MFA token, enabled if present
/// (2-Factor)
// #[serde(skip_serializing_if = "Option::is_none")]
// pub security_key_token: Option<String>,
/// Recovery codes
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub recovery_codes: Vec<String>,
}
/// MFA method
#[derive(Hash)]
pub enum MFAMethod {
Password,
Recovery,
Totp,
}
#[derive(Default)]
#[serde(tag = "status")]
pub enum Totp {
/// Disabled
#[default]
Disabled,
/// Waiting for user activation
Pending { secret: String },
/// Required on account
Enabled { secret: String },
}
);
impl MultiFactorAuthentication {
// Check whether MFA is in-use
pub fn is_active(&self) -> bool {
matches!(self.totp_token, Totp::Enabled { .. })
}
// Check whether there are still usable recovery codes
pub fn has_recovery(&self) -> bool {
!self.recovery_codes.is_empty()
}
// Get available MFA methods
pub fn get_methods(&self) -> Vec<MFAMethod> {
if let Totp::Enabled { .. } = self.totp_token {
let mut methods = vec![MFAMethod::Totp];
if self.has_recovery() {
methods.push(MFAMethod::Recovery);
}
methods
} else {
vec![MFAMethod::Password]
}
}
// Generate new recovery codes
pub fn generate_recovery_codes(&mut self) {
static ALPHABET: [char; 32] = [
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z',
];
let mut codes = vec![];
for _ in 1..=10 {
codes.push(format!(
"{}-{}",
nanoid!(5, &ALPHABET),
nanoid!(5, &ALPHABET)
));
}
self.recovery_codes = codes;
}
// Generate new TOTP secret
pub fn generate_new_totp_secret(&mut self) -> Result<String> {
if let Totp::Enabled { .. } = self.totp_token {
return Err(create_error!(OperationFailed));
}
let secret: [u8; 10] = rand::random();
let secret = base32::encode(base32::Alphabet::RFC4648 { padding: false }, &secret);
self.totp_token = Totp::Pending {
secret: secret.clone(),
};
Ok(secret)
}
/// Enable TOTP using a given MFA response
pub fn enable_totp(&mut self, response: v0::MFAResponse) -> Result<()> {
if let v0::MFAResponse::Totp { totp_code } = response {
let code = self.totp_token.generate_code()?;
if code == totp_code {
let mut totp = Totp::Disabled;
std::mem::swap(&mut totp, &mut self.totp_token);
if let Totp::Pending { secret } = totp {
self.totp_token = Totp::Enabled { secret };
Ok(())
} else {
Err(create_error!(OperationFailed))
}
} else {
Err(create_error!(InvalidToken))
}
} else {
Err(create_error!(InvalidToken))
}
}
}
impl Totp {
/// Whether TOTP information is empty
pub fn is_empty(&self) -> bool {
matches!(self, Totp::Disabled)
}
/// Whether TOTP is disabled
pub fn is_disabled(&self) -> bool {
!matches!(self, Totp::Enabled { .. })
}
// Generate a TOTP code from secret
pub fn generate_code(&self) -> Result<String> {
if let Totp::Enabled { secret } | Totp::Pending { secret } = &self {
let seconds: u64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
Ok(totp_lite::totp_custom::<totp_lite::Sha1>(
totp_lite::DEFAULT_STEP,
6,
&base32::decode(base32::Alphabet::RFC4648 { padding: false }, secret)
.expect("valid base32 secret"),
seconds,
))
} else {
Err(create_error!(OperationFailed))
}
}
}
impl Account {
/// Save model
pub async fn save(&self, db: &Database) -> Result<()> {
db.save_account(self).await
}
/// Create a new account
pub async fn new(
db: &Database,
email: String,
plaintext_password: String,
verify_email: bool,
) -> Result<Account> {
// Get a normalised representation of the user's email
let email_normalised = normalise_email(email.clone());
// Try to find an existing account
if let Some(mut account) = db
.fetch_account_by_normalised_email(&email_normalised)
.await?
{
// Resend account verification or send password reset
if let EmailVerification::Pending { .. } = &account.verification {
account.start_email_verification(db).await?;
} else {
account.start_password_reset(db, true).await?;
}
Ok(account)
} else {
// Hash the user's password
let password = hash_password(plaintext_password)?;
// Create a new account
let mut account = Account {
id: ulid::Ulid::new().to_string(),
email,
email_normalised,
password,
disabled: false,
verification: EmailVerification::Verified,
password_reset: None,
deletion: None,
lockout: None,
mfa: Default::default(),
};
// Send email verification
if verify_email {
account.start_email_verification(db).await?;
} else {
account.save(db).await?;
}
// Create and push event
EventV1::CreateAccount {
account: account.clone(),
}
.global()
.await;
Ok(account)
}
}
/// Create a new session
pub async fn create_session(&self, db: &Database, name: String) -> Result<Session> {
let config = config().await;
let session = Session {
id: ulid::Ulid::new().to_string(),
token: nanoid!(64),
user_id: self.id.clone(),
name,
last_seen: Timestamp::now_utc(),
origin: Some(config.environment),
subscription: None,
};
// Save to database
db.save_session(&session).await?;
// Create and push event
EventV1::CreateSession {
session: session.clone(),
}
.global()
.await;
Ok(session)
}
/// Send account verification email
pub async fn start_email_verification(&mut self, db: &Database) -> Result<()> {
let config = config().await;
if !config.api.smtp.host.is_empty() {
let templates = email_templates().await;
let token = nanoid!(32);
let url = format!("{}{}", templates.verify.url, token);
send_email(
&config.api.smtp,
self.email.clone(),
&templates.verify,
json!({
"email": self.email.clone(),
"url": url
}),
)?;
self.verification = EmailVerification::Pending {
token,
expiry: Timestamp::now_utc()
.checked_add(Duration::seconds(
config.api.smtp.expiry.expire_verification,
))
.unwrap(),
};
} else {
self.verification = EmailVerification::Verified;
}
self.save(db).await
}
/// Send account verification to new email
pub async fn start_email_move(&mut self, db: &Database, new_email: String) -> Result<()> {
// This method should and will never be called on an unverified account,
// but just validate this just in case.
if let EmailVerification::Pending { .. } = self.verification {
return Err(create_error!(UnverifiedAccount));
}
let config = config().await;
if !config.api.smtp.host.is_empty() {
let templates = email_templates().await;
let token = nanoid!(32);
let url = format!("{}{}", templates.verify.url, token);
send_email(
&config.api.smtp,
new_email.clone(),
&templates.verify,
json!({
"email": self.email.clone(),
"url": url
}),
)?;
self.verification = EmailVerification::Moving {
new_email,
token,
expiry: Timestamp::now_utc()
.checked_add(Duration::seconds(
config.api.smtp.expiry.expire_verification,
))
.unwrap(),
};
} else {
self.email_normalised = normalise_email(new_email.clone());
self.email = new_email;
}
self.save(db).await
}
/// Send password reset email
pub async fn start_password_reset(
&mut self,
db: &Database,
existing_account: bool,
) -> Result<()> {
let config = config().await;
if !config.api.smtp.host.is_empty() {
let templates = email_templates().await;
let template = if existing_account {
&templates.reset_existing
} else {
&templates.reset
};
let token = nanoid!(32);
let url = format!("{}{}", template.url, token);
send_email(
&config.api.smtp,
self.email.clone(),
template,
json!({
"email": self.email.clone(),
"url": url
}),
)?;
self.password_reset = Some(PasswordReset {
token,
expiry: Timestamp::now_utc()
.checked_add(Duration::seconds(
config.api.smtp.expiry.expire_password_reset,
))
.unwrap(),
});
} else {
return Err(create_error!(OperationFailed));
}
self.save(db).await
}
/// Begin account deletion process by sending confirmation email
///
/// If email verification is not on, the account will be marked for deletion instantly
pub async fn start_account_deletion(&mut self, db: &Database) -> Result<()> {
let config = config().await;
if !config.api.smtp.host.is_empty() {
let templates = email_templates().await;
let token = nanoid!(32);
let url = format!("{}{}", templates.deletion.url, token);
send_email(
&config.api.smtp,
self.email.clone(),
&templates.deletion,
json!({
"email": self.email.clone(),
"url": url
}),
)?;
self.deletion = Some(DeletionInfo::WaitingForVerification {
token,
expiry: Timestamp::now_utc()
.checked_add(Duration::seconds(
config.api.smtp.expiry.expire_password_reset,
))
.unwrap(),
});
self.save(db).await
} else {
self.schedule_deletion(db).await
}
}
/// Verify a user's password is correct
pub fn verify_password(&self, plaintext_password: &str) -> Result<()> {
argon2::verify_encoded(&self.password, plaintext_password.as_bytes())
.map(|v| {
if v {
Ok(())
} else {
Err(create_error!(InvalidCredentials))
}
})
// To prevent user enumeration, we should ignore
// the error and pretend the password is wrong.
.map_err(|_| create_error!(InvalidCredentials))?
}
/// Validate an MFA response
pub async fn consume_mfa_response(
&mut self,
db: &Database,
response: v0::MFAResponse,
ticket: Option<MFATicket>,
) -> Result<()> {
let allowed_methods = self.mfa.get_methods();
match response {
v0::MFAResponse::Password { password } => {
if allowed_methods.contains(&MFAMethod::Password) {
self.verify_password(&password)
} else {
Err(create_error!(DisallowedMFAMethod))
}
}
v0::MFAResponse::Totp { totp_code } => {
if allowed_methods.contains(&MFAMethod::Totp) {
if let Totp::Enabled { .. } = &self.mfa.totp_token {
// Use TOTP code at generation if applicable
if let Some(ticket) = ticket {
if let Some(code) = ticket.last_totp_code {
if code == totp_code {
return Ok(());
}
}
}
// Otherwise read current TOTP token
if self.mfa.totp_token.generate_code()? == totp_code {
Ok(())
} else {
Err(create_error!(InvalidToken))
}
} else {
unreachable!()
}
} else {
Err(create_error!(DisallowedMFAMethod))
}
}
v0::MFAResponse::Recovery { recovery_code } => {
if allowed_methods.contains(&MFAMethod::Recovery) {
if let Some(index) = self
.mfa
.recovery_codes
.iter()
.position(|x| x == &recovery_code)
{
self.mfa.recovery_codes.remove(index);
self.save(db).await
} else {
Err(create_error!(InvalidToken))
}
} else {
Err(create_error!(DisallowedMFAMethod))
}
}
}
}
/// Delete all sessions for an account
pub async fn delete_all_sessions(
&self,
db: &Database,
exclude_session_id: Option<String>,
) -> Result<()> {
db.delete_all_sessions(&self.id, exclude_session_id.clone())
.await?;
// Create and push event
EventV1::DeleteAllSessions {
user_id: self.id.clone(),
exclude_session_id,
}
.private(self.id.clone())
.await;
Ok(())
}
/// Disable an account
pub async fn disable(&mut self, db: &Database) -> Result<()> {
self.disabled = true;
self.delete_all_sessions(db, None).await?;
self.save(db).await
}
/// Schedule an account for deletion
pub async fn schedule_deletion(&mut self, db: &Database) -> Result<()> {
self.deletion = Some(DeletionInfo::Scheduled {
after: Timestamp::now_utc()
.checked_add(Duration::weeks(1))
.unwrap(),
});
self.disable(db).await
}
/// Removes all information from the account and marks it as fully deleted
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
self.email = format!("Deleted User {}", &self.id);
self.email_normalised = format!("Deleted User {}", &self.id);
self.deletion = Some(DeletionInfo::Deleted);
self.save(db).await?;
Ok(())
}
}

View File

@@ -0,0 +1,34 @@
use revolt_result::Result;
use crate::Account;
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAccounts: Sync + Send {
/// Find account by id
async fn fetch_account(&self, id: &str) -> Result<Account>;
/// Find account by normalised email
async fn fetch_account_by_normalised_email(
&self,
normalised_email: &str,
) -> Result<Option<Account>>;
/// Find account with active pending email verification
async fn fetch_account_with_email_verification(&self, token: &str) -> Result<Account>;
/// Find account with active password reset
async fn fetch_account_with_password_reset(&self, token: &str) -> Result<Account>;
/// Find account with active deletion token
async fn fetch_account_with_deletion_token(&self, token: &str) -> Result<Account>;
/// Find accounts which are due to be deleted
async fn fetch_accounts_due_for_deletion(&self) -> Result<Vec<Account>>;
// Save account
async fn save_account(&self, account: &Account) -> Result<()>;
}

View File

@@ -0,0 +1,118 @@
use crate::{AbstractAccounts, Account, MongoDb};
use bson::{to_bson, to_document};
use iso8601_timestamp::Timestamp;
use mongodb::options::{Collation, CollationStrength, FindOneOptions, UpdateOptions};
use revolt_result::Result;
const COL: &str = "accounts";
#[async_trait]
impl AbstractAccounts for MongoDb {
/// Find account by id
async fn fetch_account(&self, id: &str) -> Result<Account> {
query!(self, find_one_by_id, COL, id)?.ok_or_else(|| create_error!(UnknownUser))
}
/// Find account by normalised email
async fn fetch_account_by_normalised_email(
&self,
normalised_email: &str,
) -> Result<Option<Account>> {
query!(
self,
find_one_with_options,
COL,
doc! {
"email_normalised": normalised_email
},
FindOneOptions::builder()
.collation(
Collation::builder()
.locale("en")
.strength(CollationStrength::Secondary)
.build(),
)
.build()
)
}
/// Find account with active pending email verification
async fn fetch_account_with_email_verification(&self, token: &str) -> Result<Account> {
query!(
self,
find_one,
COL,
doc! {
"verification.token": token,
"verification.expiry": {
"$gte": to_bson(&Timestamp::now_utc()).unwrap()
}
}
)?
.ok_or_else(|| create_error!(InvalidToken))
}
/// Find account with active password reset
async fn fetch_account_with_password_reset(&self, token: &str) -> Result<Account> {
query!(
self,
find_one,
COL,
doc! {
"password_reset.token": token,
"password_reset.expiry": {
"$gte": to_bson(&Timestamp::now_utc()).unwrap()
}
}
)?
.ok_or_else(|| create_error!(InvalidToken))
}
/// Find account with active deletion token
async fn fetch_account_with_deletion_token(&self, token: &str) -> Result<Account> {
query!(
self,
find_one,
COL,
doc! {
"deletion.token": token,
"deletion.expiry": {
"$gte": to_bson(&Timestamp::now_utc()).unwrap()
}
}
)?
.ok_or_else(|| create_error!(InvalidToken))
}
/// Find accounts which are due to be deleted
async fn fetch_accounts_due_for_deletion(&self) -> Result<Vec<Account>> {
query!(
self,
find,
COL,
doc! {
"deletion.status": "Scheduled",
"deletion.after": {
"$lte": to_bson(&Timestamp::now_utc()).unwrap()
}
}
)
}
// Save account
async fn save_account(&self, account: &Account) -> Result<()> {
self.col::<Account>(COL)
.update_one(
doc! {
"_id": &account.id
},
doc! {
"$set": to_document(account).map_err(|_| create_database_error!("to_document", COL))?
},
)
.with_options(UpdateOptions::builder().upsert(true).build())
.await
.map_err(|_| create_database_error!("find_one", COL))
.map(|_| ())
}
}

View File

@@ -0,0 +1,99 @@
use crate::{AbstractAccounts, Account, DeletionInfo, EmailVerification, ReferenceDb};
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
#[async_trait]
impl AbstractAccounts for ReferenceDb {
/// Find account by id
async fn fetch_account(&self, id: &str) -> Result<Account> {
let accounts = self.accounts.lock().await;
accounts
.get(id)
.cloned()
.ok_or_else(|| create_error!(UnknownUser))
}
/// Find account by normalised email
async fn fetch_account_by_normalised_email(
&self,
normalised_email: &str,
) -> Result<Option<Account>> {
let accounts = self.accounts.lock().await;
Ok(accounts
.values()
.find(|account| account.email_normalised == normalised_email)
.cloned())
}
/// Find account with active pending email verification
async fn fetch_account_with_email_verification(&self, token_to_match: &str) -> Result<Account> {
let accounts = self.accounts.lock().await;
accounts
.values()
.find(|account| match &account.verification {
EmailVerification::Pending { token, .. }
| EmailVerification::Moving { token, .. } => token == token_to_match,
_ => false,
})
.cloned()
.ok_or_else(|| create_error!(InvalidToken))
}
/// Find account with active password reset
async fn fetch_account_with_password_reset(&self, token: &str) -> Result<Account> {
let accounts = self.accounts.lock().await;
accounts
.values()
.find(|account| {
if let Some(reset) = &account.password_reset {
reset.token == token
} else {
false
}
})
.cloned()
.ok_or_else(|| create_error!(InvalidToken))
}
/// Find account with active deletion token
async fn fetch_account_with_deletion_token(&self, token_to_match: &str) -> Result<Account> {
let accounts = self.accounts.lock().await;
accounts
.values()
.find(|account| {
if let Some(DeletionInfo::WaitingForVerification { token, .. }) = &account.deletion
{
token == token_to_match
} else {
false
}
})
.cloned()
.ok_or_else(|| create_error!(InvalidToken))
}
/// Find accounts which are due to be deleted
async fn fetch_accounts_due_for_deletion(&self) -> Result<Vec<Account>> {
let now = Timestamp::now_utc();
let accounts = self.accounts.lock().await;
Ok(accounts
.values()
.filter(|account| {
if let Some(DeletionInfo::Scheduled { after }) = &account.deletion {
after <= &now
} else {
false
}
})
.cloned()
.collect())
}
// Save account
async fn save_account(&self, account: &Account) -> Result<()> {
let mut accounts = self.accounts.lock().await;
accounts.insert(account.id.to_string(), account.clone());
Ok(())
}
}

View File

@@ -0,0 +1,32 @@
use crate::{Account, Database, Session};
use revolt_result::Error;
use rocket::{
http::Status,
request::{FromRequest, Outcome},
Request,
};
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Account {
type Error = Error;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.guard::<Session>().await {
Outcome::Success(session) => {
if let Ok(account) = request
.rocket()
.state::<Database>()
.expect("`Database`")
.fetch_account(&session.user_id)
.await
{
Outcome::Success(account)
} else {
Outcome::Error((Status::InternalServerError, create_error!(InternalError)))
}
}
Outcome::Forward(_) => unreachable!(),
Outcome::Error(err) => Outcome::Error(err),
}
}
}

View File

@@ -0,0 +1,32 @@
use revolt_okapi::openapi3::{SecurityScheme, SecuritySchemeData};
use revolt_rocket_okapi::{
gen::OpenApiGenerator,
request::{OpenApiFromRequest, RequestHeaderInput},
};
use crate::Account;
impl<'r> OpenApiFromRequest<'r> for Account {
fn from_request_input(
_gen: &mut OpenApiGenerator,
_name: String,
_required: bool,
) -> revolt_rocket_okapi::Result<RequestHeaderInput> {
let mut requirements = schemars::Map::new();
requirements.insert("Session Token".to_owned(), vec![]);
Ok(RequestHeaderInput::Security(
"Session Token".to_owned(),
SecurityScheme {
data: SecuritySchemeData::ApiKey {
name: "x-session-token".to_owned(),
location: "header".to_owned(),
},
description: Some("Used to authenticate as a user.".to_owned()),
extensions: schemars::Map::new(),
},
requirements,
))
}
}

View File

@@ -11,7 +11,7 @@ auto_derived!(
#[cfg(test)]
mod tests {
#[async_std::test]
#[tokio::test]
async fn migrate() {
database_test!(|db| async move {
// Initialise the database

View File

@@ -1,3 +1,4 @@
#[cfg(feature = "mongodb")]
mod mongodb;
mod reference;

View File

@@ -64,6 +64,10 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create user_settings collection.");
db.create_collection("policy_changes")
.await
.expect("Failed to create policy_changes collection.");
db.create_collection("safety_reports")
.await
.expect("Failed to create safety_reports collection.");
@@ -94,6 +98,18 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create pubsub collection.");
db.create_collection("sessions")
.await
.expect("Failed to create sessions collection.");
db.create_collection("account_invites")
.await
.expect("Failed to create account_invites collection.");
db.create_collection("mfa_tickets")
.await
.expect("Failed to create mfa_tickets collection.");
db.run_command(doc! {
"createIndexes": "users",
"indexes": [
@@ -259,5 +275,89 @@ pub async fn create_database(db: &MongoDb) {
.await
.expect("Failed to create ratelimit_events index.");
db.run_command(doc! {
"createIndexes": "accounts",
"indexes": [
{
"key": {
"email": 1
},
"name": "email",
"unique": true,
"collation": {
"locale": "en",
"strength": 2
}
},
{
"key": {
"email_normalised": 1
},
"name": "email_normalised",
"unique": true,
"collation": {
"locale": "en",
"strength": 2
}
},
{
"key": {
"verification.token": 1
},
"name": "email_verification"
},
{
"key": {
"password_reset.token": 1
},
"name": "password_reset"
},
{
"key": {
"deletion.token": 1
},
"name": "account_deletion"
}
]
})
.await
.unwrap();
db.run_command(doc! {
"createIndexes": "sessions",
"indexes": [
{
"key": {
"token": 1
},
"name": "token",
"unique": true
},
{
"key": {
"user_id": 1
},
"name": "user_id"
}
]
})
.await
.unwrap();
db.run_command(doc! {
"createIndexes": "mfa_tickets",
"indexes": [
{
"key": {
"token": 1
},
"name": "token",
"unique": true
}
]
})
.await
.unwrap();
info!("Created database.");
}

Some files were not shown because too many files have changed in this diff Show More