Compare commits

..
5 Commits
Author SHA1 Message Date
jmug 35cfb9a489 chore(images): Build all images.
Docker / base (push) Successful in 32m54s
Rust build, test, and generate specification / Rust project (push) Failing after 28m20s
Docker / Build autumn image (push) Has been skipped
Docker / Build bonfire image (push) Has been skipped
Docker / Build crond image (push) Has been skipped
Docker / Build january image (push) Has been skipped
Docker / Build pushd image (push) Has been skipped
Docker / Build delta image (push) Has been skipped
Docker / base (release) Successful in 21m18s
Docker / Build bonfire image (release) Successful in 2m5s
Docker / Build autumn image (release) Successful in 2m10s
Docker / Build crond image (release) Successful in 1m59s
Docker / Build january image (release) Successful in 2m10s
Docker / Build delta image (release) Successful in 1m51s
Docker / Build pushd image (release) Successful in 2m12s
2026-06-27 11:31:14 -07:00
jmug 7297a64856 chore(images): s/handmade-revolt/handmade-revolt-backend/
Docker / Build pushd image (push) Has been cancelled
Docker / base (push) Has been cancelled
2026-06-27 11:28:34 -07:00
jmug 720cd46879 chore(pushd/ci): Build image pushd image in-fork.
Docker / Build pushd image (push) Has been cancelled
Docker / base (push) Has been cancelled
Rust build, test, and generate specification / Rust project (push) Has been cancelled
Docker / base (release) Successful in 21m35s
Docker / Build pushd image (release) Successful in 1m32s
2026-06-27 00:34:53 -07:00
jmug 7d099d5ae7 feat(pushd): Guard against malformed vapid web pushes. 2026-06-27 00:12:11 -07:00
jmug 8847b3c5ba feat(pushd): Fix typo in inbound notification handling. 2026-06-27 00:06:05 -07:00
100 changed files with 4800 additions and 5884 deletions

No files matched your search

+1 -1
View File
@@ -1,2 +1,2 @@
[profile.default] [profile.default]
slow-timeout = { period = "5s", terminate-after = 10 } slow-timeout = { period = "3s", terminate-after = 2 }
+103 -183
View File
@@ -1,193 +1,113 @@
name: Docker Test & Publish name: Docker
on: on:
push: push:
tags: branches:
- "*" - "handmade"
pull_request: pull_request:
branches: branches:
- "main" - "handmade"
paths: release:
- "Dockerfile" types:
- published
permissions: permissions:
contents: read contents: read
packages: write
jobs: jobs:
base: # Builds the shared base image: the whole Rust workspace compiled once.
name: Test base image build # Every service image (currently only pushd) is derived from this so the
runs-on: ubuntu-latest # workspace is built a single time per release. On non-release events this
if: github.event_name == 'pull_request' # still runs as a compile check, but nothing is pushed.
steps: base:
# Configure build environment runs-on: ubuntu-latest
- name: Checkout steps:
uses: actions/checkout@v3 - name: Checkout
- name: Set up Docker Buildx uses: actions/checkout@v4
uses: docker/setup-buildx-action@v2 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Resolve registry host and owner
id: registry
# Owner is lowercased: Docker repository names must be lowercase.
run: |
echo "host=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_OUTPUT"
echo "owner=$(echo "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Base image ref
id: base
run: echo "image=${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/handmade-revolt-backend-base" >> "$GITHUB_OUTPUT"
- name: Login to registry
uses: docker/login-action@v3
if: github.event_name == 'release'
with:
registry: ${{ steps.registry.outputs.host }}
username: ${{ secrets.PACKAGE_PUBLISH_USERNAME }}
password: ${{ secrets.PACKAGE_PUBLISH_TOKEN }}
- name: Build base image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: ${{ github.event_name == 'release' }}
platforms: linux/amd64
# Pinned to the commit so the publish job below consumes this
# exact base.
tags: |
${{ steps.base.outputs.image }}:latest
${{ steps.base.outputs.image }}:${{ github.sha }}
# Build the image # Builds individual service images from the base. Only runs on release.
- name: Build base image publish:
uses: docker/build-push-action@v4 needs: [base]
with: runs-on: ubuntu-latest
context: . if: github.event_name == 'release'
push: false strategy:
platforms: linux/amd64,linux/arm64 matrix:
tags: ghcr.io/${{ github.repository_owner }}/base:latest include:
cache-from: type=gha,scope=buildx-base-multi-arch - { project: pushd, path: crates/daemons/pushd, image: handmade-revolt-backend-pushd }
cache-to: type=gha,scope=buildx-base-multi-arch,mode=max - { project: delta, path: crates/delta, image: handmade-revolt-backend-server }
- { project: bonfire, path: crates/bonfire, image: handmade-revolt-backend-bonfire }
publish: - { project: autumn, path: crates/services/autumn, image: handmade-revolt-backend-autumn }
runs-on: self-hosted - { project: january, path: crates/services/january, image: handmade-revolt-backend-january }
if: github.event_name != 'pull_request' - { project: crond, path: crates/daemons/crond, image: handmade-revolt-backend-crond }
name: Publish Docker images name: Build ${{ matrix.project }} image
steps: steps:
# Configure build environment - name: Checkout
- name: Checkout uses: actions/checkout@v4
uses: actions/checkout@v3 - name: Set up Docker Buildx
- name: Set up Docker Buildx uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v2 - name: Resolve registry host and owner
id: registry
# Build the image # Owner is lowercased: Docker repository names must be lowercase,
- name: Build base image # but the org (e.g. HMC) may be uppercase.
uses: docker/build-push-action@v4 run: |
with: echo "host=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_OUTPUT"
context: . echo "owner=$(echo "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
push: false - name: Docker meta
platforms: linux/amd64,linux/arm64 id: meta
tags: base:latest uses: docker/metadata-action@v5
with:
# Authenticate with Docker Hub and GHCR images: ${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/${{ matrix.image }}
- name: Login to DockerHub tags: |
uses: docker/login-action@v2 type=semver,pattern={{version}}
with: type=semver,pattern={{major}}.{{minor}}
registry: docker.io type=raw,value=latest
username: ${{ secrets.DOCKERHUB_USERNAME }} env:
password: ${{ secrets.DOCKERHUB_TOKEN }} DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
- name: Login to registry
- name: Login to Github Container Registry uses: docker/login-action@v3
uses: docker/login-action@v2 with:
with: registry: ${{ steps.registry.outputs.host }}
registry: ghcr.io username: ${{ secrets.PACKAGE_PUBLISH_USERNAME }}
username: ${{ github.actor }} password: ${{ secrets.PACKAGE_PUBLISH_TOKEN }}
password: ${{ secrets.GITHUB_TOKEN }} - name: Build and publish
uses: docker/build-push-action@v6
# revoltchat/server with:
- name: Docker meta context: .
id: meta-delta file: ${{ matrix.path }}/Dockerfile
uses: docker/metadata-action@v4 push: true
with: platforms: linux/amd64
images: | build-args: |
docker.io/revoltchat/server BASE_IMAGE=${{ steps.registry.outputs.host }}/${{ steps.registry.outputs.owner }}/handmade-revolt-backend-base:${{ github.sha }}
ghcr.io/revoltchat/server tags: ${{ steps.meta.outputs.tags }}
- name: Publish annotations: ${{ steps.meta.outputs.annotations }}
uses: docker/build-push-action@v4 labels: ${{ steps.meta.outputs.labels }}
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/delta/Dockerfile
tags: ${{ steps.meta-delta.outputs.tags }}
build-args: |
BASE_IMAGE=base:latest
labels: ${{ steps.meta-delta.outputs.labels }}
# revoltchat/bonfire
- name: Docker meta
id: meta-bonfire
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/bonfire
ghcr.io/revoltchat/bonfire
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/bonfire/Dockerfile
tags: ${{ steps.meta-bonfire.outputs.tags }}
build-args: |
BASE_IMAGE=base:latest
labels: ${{ steps.meta-bonfire.outputs.labels }}
# revoltchat/autumn
- name: Docker meta
id: meta-autumn
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/autumn
ghcr.io/revoltchat/autumn
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/services/autumn/Dockerfile
tags: ${{ steps.meta-autumn.outputs.tags }}
build-args: |
BASE_IMAGE=base:latest
labels: ${{ steps.meta-autumn.outputs.labels }}
# revoltchat/january
- name: Docker meta
id: meta-january
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/january
ghcr.io/revoltchat/january
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/services/january/Dockerfile
tags: ${{ steps.meta-january.outputs.tags }}
build-args: |
BASE_IMAGE=base:latest
labels: ${{ steps.meta-january.outputs.labels }}
# revoltchat/crond
- name: Docker meta
id: meta-crond
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/crond
ghcr.io/revoltchat/crond
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/daemons/crond/Dockerfile
tags: ${{ steps.meta-crond.outputs.tags }}
build-args: |
BASE_IMAGE=base:latest
labels: ${{ steps.meta-crond.outputs.labels }}
# revoltchat/pushd
- name: Docker meta
id: meta-pushd
uses: docker/metadata-action@v4
with:
images: |
docker.io/revoltchat/pushd
ghcr.io/revoltchat/pushd
- name: Publish
uses: docker/build-push-action@v4
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
file: crates/daemons/pushd/Dockerfile
tags: ${{ steps.meta-pushd.outputs.tags }}
build-args: |
BASE_IMAGE=base:latest
labels: ${{ steps.meta-pushd.outputs.labels }}
-2
View File
@@ -5,8 +5,6 @@ compose.override.yml
target target
.data .data
.env .env
.venv/
venv/
.vercel .vercel
.DS_Store .DS_Store
Generated
+1758 -2336
View File
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -11,7 +11,10 @@ members = [
] ]
[patch.crates-io] [patch.crates-io]
redis23 = { package = "redis", version = "0.23.3", git = "https://github.com/revoltchat/redis-rs", rev = "523b2937367e17bd0073722bf6e23d06042cb4e4" } redis22 = { package = "redis", version = "0.22.3", git = "https://github.com/revoltchat/redis-rs", rev = "1a41faf356fd21aebba71cea7eb7eb2653e5f0ef" }
redis23 = { package = "redis", version = "0.23.1", git = "https://github.com/revoltchat/redis-rs", rev = "f8ca28ab85da59d2ccde526b4d2fb390eff5a5f9" }
# authifier = { package = "authifier", version = "1.0.8", path = "../authifier/crates/authifier" }
# rocket_authifier = { package = "rocket_authifier", version = "1.0.8", path = "../authifier/crates/rocket_authifier" }
# I'm 99% sure this is overloading the GitHub worker # I'm 99% sure this is overloading the GitHub worker
# hence builds have been failing since, let's just # hence builds have been failing since, let's just
+1 -1
View File
@@ -1,5 +1,5 @@
# ⚠️ This configuration is intended for development environment. # ⚠️ This configuration is intended for development environment.
# If you'd like to override anything, create a Revolt.overrides.toml # If you'd like to override anything, create a Revolt.override.toml
[database] [database]
# MongoDB connection URL # MongoDB connection URL
+4 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-nodejs-bindings" name = "revolt-nodejs-bindings"
version = "0.8.4" version = "0.8.2"
description = "Node.js bindings for the Revolt software" description = "Node.js bindings for the Revolt software"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
license = "MIT" license = "MIT"
@@ -20,6 +20,6 @@ serde = { version = "1", features = ["derive"] }
async-std = "1.12.0" async-std = "1.12.0"
revolt-config = { version = "0.8.4", path = "../../core/config" } revolt-config = { version = "0.8.2", path = "../../core/config" }
revolt-result = { version = "0.8.4", path = "../../core/result" } revolt-result = { version = "0.8.2", path = "../../core/result" }
revolt-database = { version = "0.8.4", path = "../../core/database" } revolt-database = { version = "0.8.2", path = "../../core/database" }
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.8.4" version = "0.8.2"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -36,12 +36,12 @@ async-std = { version = "1.8.0", features = [
] } ] }
# core # core
authifier = { version = "1.0.10" } authifier = { version = "1.0.9" }
revolt-result = { path = "../core/result" } revolt-result = { path = "../core/result" }
revolt-models = { path = "../core/models" } revolt-models = { path = "../core/models" }
revolt-config = { path = "../core/config" } revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database" } revolt-database = { path = "../core/database" }
revolt-permissions = { version = "0.8.4", path = "../core/permissions" } revolt-permissions = { version = "0.8.2", path = "../core/permissions" }
revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] } revolt-presence = { path = "../core/presence", features = ["redis-is-patched"] }
# redis # redis
+5 -5
View File
@@ -1,6 +1,5 @@
use std::collections::HashSet; use std::collections::HashSet;
use futures::future::join_all;
use revolt_database::{ use revolt_database::{
events::client::{EventV1, ReadyPayloadFields}, events::client::{EventV1, ReadyPayloadFields},
util::permissions::DatabasePermissionQuery, util::permissions::DatabasePermissionQuery,
@@ -199,12 +198,13 @@ impl State {
.collect(); .collect();
// Make all users appear from our perspective. // Make all users appear from our perspective.
let mut users: Vec<v0::User> = join_all(users let mut users: Vec<v0::User> = users
.into_iter() .into_iter()
.map(|other_user| async { .map(|other_user| {
let is_online = online_ids.contains(&other_user.id); let is_online = online_ids.contains(&other_user.id);
other_user.into_known(&user, is_online).await other_user.into_known(&user, is_online)
})).await; })
.collect();
// Make sure we see our own user correctly. // Make sure we see our own user correctly.
users.push(user.into_self(true).await); users.push(user.into_self(true).await);
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-config" name = "revolt-config"
version = "0.8.4" version = "0.8.2"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -34,4 +34,4 @@ pretty_env_logger = "0.4.0"
sentry = "0.31.5" sentry = "0.31.5"
# Core # Core
revolt-result = { version = "0.8.4", path = "../result", optional = true } revolt-result = { version = "0.8.2", path = "../result", optional = true }
-3
View File
@@ -7,6 +7,3 @@ host = "127.0.0.1"
port = 5672 port = 5672
username = "rabbituser" username = "rabbituser"
password = "rabbitpass" password = "rabbitpass"
[features]
webhooks_enabled = true
+5 -20
View File
@@ -1,5 +1,3 @@
production = false
[database] [database]
# MongoDB connection URL # MongoDB connection URL
# Defaults to the container name specified in self-hosted # Defaults to the container name specified in self-hosted
@@ -64,28 +62,19 @@ hcaptcha_sitekey = ""
# Maximum concurrent connections (to proxy server) # Maximum concurrent connections (to proxy server)
max_concurrent_connections = 50 max_concurrent_connections = 50
[api.users]
[pushd] [pushd]
# this changes the names of the queues to not overlap # this changes the names of the queues to not overlap
# prod/beta if they happen to be on the same exchange/instance. # prod/beta if they happen to be on the same exchange/instance.
# Usually they have to be, so that messages sent from one or the other get sent to everyone # Usually they have to be, so that messages sent from one or the other get sent to everyone
production = true production = true
# Changes how many users are processed in each chunk when resolving role/everyone mentions.
# Increasing this will resolve mentions faster, but will consume more memory while resolving.
mass_mention_chunk_size = 200
# none of these should need changing # none of these should need changing
exchange = "revolt.notifications" exchange = "revolt.notifications"
message_queue = "notifications.origin.message" 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_accepted_queue = "notifications.ingest.fr_accepted" # friend request accepted fr_received_queue = "notifications.ingest.fr_received" # friend request received
fr_received_queue = "notifications.ingest.fr_received" # friend request received generic_queue = "notifications.ingest.generic" # generic messages (title + body)
generic_queue = "notifications.ingest.generic" # generic messages (title + body) ack_queue = "notifications.process.ack" # updates badges for apple devices
ack_queue = "notifications.process.ack" # updates badges for apple devices
[pushd.vapid] [pushd.vapid]
queue = "notifications.outbound.vapid" queue = "notifications.outbound.vapid"
@@ -182,13 +171,9 @@ default_bucket = "revolt-uploads"
[features] [features]
# Bucket to upload to by default
# Feature gate options # Feature gate options
webhooks_enabled = false webhooks_enabled = false
# Enable push notifications for mass pings (everyone, online, roles)
# When false this will still ping in-client but will not send notifications from pushd
mass_mentions_send_notifications = true
# Can role/everyone pings be used at all
mass_mentions_enabled = true
[features.limits] [features.limits]
+1 -26
View File
@@ -175,29 +175,19 @@ pub struct ApiWorkers {
pub max_concurrent_connections: usize, pub max_concurrent_connections: usize,
} }
#[derive(Deserialize, Debug, Clone)]
pub struct ApiUsers {
pub early_adopter_cutoff: Option<u64>
}
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct Api { pub struct Api {
pub registration: ApiRegistration, pub registration: ApiRegistration,
pub smtp: ApiSmtp, pub smtp: ApiSmtp,
pub security: ApiSecurity, pub security: ApiSecurity,
pub workers: ApiWorkers, pub workers: ApiWorkers,
pub users: ApiUsers,
} }
#[derive(Deserialize, Debug, Clone)] #[derive(Deserialize, Debug, Clone)]
pub struct Pushd { pub struct Pushd {
pub production: bool, pub production: bool,
pub exchange: String, pub exchange: String,
pub mass_mention_chunk_size: usize,
// Queues
pub message_queue: String, pub message_queue: String,
pub mass_mention_queue: String,
pub fr_accepted_queue: String, pub fr_accepted_queue: String,
pub fr_received_queue: String, pub fr_received_queue: String,
pub generic_queue: String, pub generic_queue: String,
@@ -224,10 +214,6 @@ impl Pushd {
self.get_routing_key(self.message_queue.clone()) self.get_routing_key(self.message_queue.clone())
} }
pub fn get_mass_mention_routing_key(&self) -> String {
self.get_routing_key(self.mass_mention_queue.clone())
}
pub fn get_fr_accepted_routing_key(&self) -> String { pub fn get_fr_accepted_routing_key(&self) -> String {
self.get_routing_key(self.fr_accepted_queue.clone()) self.get_routing_key(self.fr_accepted_queue.clone())
} }
@@ -328,9 +314,6 @@ impl Default for FeaturesAdvanced {
pub struct Features { pub struct Features {
pub limits: FeaturesLimitsCollection, pub limits: FeaturesLimitsCollection,
pub webhooks_enabled: bool, pub webhooks_enabled: bool,
pub mass_mentions_send_notifications: bool,
pub mass_mentions_enabled: bool,
#[serde(default)] #[serde(default)]
pub advanced: FeaturesAdvanced, pub advanced: FeaturesAdvanced,
} }
@@ -354,7 +337,6 @@ pub struct Settings {
pub files: Files, pub files: Files,
pub features: Features, pub features: Features,
pub sentry: Sentry, pub sentry: Sentry,
pub production: bool,
} }
impl Settings { impl Settings {
@@ -382,14 +364,7 @@ pub async fn read() -> Config {
#[cached(time = 30)] #[cached(time = 30)]
pub async fn config() -> Settings { pub async fn config() -> Settings {
let mut config = read().await.try_deserialize::<Settings>().unwrap(); read().await.try_deserialize::<Settings>().unwrap()
// auto-detect production nodes
if config.hosts.api.contains("https") && config.hosts.api.contains("revolt.chat") {
config.production = true;
}
config
} }
/// Configure logging and common Rust variables /// Configure logging and common Rust variables
+8 -9
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.8.4" version = "0.8.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -24,19 +24,18 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.8.4", path = "../config", features = [ revolt-config = { version = "0.8.2", path = "../config", features = [
"report-macros", "report-macros",
] } ] }
revolt-result = { version = "0.8.4", path = "../result" } revolt-result = { version = "0.8.2", path = "../result" }
revolt-models = { version = "0.8.4", path = "../models", features = [ revolt-models = { version = "0.8.2", path = "../models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.8.4", path = "../presence" } revolt-presence = { version = "0.8.2", path = "../presence" }
revolt-permissions = { version = "0.8.4", path = "../permissions", features = [ revolt-permissions = { version = "0.8.2", path = "../permissions", features = [
"serde", "serde",
"bson", "bson",
] } ] }
revolt-parser = { version = "0.1.0", path = "../parser" }
# Utility # Utility
log = "0.4" log = "0.4"
@@ -65,7 +64,7 @@ redis-kiss = { version = "0.1.4" }
# Database # Database
bson = { optional = true, version = "2.1.0" } bson = { optional = true, version = "2.1.0" }
mongodb = { optional = true, version = "3.1.0" } mongodb = { optional = true, version = "2.1.0", default-features = false }
# Database Migration # Database Migration
unicode-segmentation = "1.10.1" unicode-segmentation = "1.10.1"
@@ -97,7 +96,7 @@ web-push = "0.10.0"
revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] } revolt_a2 = { version = "0.10", default-features = false, features = ["ring"] }
# Authifier # Authifier
authifier = { version = "1.0.10", features = ["rocket_impl"] } authifier = { version = "1.0.9", features = ["rocket_impl"] }
# RabbitMQ # RabbitMQ
amqprs = { version = "1.7.0" } amqprs = { version = "1.7.0" }
-32
View File
@@ -170,38 +170,6 @@ impl AMQP {
.await .await
} }
pub async fn mass_mention_message_sent(
&self,
server_id: String,
payload: Vec<PushNotification>,
) -> Result<(), AMQPError> {
let config = revolt_config::config().await;
let payload = MassMessageSentPayload {
notifications: payload,
server_id,
};
let payload = to_string(&payload).unwrap();
let routing_key = config.pushd.get_mass_mention_routing_key();
debug!(
"Sending mass mention payload on channel {}: {}",
routing_key, payload
);
self.channel
.basic_publish(
BasicProperties::default()
.with_content_type("application/json")
.with_persistence(true)
.finish(),
payload.into(),
BasicPublishArguments::new(&config.pushd.exchange, routing_key.as_str()),
)
.await
}
pub async fn ack_message( pub async fn ack_message(
&self, &self,
user_id: String, user_id: String,
+20 -44
View File
@@ -120,50 +120,26 @@ impl Database {
use_tls: config.api.smtp.use_tls, use_tls: config.api.smtp.use_tls,
}, },
expiry: Default::default(), expiry: Default::default(),
templates: if config.production { templates: Templates {
Templates { verify: Template {
verify: Template { title: "Verify your Revolt account.".into(),
title: "Verify your Revolt account.".into(), text: include_str!("../../templates/verify.txt").into(),
text: include_str!("../../templates/verify.txt").into(), url: format!("{}/login/verify/", config.hosts.app),
url: format!("{}/login/verify/", config.hosts.app), html: Some(include_str!("../../templates/verify.html").into()),
html: Some(include_str!("../../templates/verify.html").into()), },
}, reset: Template {
reset: Template { title: "Reset your Revolt password.".into(),
title: "Reset your Revolt password.".into(), text: include_str!("../../templates/reset.txt").into(),
text: include_str!("../../templates/reset.txt").into(), url: format!("{}/login/reset/", config.hosts.app),
url: format!("{}/login/reset/", config.hosts.app), html: Some(include_str!("../../templates/reset.html").into()),
html: Some(include_str!("../../templates/reset.html").into()), },
}, deletion: Template {
deletion: Template { title: "Confirm account deletion.".into(),
title: "Confirm account deletion.".into(), text: include_str!("../../templates/deletion.txt").into(),
text: include_str!("../../templates/deletion.txt").into(), url: format!("{}/delete/", config.hosts.app),
url: format!("{}/delete/", config.hosts.app), html: Some(include_str!("../../templates/deletion.html").into()),
html: Some(include_str!("../../templates/deletion.html").into()), },
}, welcome: None,
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 { } else {
+8 -10
View File
@@ -31,17 +31,17 @@ impl MongoDb {
} }
/// Get a collection by its name /// Get a collection by its name
pub fn col<T: Send + Sync>(&self, collection: &str) -> mongodb::Collection<T> { pub fn col<T>(&self, collection: &str) -> mongodb::Collection<T> {
self.db().collection(collection) self.db().collection(collection)
} }
/// Insert one document into a collection /// Insert one document into a collection
pub async fn insert_one<T: Serialize + Send + Sync>( pub async fn insert_one<T: Serialize>(
&self, &self,
collection: &'static str, collection: &'static str,
document: T, document: T,
) -> Result<InsertOneResult> { ) -> Result<InsertOneResult> {
self.col::<T>(collection).insert_one(document).await self.col::<T>(collection).insert_one(document, None).await
} }
/// Count documents by projection /// Count documents by projection
@@ -51,7 +51,7 @@ impl MongoDb {
projection: Document, projection: Document,
) -> Result<u64> { ) -> Result<u64> {
self.col::<Document>(collection) self.col::<Document>(collection)
.count_documents(projection) .count_documents(projection, None)
.await .await
} }
@@ -67,8 +67,7 @@ impl MongoDb {
{ {
Ok(self Ok(self
.col::<T>(collection) .col::<T>(collection)
.find(projection) .find(projection, options)
.with_options(options)
.await? .await?
.filter_map(|s| async { .filter_map(|s| async {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
@@ -102,8 +101,7 @@ impl MongoDb {
O: Into<Option<FindOneOptions>>, O: Into<Option<FindOneOptions>>,
{ {
self.col::<T>(collection) self.col::<T>(collection)
.find_one(projection) .find_one(projection, options)
.with_options(options)
.await .await
} }
@@ -167,7 +165,7 @@ impl MongoDb {
}; };
self.col::<Document>(collection) self.col::<Document>(collection)
.update_one(projection, query) .update_one(projection, query, None)
.await .await
} }
@@ -202,7 +200,7 @@ impl MongoDb {
projection: Document, projection: Document,
) -> Result<DeleteResult> { ) -> Result<DeleteResult> {
self.col::<Document>(collection) self.col::<Document>(collection)
.delete_one(projection) .delete_one(projection, None)
.await .await
} }
@@ -11,12 +11,6 @@ pub struct MessageSentPayload {
pub users: Vec<String>, pub users: Vec<String>,
} }
#[derive(Serialize, Deserialize)]
pub struct MassMessageSentPayload {
pub notifications: Vec<PushNotification>,
pub server_id: String,
}
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct FRAcceptedPayload { pub struct FRAcceptedPayload {
pub accepted_user: User, pub accepted_user: User,
@@ -10,7 +10,7 @@ impl AbstractMigrations for MongoDb {
#[cfg(test)] #[cfg(test)]
/// Drop the database /// Drop the database
async fn drop_database(&self) { async fn drop_database(&self) {
self.db().drop().await.ok(); self.db().drop(None).await.ok();
} }
/// Migrate the database /// Migrate the database
@@ -18,7 +18,7 @@ impl AbstractMigrations for MongoDb {
info!("Migrating the database."); info!("Migrating the database.");
let list = self let list = self
.list_database_names() .list_database_names(None, None)
.await .await
.expect("Failed to fetch database names."); .expect("Failed to fetch database names.");
@@ -8,254 +8,278 @@ pub async fn create_database(db: &MongoDb) {
info!("Creating database."); info!("Creating database.");
let db = db.db(); let db = db.db();
db.create_collection("accounts") db.create_collection("accounts", None)
.await .await
.expect("Failed to create accounts collection."); .expect("Failed to create accounts collection.");
db.create_collection("users") db.create_collection("users", None)
.await .await
.expect("Failed to create users collection."); .expect("Failed to create users collection.");
db.create_collection("channels") db.create_collection("channels", None)
.await .await
.expect("Failed to create channels collection."); .expect("Failed to create channels collection.");
db.create_collection("messages") db.create_collection("messages", None)
.await .await
.expect("Failed to create messages collection."); .expect("Failed to create messages collection.");
db.create_collection("servers") db.create_collection("servers", None)
.await .await
.expect("Failed to create servers collection."); .expect("Failed to create servers collection.");
db.create_collection("server_members") db.create_collection("server_members", None)
.await .await
.expect("Failed to create server_members collection."); .expect("Failed to create server_members collection.");
db.create_collection("server_bans") db.create_collection("server_bans", None)
.await .await
.expect("Failed to create server_bans collection."); .expect("Failed to create server_bans collection.");
db.create_collection("channel_invites") db.create_collection("channel_invites", None)
.await .await
.expect("Failed to create channel_invites collection."); .expect("Failed to create channel_invites collection.");
db.create_collection("channel_unreads") db.create_collection("channel_unreads", None)
.await .await
.expect("Failed to create channel_unreads collection."); .expect("Failed to create channel_unreads collection.");
db.create_collection("channel_webhooks") db.create_collection("channel_webhooks", None)
.await .await
.expect("Failed to create channel_webhooks collection."); .expect("Failed to create channel_webhooks collection.");
db.create_collection("migrations") db.create_collection("migrations", None)
.await .await
.expect("Failed to create migrations collection."); .expect("Failed to create migrations collection.");
db.create_collection("attachments") db.create_collection("attachments", None)
.await .await
.expect("Failed to create attachments collection."); .expect("Failed to create attachments collection.");
db.create_collection("attachment_hashes") db.create_collection("attachment_hashes", None)
.await .await
.expect("Failed to create attachment_hashes collection."); .expect("Failed to create attachment_hashes collection.");
db.create_collection("user_settings") db.create_collection("user_settings", None)
.await .await
.expect("Failed to create user_settings collection."); .expect("Failed to create user_settings collection.");
db.create_collection("safety_reports") db.create_collection("safety_reports", None)
.await .await
.expect("Failed to create safety_reports collection."); .expect("Failed to create safety_reports collection.");
db.create_collection("safety_snapshots") db.create_collection("safety_snapshots", None)
.await .await
.expect("Failed to create safety_snapshots collection."); .expect("Failed to create safety_snapshots collection.");
db.create_collection("safety_strikes") db.create_collection("safety_strikes", None)
.await .await
.expect("Failed to create safety_strikes collection."); .expect("Failed to create safety_strikes collection.");
db.create_collection("bots") db.create_collection("bots", None)
.await .await
.expect("Failed to create bots collection."); .expect("Failed to create bots collection.");
db.create_collection("ratelimit_events") db.create_collection("ratelimit_events", None)
.await .await
.expect("Failed to create ratelimit_events collection."); .expect("Failed to create ratelimit_events collection.");
db.create_collection("pubsub") db.create_collection(
.with_options( "pubsub",
CreateCollectionOptions::builder() CreateCollectionOptions::builder()
.capped(true) .capped(true)
.size(1_000_000) .size(1_000_000)
.build(), .build(),
) )
.await .await
.expect("Failed to create pubsub collection."); .expect("Failed to create pubsub collection.");
db.run_command(doc! { db.run_command(
"createIndexes": "users", doc! {
"indexes": [ "createIndexes": "users",
{ "indexes": [
"key": { {
"username": 1_i32 "key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
}, },
"name": "username", {
"unique": false, "key": {
"collation": { "username": 1_i32,
"locale": "en", "discriminator": 1_i32
"strength": 2_i32 },
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
} }
}, ]
{ },
"key": { None,
"username": 1_i32, )
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
})
.await .await
.expect("Failed to create username index."); .expect("Failed to create username index.");
db.run_command(doc! { db.run_command(
"createIndexes": "messages", doc! {
"indexes": [ "createIndexes": "messages",
{ "indexes": [
"key": { {
"content": "text" "key": {
"content": "text"
},
"name": "content"
}, },
"name": "content" {
}, "key": {
{ "channel": 1_i32,
"key": { "_id": 1_i32
"channel": 1_i32, },
"_id": 1_i32 "name": "channel_id_compound"
}, },
"name": "channel_id_compound" {
}, "key": {
{ "author": 1_i32
"key": { },
"author": 1_i32 "name": "author"
}, },
"name": "author" {
}, "key": {
{ "channel": 1_i32,
"key": { "pinned": 1_i32
"channel": 1_i32, },
"pinned": 1_i32 "name": "channel_pinned_compound"
}, },
"name": "channel_pinned_compound" ]
}, },
] None,
}) )
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
db.run_command(doc! { db.run_command(
"createIndexes": "channel_unreads", doc! {
"indexes": [ "createIndexes": "channel_unreads",
{ "indexes": [
"key": { {
"_id.channel": 1_i32, "key": {
"_id.user": 1_i32, "_id.channel": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
}, },
"name": "compound_id" {
}, "key": {
{ "_id.user": 1_i32,
"key": { },
"_id.user": 1_i32, "name": "user_id"
}, }
"name": "user_id" ]
} },
] None,
}) )
.await .await
.expect("Failed to create channel_unreads index."); .expect("Failed to create channel_unreads index.");
db.run_command(doc! { db.run_command(
"createIndexes": "server_members", doc! {
"indexes": [ "createIndexes": "server_members",
{ "indexes": [
"key": { {
"_id.server": 1_i32, "key": {
"_id.user": 1_i32, "_id.server": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
}, },
"name": "compound_id" {
}, "key": {
{ "_id.user": 1_i32,
"key": { },
"_id.user": 1_i32, "name": "user_id"
}, }
"name": "user_id" ]
} },
] None,
}) )
.await .await
.expect("Failed to create server_members index."); .expect("Failed to create server_members index.");
db.run_command(doc! { db.run_command(
"createIndexes": "attachments", doc! {
"indexes": [ "createIndexes": "attachments",
{ "indexes": [
"key": { {
"hash": 1_i32 "key": {
"hash": 1_i32
},
"name": "hash"
}, },
"name": "hash" {
}, "key": {
{ "used_for.id": 1_i32
"key": { },
"used_for.id": 1_i32 "name": "used_for_id"
}, }
"name": "used_for_id" ]
} },
] None,
}) )
.await .await
.expect("Failed to create attachments index."); .expect("Failed to create attachments index.");
db.run_command(doc! { db.run_command(
"createIndexes": "attachment_hashes", doc! {
"indexes": [ "createIndexes": "attachment_hashes",
{ "indexes": [
"key": { {
"processed_hash": 1_i32 "key": {
}, "processed_hash": 1_i32
"name": "processed_hash" },
} "name": "processed_hash"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create attachment_hashes index."); .expect("Failed to create attachment_hashes index.");
db.collection("migrations") db.collection("migrations")
.insert_one(doc! { .insert_one(
"_id": 0_i32, doc! {
"revision": LATEST_REVISION "_id": 0_i32,
}) "revision": LATEST_REVISION
},
None,
)
.await .await
.expect("Failed to save migration info."); .expect("Failed to save migration info.");
db.run_command(doc! { db.run_command(
"createIndexes": "ratelimit_events", doc! {
"indexes": [ "createIndexes": "ratelimit_events",
{ "indexes": [
"key": { {
"_id": 1_i32, "key": {
"target_id": 1_i32, "_id": 1_i32,
"event_type": 1_i32, "target_id": 1_i32,
}, "event_type": 1_i32,
"name": "compound_key" },
} "name": "compound_key"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create ratelimit_events index."); .expect("Failed to create ratelimit_events index.");
@@ -26,7 +26,7 @@ pub const LATEST_REVISION: i32 = 31;
pub async fn migrate_database(db: &MongoDb) { pub async fn migrate_database(db: &MongoDb) {
let migrations = db.col::<Document>("migrations"); let migrations = db.col::<Document>("migrations");
let data = migrations let data = migrations
.find_one(doc! {}) .find_one(None, None)
.await .await
.expect("Failed to fetch migration data."); .expect("Failed to fetch migration data.");
@@ -46,6 +46,7 @@ pub async fn migrate_database(db: &MongoDb) {
"revision": revision "revision": revision
} }
}, },
None,
) )
.await .await
.expect("Failed to commit migration information."); .expect("Failed to commit migration information.");
@@ -73,6 +74,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_many( .update_many(
doc! { "attachment": { "$exists": 1_i32 } }, doc! { "attachment": { "$exists": 1_i32 } },
doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0_i32 } }, doc! { "$set": { "attachment.tag": "attachments", "attachment.size": 0_i32 } },
None,
) )
.await .await
.expect("Failed to update messages."); .expect("Failed to update messages.");
@@ -81,6 +83,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_many( .update_many(
doc! {}, doc! {},
doc! { "$set": { "tag": "attachments", "size": 0_i32 } }, doc! { "$set": { "tag": "attachments", "size": 0_i32 } },
None,
) )
.await .await
.expect("Failed to update attachments."); .expect("Failed to update attachments.");
@@ -90,7 +93,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 2 / 2021-05-08]: Add servers collection."); info!("Running migration [revision 2 / 2021-05-08]: Add servers collection.");
db.db() db.db()
.create_collection("servers") .create_collection("servers", None)
.await .await
.expect("Failed to create servers collection."); .expect("Failed to create servers collection.");
} }
@@ -100,12 +103,12 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let messages = db.col::<Document>("messages"); let messages = db.col::<Document>("messages");
let mut cursor = messages let mut cursor = messages
.find(doc! { .find(
"attachment": { doc! {
"$exists": 1_i32 "attachment": {
} "$exists": 1_i32
}) }
.with_options( },
FindOptions::builder() FindOptions::builder()
.projection(doc! { .projection(doc! {
"_id": 1_i32, "_id": 1_i32,
@@ -125,18 +128,19 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.update_one( .update_one(
doc! { "_id": id }, doc! { "_id": id },
doc! { "$unset": { "attachment": 1_i32 }, "$set": { "attachments": attachments } }, doc! { "$unset": { "attachment": 1_i32 }, "$set": { "attachments": attachments } },
None,
) )
.await .await
.unwrap(); .unwrap();
} }
db.db() db.db()
.create_collection("channel_unreads") .create_collection("channel_unreads", None)
.await .await
.expect("Failed to create channel_unreads collection."); .expect("Failed to create channel_unreads collection.");
db.db() db.db()
.create_collection("user_settings") .create_collection("user_settings", None)
.await .await
.expect("Failed to create user_settings collection."); .expect("Failed to create user_settings collection.");
} }
@@ -145,17 +149,17 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 4 / 2021-06-01]: Add more server collections."); info!("Running migration [revision 4 / 2021-06-01]: Add more server collections.");
db.db() db.db()
.create_collection("server_members") .create_collection("server_members", None)
.await .await
.expect("Failed to create server_members collection."); .expect("Failed to create server_members collection.");
db.db() db.db()
.create_collection("server_bans") .create_collection("server_bans", None)
.await .await
.expect("Failed to create server_bans collection."); .expect("Failed to create server_bans collection.");
db.db() db.db()
.create_collection("channel_invites") .create_collection("channel_invites", None)
.await .await
.expect("Failed to create channel_invites collection."); .expect("Failed to create channel_invites collection.");
} }
@@ -178,6 +182,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! { doc! {
"$set": to_document(&server).unwrap() "$set": to_document(&server).unwrap()
}, },
None,
) )
.await .await
.expect("Failed to migrate servers."); .expect("Failed to migrate servers.");
@@ -187,17 +192,20 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 6 / 2021-07-09]: Add message text index."); info!("Running migration [revision 6 / 2021-07-09]: Add message text index.");
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "messages", doc! {
"indexes": [ "createIndexes": "messages",
{ "indexes": [
"key": { {
"content": "text" "key": {
}, "content": "text"
"name": "content" },
} "name": "content"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
} }
@@ -206,7 +214,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 7 / 2021-08-11]: Add message text index."); info!("Running migration [revision 7 / 2021-08-11]: Add message text index.");
db.db() db.db()
.create_collection("bots") .create_collection("bots", None)
.await .await
.expect("Failed to create bots collection."); .expect("Failed to create bots collection.");
} }
@@ -215,15 +223,22 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 8 / 2021-09-10]: Update to Authifier version 1."); info!("Running migration [revision 8 / 2021-09-10]: Update to Authifier version 1.");
db.db() db.db()
.run_command(doc! { .run_command(
"dropIndexes": "accounts", doc! {
"index": ["email", "email_normalised"] "dropIndexes": "accounts",
}) "index": ["email", "email_normalised"]
},
None,
)
.await .await
.expect("Failed to delete legacy account indexes."); .expect("Failed to delete legacy account indexes.");
let col = db.col::<Document>("sessions"); let col = db.col::<Document>("sessions");
let mut cursor = db.col::<Document>("accounts").find(doc! {}).await.unwrap(); let mut cursor = db
.col::<Document>("accounts")
.find(doc! {}, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await { while let Some(doc) = cursor.next().await {
if let Ok(account) = doc { if let Ok(account) = doc {
@@ -252,7 +267,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc.insert("subscription", sub); doc.insert("subscription", sub);
} }
col.insert_one(doc).await.ok(); col.insert_one(doc, None).await.ok();
} }
} else { } else {
info!("Account doesn't have any sessions!"); info!("Account doesn't have any sessions!");
@@ -273,6 +288,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -281,7 +297,11 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 9 { if revision <= 9 {
info!("Running migration [revision 9 / 2021-09-14]: Switch from last_message to last_message_id."); info!("Running migration [revision 9 / 2021-09-14]: Switch from last_message to last_message_id.");
let mut cursor = db.col::<Document>("channels").find(doc! {}).await.unwrap(); let mut cursor = db
.col::<Document>("channels")
.find(doc! {}, None)
.await
.unwrap();
while let Some(doc) = cursor.next().await { while let Some(doc) = cursor.next().await {
if let Ok(channel) = doc { if let Ok(channel) = doc {
@@ -320,6 +340,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"last_message": 1_i32, "last_message": 1_i32,
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -341,6 +362,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"nonce": 1_i32, "nonce": 1_i32,
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -353,6 +375,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"nonce": 1_i32, "nonce": 1_i32,
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -362,61 +385,70 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 11 / 2021-11-14]: Add indexes to database."); info!("Running migration [revision 11 / 2021-11-14]: Add indexes to database.");
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "messages", doc! {
"indexes": [ "createIndexes": "messages",
{ "indexes": [
"key": { {
"channel": 1_i32 "key": {
}, "channel": 1_i32
"name": "channel" },
} "name": "channel"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "channel_unreads", doc! {
"indexes": [ "createIndexes": "channel_unreads",
{ "indexes": [
"key": { {
"_id.channel": 1_i32, "key": {
"_id.user": 1_i32, "_id.channel": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
}, },
"name": "compound_id" {
}, "key": {
{ "_id.user": 1_i32,
"key": { },
"_id.user": 1_i32, "name": "user_id"
}, }
"name": "user_id" ]
} },
] None,
}) )
.await .await
.expect("Failed to create channel_unreads index."); .expect("Failed to create channel_unreads index.");
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "server_members", doc! {
"indexes": [ "createIndexes": "server_members",
{ "indexes": [
"key": { {
"_id.server": 1_i32, "key": {
"_id.user": 1_i32, "_id.server": 1_i32,
"_id.user": 1_i32,
},
"name": "compound_id"
}, },
"name": "compound_id" {
}, "key": {
{ "_id.user": 1_i32,
"key": { },
"_id.user": 1_i32, "name": "user_id"
}, }
"name": "user_id" ]
} },
] None,
}) )
.await .await
.expect("Failed to create server_members index."); .expect("Failed to create server_members index.");
} }
@@ -425,18 +457,21 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 12 / 2021-11-21]: Add indexes to database."); info!("Running migration [revision 12 / 2021-11-21]: Add indexes to database.");
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "messages", doc! {
"indexes": [ "createIndexes": "messages",
{ "indexes": [
"key": { {
"channel": 1_i32, "key": {
"_id": 1_i32 "channel": 1_i32,
}, "_id": 1_i32
"name": "channel_id_compound" },
} "name": "channel_id_compound"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
} }
@@ -450,7 +485,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
async_std::task::sleep(Duration::from_secs(10)).await; async_std::task::sleep(Duration::from_secs(10)).await;
let servers = db.col::<Document>("servers"); let servers = db.col::<Document>("servers");
let mut cursor = servers.find(doc! {}).await.unwrap(); let mut cursor = servers.find(doc! {}, None).await.unwrap();
while let Some(Ok(mut document)) = cursor.next().await { while let Some(Ok(mut document)) = cursor.next().await {
let id = document.get_str("_id").unwrap().to_string(); let id = document.get_str("_id").unwrap().to_string();
@@ -494,13 +529,13 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
servers servers
.update_one(doc! { "_id": id }, doc! { "$set": update }) .update_one(doc! { "_id": id }, doc! { "$set": update }, None)
.await .await
.unwrap(); .unwrap();
} }
let channels = db.col::<Document>("channels"); let channels = db.col::<Document>("channels");
let mut cursor = channels.find(doc! {}).await.unwrap(); let mut cursor = channels.find(doc! {}, None).await.unwrap();
while let Some(Ok(document)) = cursor.next().await { while let Some(Ok(document)) = cursor.next().await {
let id = document.get_str("_id").unwrap().to_string(); let id = document.get_str("_id").unwrap().to_string();
@@ -540,7 +575,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
channels channels
.update_one(doc! { "_id": id }, update) .update_one(doc! { "_id": id }, update, None)
.await .await
.unwrap(); .unwrap();
} }
@@ -561,6 +596,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"content": "system" "content": "system"
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -585,22 +621,25 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
.unwrap(); .unwrap();
db.db() db.db()
.create_collection("emojis") .create_collection("emojis", None)
.await .await
.expect("Failed to create emojis collection."); .expect("Failed to create emojis collection.");
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "emojis", doc! {
"indexes": [ "createIndexes": "emojis",
{ "indexes": [
"key": { {
"parent.id": 1_i32, "key": {
}, "parent.id": 1_i32,
"name": "parent_id" },
} "name": "parent_id"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create emoji parent index."); .expect("Failed to create emoji parent index.");
} }
@@ -613,9 +652,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
doc! {}, doc! {},
doc! { doc! {
"$set": { "$set": {
"joined_at": DateTime::now().try_to_rfc3339_string().expect("Failed to convert the date to rfc3339") "joined_at": DateTime::now().to_rfc3339_string()
} }
}, },
None,
) )
.await .await
.expect("Failed to update server members."); .expect("Failed to update server members.");
@@ -626,10 +666,13 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if db if db
.db() .db()
.run_command(doc! { .run_command(
"dropIndexes": "messages", doc! {
"index": ["channel"] "dropIndexes": "messages",
}) "index": ["channel"]
},
None,
)
.await .await
.is_err() .is_err()
{ {
@@ -637,17 +680,20 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
} }
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "messages", doc! {
"indexes": [ "createIndexes": "messages",
{ "indexes": [
"key": { {
"author": 1_i32, "key": {
}, "author": 1_i32,
"name": "author" },
} "name": "author"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create messages author index."); .expect("Failed to create messages author index.");
} }
@@ -657,26 +703,35 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections." "Running migration [revision 19 / 27-02-2023]: Create report / snapshot collections."
); );
db.db().create_collection("safety_reports").await.unwrap(); db.db()
.create_collection("safety_reports", None)
.await
.unwrap();
db.db().create_collection("safety_snapshots").await.unwrap(); db.db()
.create_collection("safety_snapshots", None)
.await
.unwrap();
} }
if revision <= 20 { if revision <= 20 {
info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`."); info!("Running migration [revision 20 / 28-02-2023]: Add index `snapshot.report_id`.");
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "safety_snapshots", doc! {
"indexes": [ "createIndexes": "safety_snapshots",
{ "indexes": [
"key": { {
"report_id": 1_i32 "key": {
}, "report_id": 1_i32
"name": "report_id" },
} "name": "report_id"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create safety snapshot index."); .expect("Failed to create safety snapshot index.");
} }
@@ -684,7 +739,10 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 21 { if revision <= 21 {
info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`."); info!("Running migration [revision 21 / 31-05-2023]: Add collection `safety_strikes`.");
db.db().create_collection("safety_strikes").await.unwrap(); db.db()
.create_collection("safety_strikes", None)
.await
.unwrap();
} }
if revision <= 22 { if revision <= 22 {
@@ -698,6 +756,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"moderator_id": "01EX2NCWQ0CHS3QJF0FEQS1GR4" "moderator_id": "01EX2NCWQ0CHS3QJF0FEQS1GR4"
} }
}, },
None,
) )
.await .await
.expect("Failed to update server members."); .expect("Failed to update server members.");
@@ -707,10 +766,13 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users."); info!("Running migration [revision 23 / 10-06-2023]: Generate discriminators for users.");
db.db() db.db()
.run_command(doc! { .run_command(
"dropIndexes": "users", doc! {
"index": "username" "dropIndexes": "users",
}) "index": "username"
},
None,
)
.await .await
.expect("Failed to drop existing username index."); .expect("Failed to drop existing username index.");
@@ -725,7 +787,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let users: Vec<UserInformation> = db let users: Vec<UserInformation> = db
.col::<UserInformation>("users") .col::<UserInformation>("users")
.find(doc! {}) .find(doc! {}, None)
.await .await
.unwrap() .unwrap()
.map(|doc| doc.expect("id and username")) .map(|doc| doc.expect("id and username"))
@@ -777,6 +839,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"discriminator": discriminator "discriminator": discriminator
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -824,6 +887,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"display_name": &info.username "display_name": &info.username
} }
}, },
None,
) )
.await .await
.unwrap(); .unwrap();
@@ -834,37 +898,43 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 24 { if revision <= 24 {
info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index."); info!("Running migration [revision 24 / 09-06-2023]: Add collection `channel_webhooks` if not exists, update users index.");
db.db().create_collection("channel_webhooks").await.ok(); db.db()
.create_collection("channel_webhooks", None)
.await
.ok();
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "users", doc! {
"indexes": [ "createIndexes": "users",
{ "indexes": [
"key": { {
"username": 1_i32 "key": {
"username": 1_i32
},
"name": "username",
"unique": false,
"collation": {
"locale": "en",
"strength": 2_i32
}
}, },
"name": "username", {
"unique": false, "key": {
"collation": { "username": 1_i32,
"locale": "en", "discriminator": 1_i32
"strength": 2_i32 },
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
} }
}, ]
{ },
"key": { None,
"username": 1_i32, )
"discriminator": 1_i32
},
"name": "username_discriminator",
"unique": true,
"collation": {
"locale": "en",
"strength": 2_i32
}
}
]
})
.await .await
.expect("Failed to create username index."); .expect("Failed to create username index.");
}; };
@@ -880,6 +950,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"permissions": *DEFAULT_WEBHOOK_PERMISSIONS as i64 "permissions": *DEFAULT_WEBHOOK_PERMISSIONS as i64
} }
}, },
None,
) )
.await .await
.expect("Failed to update webhooks."); .expect("Failed to update webhooks.");
@@ -888,22 +959,28 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 25 { if revision <= 25 {
info!("Running migration [revision 25 / 15-06-2023]: Add collection `ratelimit_events` with index."); info!("Running migration [revision 25 / 15-06-2023]: Add collection `ratelimit_events` with index.");
db.db().create_collection("ratelimit_events").await.ok(); db.db()
.create_collection("ratelimit_events", None)
.await
.ok();
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "ratelimit_events", doc! {
"indexes": [ "createIndexes": "ratelimit_events",
{ "indexes": [
"key": { {
"_id": 1_i32, "key": {
"target_id": 1_i32, "_id": 1_i32,
"event_type": 1_i32, "target_id": 1_i32,
}, "event_type": 1_i32,
"name": "compound_key" },
} "name": "compound_key"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create ratelimit_events index."); .expect("Failed to create ratelimit_events index.");
} }
@@ -939,9 +1016,12 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let invites = db let invites = db
.db() .db()
.collection::<Outer>("channel_invites") .collection::<Outer>("channel_invites")
.find(doc! { .find(
"type": { "$exists": false } doc! {
}) "type": { "$exists": false }
},
None,
)
.await .await
.expect("failed to find invites") .expect("failed to find invites")
.filter_map(|s| async { s.ok() }) .filter_map(|s| async { s.ok() })
@@ -975,15 +1055,18 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if !invites.is_empty() { if !invites.is_empty() {
db.db() db.db()
.collection("channel_invites") .collection("channel_invites")
.insert_many(invites) .insert_many(invites, None)
.await .await
.expect("failed to insert corrected invite"); .expect("failed to insert corrected invite");
db.db() db.db()
.collection::<Outer>("channel_invites") .collection::<Outer>("channel_invites")
.delete_many(doc! { .delete_many(
"type": { "$exists": false } doc! {
}) "type": { "$exists": false }
},
None,
)
.await .await
.expect("failed to find invites"); .expect("failed to find invites");
} }
@@ -993,18 +1076,21 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 27 / 21-07-2024]: create message pinned index."); info!("Running migration [revision 27 / 21-07-2024]: create message pinned index.");
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "messages", doc! {
"indexes": [ "createIndexes": "messages",
{ "indexes": [
"key": { {
"channel": 1_i32, "key": {
"pinned": 1_i32 "channel": 1_i32,
}, "pinned": 1_i32
"name": "channel_pinned_compound" },
} "name": "channel_pinned_compound"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create message index."); .expect("Failed to create message index.");
} }
@@ -1012,35 +1098,44 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
if revision <= 28 { if revision <= 28 {
info!("Running migration [revision 28 / 10-09-2024]: Add support for new Autumn."); info!("Running migration [revision 28 / 10-09-2024]: Add support for new Autumn.");
db.db().create_collection("attachment_hashes").await.ok(); db.db()
.create_collection("attachment_hashes", None)
.await
.ok();
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "attachments", doc! {
"indexes": [ "createIndexes": "attachments",
{ "indexes": [
"key": { {
"hash": 1_i32 "key": {
}, "hash": 1_i32
"name": "hash" },
} "name": "hash"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create attachments index."); .expect("Failed to create attachments index.");
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "attachment_hashes", doc! {
"indexes": [ "createIndexes": "attachment_hashes",
{ "indexes": [
"key": { {
"processed_hash": 1_i32 "key": {
}, "processed_hash": 1_i32
"name": "processed_hash" },
} "name": "processed_hash"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create attachment_hashes index."); .expect("Failed to create attachment_hashes index.");
} }
@@ -1051,17 +1146,20 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
info!("Running migration [revision 30 / 29-09-2024]: Add index for used_for.id to attachments."); info!("Running migration [revision 30 / 29-09-2024]: Add index for used_for.id to attachments.");
db.db() db.db()
.run_command(doc! { .run_command(
"createIndexes": "attachments", doc! {
"indexes": [ "createIndexes": "attachments",
{ "indexes": [
"key": { {
"used_for.id": 1_i32 "key": {
}, "used_for.id": 1_i32
"name": "used_for_id" },
} "name": "used_for_id"
] }
}) ]
},
None,
)
.await .await
.expect("Failed to create attachments index."); .expect("Failed to create attachments index.");
} }
@@ -1078,7 +1176,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
let webhooks = db let webhooks = db
.db() .db()
.collection::<WebhookShell>("channel_webhooks") .collection::<WebhookShell>("channel_webhooks")
.find(doc! {}) .find(doc! {}, None)
.await .await
.expect("webhooks") .expect("webhooks")
.filter_map(|s| async { s.ok() }) .filter_map(|s| async { s.ok() })
@@ -1109,6 +1207,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
"creator_id": creator_id "creator_id": creator_id
} }
}, },
None,
) )
.await .await
.expect("update webhook"); .expect("update webhook");
@@ -1119,7 +1218,7 @@ pub async fn run_migrations(db: &MongoDb, revision: i32) -> i32 {
}) => { }) => {
db.db() db.db()
.collection::<WebhookShell>("channel_webhooks") .collection::<WebhookShell>("channel_webhooks")
.delete_one(doc! { "_id": webhook._id }) .delete_one(doc! { "_id": webhook._id }, None)
.await .await
.expect("failed to delete invalid webhook"); .expect("failed to delete invalid webhook");
} }
@@ -24,9 +24,12 @@ impl AbstractChannelInvites for MongoDb {
async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> { async fn fetch_invites_for_server(&self, server_id: &str) -> Result<Vec<Invite>> {
Ok(self Ok(self
.col::<Invite>(COL) .col::<Invite>(COL)
.find(doc! { .find(
"server": server_id, doc! {
}) "server": server_id,
},
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -26,14 +26,6 @@ pub trait AbstractChannelUnreads: Sync + Send {
message_ids: &[String], message_ids: &[String],
) -> Result<()>; ) -> Result<()>;
/// Add a mention.
async fn add_mention_to_many_unreads<'a>(
&self,
channel_id: &str,
user_ids: &[String],
message_ids: &[String],
) -> Result<()>;
/// Fetch all unreads with mentions for a user. /// Fetch all unreads with mentions for a user.
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>>; async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>>;
@@ -37,8 +37,6 @@ impl AbstractChannelUnreads for MongoDb {
"last_id": message_id "last_id": message_id
} }
}, },
)
.with_options(
FindOneAndUpdateOptions::builder() FindOneAndUpdateOptions::builder()
.upsert(true) .upsert(true)
.return_document(ReturnDocument::After) .return_document(ReturnDocument::After)
@@ -53,12 +51,15 @@ impl AbstractChannelUnreads for MongoDb {
let current_time = Ulid::new().to_string(); let current_time = Ulid::new().to_string();
self.col::<Document>(COL) self.col::<Document>(COL)
.delete_many(doc! { .delete_many(
"_id.channel": { doc! {
"$in": channel_ids "_id.channel": {
"$in": channel_ids
},
"_id.user": user_id
}, },
"_id.user": user_id None,
}) )
.await .await
.map_err(|_| create_database_error!("delete_many", COL))?; .map_err(|_| create_database_error!("delete_many", COL))?;
@@ -76,6 +77,7 @@ impl AbstractChannelUnreads for MongoDb {
} }
}) })
.collect::<Vec<Document>>(), .collect::<Vec<Document>>(),
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -102,42 +104,13 @@ impl AbstractChannelUnreads for MongoDb {
} }
} }
}, },
UpdateOptions::builder().upsert(true).build(),
) )
.with_options(UpdateOptions::builder().upsert(true).build())
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| create_database_error!("update_one", COL)) .map_err(|_| create_database_error!("update_one", COL))
} }
/// Add a mention to multiple users.
async fn add_mention_to_many_unreads<'a>(
&self,
channel_id: &str,
user_ids: &[String],
message_ids: &[String],
) -> Result<()> {
self.col::<Document>(COL)
.update_many(
doc! {
"_id.channel": channel_id,
"_id.user": {
"$in": user_ids
},
},
doc! {
"$push": {
"mentions": {
"$each": message_ids
}
}
},
)
.with_options(UpdateOptions::builder().upsert(true).build())
.await
.map(|_| ())
.map_err(|_| create_database_error!("update_many", COL))
}
/// Fetch all channel unreads for a user. /// Fetch all channel unreads for a user.
async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> { async fn fetch_unreads(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
query!( query!(
@@ -78,38 +78,6 @@ impl AbstractChannelUnreads for ReferenceDb {
Ok(()) Ok(())
} }
/// Add a mention to multiple users.
async fn add_mention_to_many_unreads<'a>(
&self,
channel_id: &str,
user_ids: &[String],
message_ids: &[String],
) -> Result<()> {
let mut unreads = self.channel_unreads.lock().await;
for user_id in user_ids {
let key = ChannelCompositeKey {
channel: channel_id.to_string(),
user: user_id.to_string(),
};
if let Some(unread) = unreads.get_mut(&key) {
unread.mentions.replace(message_ids.to_vec());
} else {
unreads.insert(
key.clone(),
ChannelUnread {
id: key,
last_id: None,
mentions: Some(message_ids.to_vec()),
},
);
}
}
Ok(())
}
async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> { async fn fetch_unread_mentions(&self, user_id: &str) -> Result<Vec<ChannelUnread>> {
let unreads = self.channel_unreads.lock().await; let unreads = self.channel_unreads.lock().await;
Ok(unreads Ok(unreads
@@ -24,9 +24,12 @@ impl AbstractWebhooks for MongoDb {
async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> { async fn fetch_webhooks_for_channel(&self, channel_id: &str) -> Result<Vec<Webhook>> {
Ok(self Ok(self
.col::<Webhook>(COL) .col::<Webhook>(COL)
.find(doc! { .find(
"channel_id": channel_id, doc! {
}) "channel_id": channel_id,
},
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -269,24 +269,16 @@ impl Channel {
})); }));
} }
let id = ulid::Ulid::new().to_string();
let icon = if let Some(icon_id) = data.icon {
Some(File::use_channel_icon(db, &icon_id, &id, &owner_id).await?)
} else {
None
};
let recipients = data.users.into_iter().collect::<Vec<String>>(); let recipients = data.users.into_iter().collect::<Vec<String>>();
let channel = Channel::Group { let channel = Channel::Group {
id, id: ulid::Ulid::new().to_string(),
name: data.name, name: data.name,
owner: owner_id, owner: owner_id,
description: data.description, description: data.description,
recipients: recipients.clone(), recipients: recipients.clone(),
icon, icon: None,
last_message_id: None, last_message_id: None,
permissions: None, permissions: None,
@@ -23,11 +23,14 @@ impl AbstractChannels for MongoDb {
async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> { async fn fetch_channels<'a>(&self, ids: &'a [String]) -> Result<Vec<Channel>> {
Ok(self Ok(self
.col::<Channel>(COL) .col::<Channel>(COL)
.find(doc! { .find(
"_id": { doc! {
"$in": ids "_id": {
} "$in": ids
}) }
},
None,
)
.await .await
.map_err(|_| create_database_error!("fetch", "channels"))? .map_err(|_| create_database_error!("fetch", "channels"))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -116,6 +119,7 @@ impl AbstractChannels for MongoDb {
"recipients": user "recipients": user
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -137,6 +141,7 @@ impl AbstractChannels for MongoDb {
"role_permissions.".to_owned() + role: permissions "role_permissions.".to_owned() + role: permissions
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -174,6 +179,7 @@ impl AbstractChannels for MongoDb {
"recipients": user "recipients": user
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -247,6 +253,7 @@ impl AbstractChannels for MongoDb {
"_id": server.id "_id": server.id
}, },
update, update,
None,
) )
.await .await
.map_err(|_| create_database_error!("update_one", "servers"))?; .map_err(|_| create_database_error!("update_one", "servers"))?;
@@ -267,17 +274,23 @@ impl MongoDb {
pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> { pub async fn delete_associated_channel_objects(&self, id: Bson) -> Result<()> {
// Delete all invites to these channels. // Delete all invites to these channels.
self.col::<Document>("channel_invites") self.col::<Document>("channel_invites")
.delete_many(doc! { .delete_many(
"channel": &id doc! {
}) "channel": &id
},
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", "channel_invites"))?; .map_err(|_| create_database_error!("delete_many", "channel_invites"))?;
// Delete unread message objects on channels. // Delete unread message objects on channels.
self.col::<Document>("channel_unreads") self.col::<Document>("channel_unreads")
.delete_many(doc! { .delete_many(
"_id.channel": &id doc! {
}) "_id.channel": &id
},
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", "channel_unreads")) .map_err(|_| create_database_error!("delete_many", "channel_unreads"))
.map(|_| ())?; .map(|_| ())?;
@@ -286,9 +299,12 @@ impl MongoDb {
// Delete all webhooks on this channel. // Delete all webhooks on this channel.
self.col::<Document>("webhooks") self.col::<Document>("webhooks")
.delete_many(doc! { .delete_many(
"channel": &id doc! {
}) "channel": &id
},
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", "webhooks")) .map_err(|_| create_database_error!("delete_many", "webhooks"))
.map(|_| ()) .map(|_| ())
@@ -60,6 +60,7 @@ impl AbstractEmojis for MongoDb {
} }
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -42,6 +42,7 @@ impl AbstractAttachmentHashes for MongoDb {
"iv": nonce "iv": nonce
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -110,6 +110,7 @@ impl AbstractAttachments for MongoDb {
"uploader_id": uploader_id "uploader_id": uploader_id
} }
}, },
None,
) )
.await .await
.map_err(|_| create_database_error!("update_one", COL))?; .map_err(|_| create_database_error!("update_one", COL))?;
@@ -129,6 +130,7 @@ impl AbstractAttachments for MongoDb {
"reported": true "reported": true
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -147,6 +149,7 @@ impl AbstractAttachments for MongoDb {
"deleted": true "deleted": true
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -167,6 +170,7 @@ impl AbstractAttachments for MongoDb {
"deleted": true "deleted": true
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -189,6 +193,7 @@ impl MongoDb {
"deleted": true "deleted": true
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
+41 -208
View File
@@ -5,20 +5,17 @@ use iso8601_timestamp::Timestamp;
use revolt_config::{config, FeaturesLimits}; use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{ use revolt_models::v0::{
self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort, self, BulkMessageResponse, DataMessageSend, Embed, MessageAuthor, MessageFlags, MessageSort,
MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text MessageWebhook, PushNotification, ReplyIntent, SendableEmbed, Text, RE_MENTION,
}; };
use revolt_permissions::{calculate_channel_permissions, ChannelPermission, PermissionValue}; use revolt_permissions::{ChannelPermission, PermissionValue};
use revolt_result::{ErrorType, Result}; use revolt_result::Result;
use ulid::Ulid; use ulid::Ulid;
use validator::Validate; use validator::Validate;
use crate::{ use crate::{
events::client::EventV1, events::client::EventV1,
tasks::{self, ack::AckEvent}, tasks::{self, ack::AckEvent},
util::{ util::{bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey},
bulk_permissions::BulkDatabasePermissionQuery, idempotency::IdempotencyKey,
permissions::DatabasePermissionQuery,
},
Channel, Database, Emoji, File, User, AMQP, Channel, Database, Emoji, File, User, AMQP,
}; };
@@ -56,9 +53,6 @@ auto_derived_partial!(
/// Array of user ids mentioned in this message /// Array of user ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>, pub mentions: Option<Vec<String>>,
/// Array of role ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub role_mentions: Option<Vec<String>>,
/// Array of message ids this message is replying to /// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>, pub replies: Option<Vec<String>>,
@@ -77,7 +71,7 @@ auto_derived_partial!(
/// Bitfield of message flags /// Bitfield of message flags
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub flags: Option<u32>, pub flags: Option<i32>,
}, },
"PartialMessage" "PartialMessage"
); );
@@ -205,30 +199,6 @@ auto_derived!(
} }
); );
pub struct MessageFlagsValue(pub u32);
impl MessageFlagsValue {
pub fn has(&self, flag: MessageFlags) -> bool {
self.has_value(flag as u32)
}
pub fn has_value(&self, bit: u32) -> bool {
let mask = 1 << bit;
self.0 & mask == mask
}
pub fn set(&mut self, flag: MessageFlags, toggle: bool) -> &mut Self {
self.set_value(flag as u32, toggle)
}
pub fn set_value(&mut self, bit: u32, toggle: bool) -> &mut Self {
if toggle {
self.0 |= 1 << bit;
} else {
self.0 &= !(1 << bit);
}
self
}
}
#[allow(clippy::derivable_impls)] #[allow(clippy::derivable_impls)]
impl Default for Message { impl Default for Message {
fn default() -> Self { fn default() -> Self {
@@ -244,7 +214,6 @@ impl Default for Message {
edited: None, edited: None,
embeds: None, embeds: None,
mentions: None, mentions: None,
role_mentions: None,
replies: None, replies: None,
reactions: Default::default(), reactions: Default::default(),
interactions: Default::default(), interactions: Default::default(),
@@ -293,43 +262,13 @@ impl Message {
return Err(create_error!(EmptyMessage)); return Err(create_error!(EmptyMessage));
} }
let allow_mass_mentions = allow_mentions && config.features.mass_mentions_enabled; // Ensure flags are either not set or have permissible values
if let Some(flags) = &data.flags {
let mut mentions_everyone = false; if flags != &0 && flags != &1 {
let mut mentions_online = false;
let mut suppress_notifications = false;
if let Some(raw_flags) = &data.flags {
if raw_flags > &7 {
// quick path to failure: bigger than all the bits combined
return Err(create_error!(InvalidProperty)); return Err(create_error!(InvalidProperty));
} }
// First step of mass mention resolution
let flags = MessageFlagsValue(*raw_flags);
suppress_notifications = flags.has(MessageFlags::SuppressNotifications);
mentions_everyone = allow_mentions && flags.has(MessageFlags::MentionsEveryone);
mentions_online = allow_mentions && flags.has(MessageFlags::MentionsOnline);
// Not a bot, and attempting to set mention flags
if user.as_ref().is_some_and(|u| u.bot.as_ref().is_none())
&& (mentions_everyone || mentions_online)
{
return Err(create_error!(IsNotBot));
}
if mentions_everyone && mentions_online {
return Err(create_error!(InvalidFlagValue));
}
} }
let server_id = match channel {
Channel::TextChannel { ref server, .. } | Channel::VoiceChannel { ref server, .. } => {
Some(server.clone())
}
_ => None,
};
// Ensure restrict_reactions is not specified without reactions list // Ensure restrict_reactions is not specified without reactions list
if let Some(interactions) = &data.interactions { if let Some(interactions) = &data.interactions {
if interactions.restrict_reactions { if interactions.restrict_reactions {
@@ -363,76 +302,18 @@ impl Message {
.unwrap_or_default(), .unwrap_or_default(),
author: author_id, author: author_id,
webhook: webhook.map(|w| w.into()), webhook: webhook.map(|w| w.into()),
flags: data.flags, flags: data.flags.map(|v| v as i32),
..Default::default() ..Default::default()
}; };
// Parse mentions in message. // Parse mentions in message.
let mut mentions = HashSet::new();
let mut message_mentions = if let Some(raw_content) = &data.content { if allow_mentions {
revolt_parser::parse_message(raw_content) if let Some(content) = &data.content {
} else { for capture in RE_MENTION.captures_iter(content) {
revolt_parser::MessageResults::default() if let Some(mention) = capture.get(1) {
}; mentions.insert(mention.as_str().to_string());
}
message_mentions.mentions_everyone |= mentions_everyone;
message_mentions.mentions_online |= mentions_online;
let revolt_parser::MessageResults {
mut user_mentions,
mut role_mentions,
mut mentions_everyone,
mut mentions_online
} = message_mentions;
if allow_mass_mentions && server_id.is_some() && !role_mentions.is_empty() {
let server_data = db
.fetch_server(server_id.unwrap().as_str())
.await
.expect("Failed to fetch server");
role_mentions.retain(|role_id| server_data.roles.contains_key(role_id));
}
// Validate the user can perform a mass mention
if !config.features.mass_mentions_enabled
&& (mentions_everyone || mentions_online || !role_mentions.is_empty())
{
mentions_everyone = false;
mentions_online = false;
role_mentions.clear();
} else if mentions_everyone || mentions_online || !role_mentions.is_empty() {
debug!(
"Mentioned everyone: {}, mentioned online: {}, mentioned roles: {:?}",
mentions_everyone, mentions_online, &role_mentions
);
if let Some(user) = match author {
MessageAuthor::User(user) => Some(Ok(user)),
MessageAuthor::System { .. } => Some(Err(())), // DISALLOWED
MessageAuthor::Webhook(..) => None, // Bypass check
} {
if user.is_err() {
return Err(create_error!(InvalidProperty));
}
let owned_user: User = user.unwrap().to_owned().into();
let mut query = DatabasePermissionQuery::new(db, &owned_user).channel(&channel);
let perms = calculate_channel_permissions(&mut query).await;
if (mentions_everyone || mentions_online)
&& !perms.has_channel_permission(ChannelPermission::MentionEveryone)
{
return Err(create_error!(MissingPermission {
permission: ChannelPermission::MentionEveryone.to_string()
}));
}
if !role_mentions.is_empty()
&& !perms.has_channel_permission(ChannelPermission::MentionRoles)
{
return Err(create_error!(MissingPermission {
permission: ChannelPermission::MentionRoles.to_string()
}));
} }
} }
} }
@@ -446,54 +327,38 @@ impl Message {
})); }));
} }
for ReplyIntent { for ReplyIntent { id, mention } in entries {
id, let message = db.fetch_message(&id).await?;
mention,
fail_if_not_exists,
} in entries
{
match db.fetch_message(&id).await {
// Referenced message exists
Ok(message) => {
if mention && allow_mentions {
user_mentions.insert(message.author.to_owned());
}
replies.insert(message.id); if mention && allow_mentions {
} mentions.insert(message.author.to_owned());
// If the referenced message doesn't exist and fail_if_not_exists
// is set to false, send the message without the reply.
Err(e) => {
if !matches!(e.error_type, ErrorType::NotFound)
|| fail_if_not_exists.unwrap_or(true)
{
return Err(e);
}
}
} }
replies.insert(message.id);
} }
} }
// Validate the mentions go to users in the channel/server // Validate the mentions go to users in the channel/server
if !user_mentions.is_empty() { if !mentions.is_empty() {
match channel { match channel {
Channel::DirectMessage { ref recipients, .. } Channel::DirectMessage { ref recipients, .. }
| Channel::Group { ref recipients, .. } => { | Channel::Group { ref recipients, .. } => {
let recipients_hash = HashSet::<&String, RandomState>::from_iter(recipients); let recipients_hash: HashSet<&String, RandomState> =
user_mentions.retain(|m| recipients_hash.contains(m)); HashSet::from_iter(recipients);
role_mentions.clear(); mentions.retain(|m| recipients_hash.contains(m));
} }
Channel::TextChannel { ref server, .. } Channel::TextChannel { ref server, .. }
| Channel::VoiceChannel { ref server, .. } => { | Channel::VoiceChannel { ref server, .. } => {
let mentions_vec = Vec::from_iter(user_mentions.iter().cloned()); let mentions_vec = Vec::from_iter(mentions.iter().cloned());
let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await; let valid_members = db.fetch_members(server.as_str(), &mentions_vec[..]).await;
if let Ok(valid_members) = valid_members { if let Ok(valid_members) = valid_members {
let valid_mentions = HashSet::<&String, RandomState>::from_iter(valid_members.iter().map(|m| &m.id.user)); let valid_mentions: HashSet<&String, RandomState> =
HashSet::from_iter(valid_members.iter().map(|m| &m.id.user));
user_mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server mentions.retain(|m| valid_mentions.contains(m)); // quick pass, validate mentions are in the server
if !user_mentions.is_empty() { if !mentions.is_empty() {
// if there are still mentions, drill down to a channel-level // if there are still mentions, drill down to a channel-level
let member_channel_view_perms = let member_channel_view_perms =
BulkDatabasePermissionQuery::from_server_id(db, server) BulkDatabasePermissionQuery::from_server_id(db, server)
@@ -503,7 +368,8 @@ impl Message {
.members_can_see_channel() .members_can_see_channel()
.await; .await;
user_mentions.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false)); mentions
.retain(|m| *member_channel_view_perms.get(m).unwrap_or(&false));
} }
} else { } else {
revolt_config::capture_error(&valid_members.unwrap_err()); revolt_config::capture_error(&valid_members.unwrap_err());
@@ -511,19 +377,13 @@ impl Message {
} }
} }
Channel::SavedMessages { .. } => { Channel::SavedMessages { .. } => {
user_mentions.clear(); mentions.clear();
} }
} }
} }
if !user_mentions.is_empty() { if !mentions.is_empty() {
message.mentions.replace(user_mentions.into_iter().collect()); message.mentions.replace(mentions.into_iter().collect());
}
if !role_mentions.is_empty() {
message
.role_mentions
.replace(role_mentions.into_iter().collect());
} }
if !replies.is_empty() { if !replies.is_empty() {
@@ -532,15 +392,6 @@ impl Message {
.replace(replies.into_iter().collect::<Vec<String>>()); .replace(replies.into_iter().collect::<Vec<String>>());
} }
// Calculate final message flags
let mut flag_value = MessageFlagsValue(0);
flag_value
.set(MessageFlags::SuppressNotifications, suppress_notifications)
.set(MessageFlags::MentionsEveryone, mentions_everyone)
.set(MessageFlags::MentionsOnline, mentions_online);
message.flags = Some(flag_value.0);
// Add attachments to message. // Add attachments to message.
let mut attachments = vec![]; let mut attachments = vec![];
if data if data
@@ -619,12 +470,7 @@ impl Message {
tasks::ack::queue_message( tasks::ack::queue_message(
self.channel.to_string(), self.channel.to_string(),
AckEvent::ProcessMessage { AckEvent::ProcessMessage {
messages: vec![( messages: vec![(None, self.clone(), mentions.clone(), true)],
None,
self.clone(),
mentions.clone(),
self.has_suppressed_notifications(),
)],
}, },
) )
.await; .await;
@@ -668,9 +514,7 @@ impl Message {
) )
.await?; .await?;
if !self.has_suppressed_notifications() if !self.has_suppressed_notifications() {
&& (self.mentions.is_some() || self.contains_mass_push_mention())
{
// send Push notifications // send Push notifications
tasks::ack::queue_message( tasks::ack::queue_message(
self.channel.to_string(), self.channel.to_string(),
@@ -693,7 +537,7 @@ impl Message {
} }
_ => vec![], _ => vec![],
}, },
false, // branch already dictates this self.has_suppressed_notifications(),
)], )],
}, },
) )
@@ -730,24 +574,13 @@ impl Message {
/// Whether this message has suppressed notifications /// Whether this message has suppressed notifications
pub fn has_suppressed_notifications(&self) -> bool { pub fn has_suppressed_notifications(&self) -> bool {
if let Some(flags) = self.flags { if let Some(flags) = self.flags {
flags & MessageFlags::SuppressNotifications as u32 flags & MessageFlags::SuppressNotifications as i32
== MessageFlags::SuppressNotifications as u32 == MessageFlags::SuppressNotifications as i32
} else { } else {
false false
} }
} }
pub fn contains_mass_push_mention(&self) -> bool {
let ping = if let Some(flags) = self.flags {
let flags = MessageFlagsValue(flags);
flags.has(MessageFlags::MentionsEveryone)
} else {
false
};
ping || self.role_mentions.is_some()
}
/// Update message data /// Update message data
pub async fn update( pub async fn update(
&mut self, &mut self,
@@ -228,6 +228,7 @@ impl AbstractMessages for MongoDb {
"_id": id "_id": id
}, },
query, query,
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -246,6 +247,7 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): user format!("reactions.{emoji}"): user
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -264,6 +266,7 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): user format!("reactions.{emoji}"): user
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -282,6 +285,7 @@ impl AbstractMessages for MongoDb {
format!("reactions.{emoji}"): 1 format!("reactions.{emoji}"): 1
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -296,12 +300,15 @@ impl AbstractMessages for MongoDb {
/// Delete messages from a channel by their ids and corresponding channel id /// Delete messages from a channel by their ids and corresponding channel id
async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> { async fn delete_messages(&self, channel: &str, ids: &[String]) -> Result<()> {
self.col::<Document>(COL) self.col::<Document>(COL)
.delete_many(doc! { .delete_many(
"channel": channel, doc! {
"_id": { "channel": channel,
"$in": ids "_id": {
} "$in": ids
}) }
},
None,
)
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL)) .map_err(|_| create_database_error!("delete_many", COL))
@@ -355,6 +362,7 @@ impl MongoDb {
"deleted": true "deleted": true
} }
}, },
None,
) )
.await .await
.map_err(|_| create_database_error!("update_many", "attachments"))?; .map_err(|_| create_database_error!("update_many", "attachments"))?;
@@ -362,7 +370,7 @@ impl MongoDb {
// And then delete said messages. // And then delete said messages.
self.col::<Document>(COL) self.col::<Document>(COL)
.delete_many(projection) .delete_many(projection, None)
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| create_database_error!("delete_many", COL)) .map_err(|_| create_database_error!("delete_many", COL))
@@ -23,13 +23,16 @@ impl AbstractRatelimitEvents for MongoDb {
count: usize, count: usize,
) -> Result<bool> { ) -> Result<bool> {
self.col::<RatelimitEvent>(COL) self.col::<RatelimitEvent>(COL)
.count_documents(doc! { .count_documents(
"_id": { doc! {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string() "_id": {
"$gte": Ulid::from_datetime(SystemTime::now() - period).to_string()
},
"target_id": target_id,
"event_type": event_type.to_string()
}, },
"target_id": target_id, None,
"event_type": event_type.to_string() )
})
.await .await
.map(|c| c as usize >= count) .map(|c| c as usize >= count)
.map_err(|_| create_database_error!("count_documents", COL)) .map_err(|_| create_database_error!("count_documents", COL))
@@ -1,4 +1,3 @@
use ::mongodb::SessionCursor;
use revolt_result::Result; use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
@@ -6,66 +5,6 @@ use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
mod mongodb; mod mongodb;
mod reference; mod reference;
#[derive(Debug)]
pub enum ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
MongoDb {
session: ::mongodb::ClientSession,
cursor: Option<SessionCursor<Member>>,
},
Reference {
offset: i32,
data: Option<Vec<Member>>,
},
}
impl ChunkedServerMembersGenerator {
#[cfg(feature = "mongodb")]
pub fn new_mongo(session: ::mongodb::ClientSession, cursor: SessionCursor<Member>) -> Self {
ChunkedServerMembersGenerator::MongoDb {
session,
cursor: Some(cursor),
}
}
pub fn new_reference(data: Vec<Member>) -> Self {
ChunkedServerMembersGenerator::Reference {
offset: 0,
data: Some(data),
}
}
pub async fn next(&mut self) -> Option<Member> {
match self {
#[cfg(feature = "mongodb")]
ChunkedServerMembersGenerator::MongoDb { session, cursor } => {
if let Some(cursor) = cursor {
let value = cursor.next(session).await;
value.map(|val| val.expect("Failed to fetch the next member"))
} else {
warn!("Attempted to access a (MongoDb) server member generator without first setting a cursor");
None
}
}
ChunkedServerMembersGenerator::Reference { offset, data } => {
if let Some(data) = data {
if data.len() as i32 >= *offset {
None
} else {
let resp = &data[*offset as usize];
*offset += 1;
Some(resp.clone())
}
} else {
warn!("Attempted to access a (Reference) server member generator without first providing data");
None
}
}
}
}
}
#[async_trait] #[async_trait]
pub trait AbstractServerMembers: Sync + Send { pub trait AbstractServerMembers: Sync + Send {
/// Insert a new server member into the database /// Insert a new server member into the database
@@ -77,24 +16,6 @@ pub trait AbstractServerMembers: Sync + Send {
/// Fetch all members in a server /// Fetch all members in a server
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>; async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>>;
/// Fetch all members in a server as an iterator
async fn fetch_all_members_chunked(
&self,
server_id: &str,
) -> Result<ChunkedServerMembersGenerator>;
async fn fetch_all_members_with_roles(
&self,
server_id: &str,
roles: &[String],
) -> Result<Vec<Member>>;
async fn fetch_all_members_with_roles_chunked(
&self,
server_id: &str,
roles: &[String],
) -> Result<ChunkedServerMembersGenerator>;
/// Fetch all memberships for a user /// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>; async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>>;
@@ -1,11 +1,10 @@
use futures::StreamExt; use futures::StreamExt;
use mongodb::options::ReadConcern;
use revolt_result::Result; use revolt_result::Result;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use crate::{IntoDocumentPath, MongoDb}; use crate::{IntoDocumentPath, MongoDb};
use super::{AbstractServerMembers, ChunkedServerMembersGenerator}; use super::AbstractServerMembers;
static COL: &str = "server_members"; static COL: &str = "server_members";
@@ -34,9 +33,12 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> { async fn fetch_all_members<'a>(&self, server_id: &str) -> Result<Vec<Member>> {
Ok(self Ok(self
.col::<Member>(COL) .col::<Member>(COL)
.find(doc! { .find(
"_id.server": server_id doc! {
}) "_id.server": server_id
},
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -50,101 +52,16 @@ impl AbstractServerMembers for MongoDb {
.await) .await)
} }
/// Fetch all members in a server as a generator.
/// Uses config key pushd.mass_mention_chunk_size as the batch size.
async fn fetch_all_members_chunked(
&self,
server_id: &str,
) -> Result<ChunkedServerMembersGenerator> {
let config = revolt_config::config().await;
let mut session = self
.start_session()
.await
.map_err(|_| create_database_error!("start_session", COL))?;
session
.start_transaction()
.read_concern(ReadConcern::snapshot())
.await
.map_err(|_| create_database_error!("start_transaction", COL))?;
let cursor = self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id
})
.session(&mut session)
.batch_size(config.pushd.mass_mention_chunk_size as u32)
.await
.map_err(|_| create_database_error!("find", COL))?;
Ok(ChunkedServerMembersGenerator::new_mongo(session, cursor))
}
async fn fetch_all_members_with_roles(
&self,
server_id: &str,
roles: &[String],
) -> Result<Vec<Member>> {
Ok(self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id,
"roles": {"$in": roles}
})
.await
.map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async {
if cfg!(debug_assertions) {
Some(s.unwrap())
} else {
s.ok()
}
})
.collect()
.await)
}
async fn fetch_all_members_with_roles_chunked(
&self,
server_id: &str,
roles: &[String],
) -> Result<ChunkedServerMembersGenerator> {
let config = revolt_config::config().await;
let mut session = self
.start_session()
.await
.map_err(|_| create_database_error!("start_session", COL))?;
session
.start_transaction()
.read_concern(ReadConcern::snapshot())
.await
.map_err(|_| create_database_error!("start_transaction", COL))?;
let cursor = self
.col::<Member>(COL)
.find(doc! {
"_id.server": server_id,
"roles": {"$in": roles}
})
.session(&mut session)
.batch_size(config.pushd.mass_mention_chunk_size as u32)
.await
.map_err(|_| create_database_error!("find", COL))?;
return Ok(ChunkedServerMembersGenerator::new_mongo(session, cursor));
}
/// Fetch all memberships for a user /// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> { async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
Ok(self Ok(self
.col::<Member>(COL) .col::<Member>(COL)
.find(doc! { .find(
"_id.user": user_id doc! {
}) "_id.user": user_id
},
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -162,12 +79,15 @@ impl AbstractServerMembers for MongoDb {
async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> { async fn fetch_members<'a>(&self, server_id: &str, ids: &'a [String]) -> Result<Vec<Member>> {
Ok(self Ok(self
.col::<Member>(COL) .col::<Member>(COL)
.find(doc! { .find(
"_id.server": server_id, doc! {
"_id.user": { "_id.server": server_id,
"$in": ids "_id.user": {
} "$in": ids
}) }
},
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -184,9 +104,12 @@ impl AbstractServerMembers for MongoDb {
/// Fetch member count of a server /// Fetch member count of a server
async fn fetch_member_count(&self, server_id: &str) -> Result<usize> { async fn fetch_member_count(&self, server_id: &str) -> Result<usize> {
self.col::<Member>(COL) self.col::<Member>(COL)
.count_documents(doc! { .count_documents(
"_id.server": server_id doc! {
}) "_id.server": server_id
},
None,
)
.await .await
.map(|c| c as usize) .map(|c| c as usize)
.map_err(|_| create_database_error!("count_documents", COL)) .map_err(|_| create_database_error!("count_documents", COL))
@@ -195,9 +118,12 @@ impl AbstractServerMembers for MongoDb {
/// Fetch server count of a user /// Fetch server count of a user
async fn fetch_server_count(&self, user_id: &str) -> Result<usize> { async fn fetch_server_count(&self, user_id: &str) -> Result<usize> {
self.col::<Member>(COL) self.col::<Member>(COL)
.count_documents(doc! { .count_documents(
"_id.user": user_id doc! {
}) "_id.user": user_id
},
None,
)
.await .await
.map(|c| c as usize) .map(|c| c as usize)
.map_err(|_| create_database_error!("count_documents", COL)) .map_err(|_| create_database_error!("count_documents", COL))
@@ -3,7 +3,7 @@ use revolt_result::Result;
use crate::ReferenceDb; use crate::ReferenceDb;
use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember}; use crate::{FieldsMember, Member, MemberCompositeKey, PartialMember};
use super::{AbstractServerMembers, ChunkedServerMembersGenerator}; use super::AbstractServerMembers;
#[async_trait] #[async_trait]
impl AbstractServerMembers for ReferenceDb { impl AbstractServerMembers for ReferenceDb {
@@ -40,70 +40,6 @@ impl AbstractServerMembers for ReferenceDb {
.collect()) .collect())
} }
/// Fetch all members in a server as an iterator
async fn fetch_all_members_chunked(
&self,
server_id: &str,
) -> Result<ChunkedServerMembersGenerator> {
let server_members = self.server_members.lock().await;
let members = server_members
.clone()
.into_values()
.filter(move |member| member.id.server == server_id)
.collect();
// this is inefficient as shit but its the reference db so its fine
Ok(ChunkedServerMembersGenerator::new_reference(members))
}
/// Fetch all members that have any of the roles given
async fn fetch_all_members_with_roles(
&self,
server_id: &str,
roles: &[String],
) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await;
Ok(server_members
.clone()
.into_values()
.filter(|member| {
member.id.server == server_id
&& !member
.roles
.iter()
.filter(|p| roles.contains(*p))
.collect::<Vec<&String>>()
.is_empty()
})
.collect())
}
async fn fetch_all_members_with_roles_chunked(
&self,
server_id: &str,
roles: &[String],
) -> Result<ChunkedServerMembersGenerator> {
let server_members = self.server_members.lock().await;
let resp = server_members
.clone()
.into_values()
.filter(|member| {
member.id.server == server_id
&& !member
.roles
.iter()
.filter(|p| roles.contains(*p))
.collect::<Vec<&String>>()
.is_empty()
})
.collect();
return Ok(ChunkedServerMembersGenerator::new_reference(resp));
}
/// Fetch all memberships for a user /// Fetch all memberships for a user
async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> { async fn fetch_all_memberships<'a>(&self, user_id: &str) -> Result<Vec<Member>> {
let server_members = self.server_members.lock().await; let server_members = self.server_members.lock().await;
@@ -25,11 +25,14 @@ impl AbstractServers for MongoDb {
async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> { async fn fetch_servers<'a>(&self, ids: &'a [String]) -> Result<Vec<Server>> {
Ok(self Ok(self
.col::<Server>(COL) .col::<Server>(COL)
.find(doc! { .find(
"_id": { doc! {
"$in": ids "_id": {
} "$in": ids
}) }
},
None,
)
.await .await
.map_err(|_| create_database_error!("find", "servers"))? .map_err(|_| create_database_error!("find", "servers"))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -81,6 +84,7 @@ impl AbstractServers for MongoDb {
.map_err(|_| create_database_error!("to_document", "role"))? .map_err(|_| create_database_error!("to_document", "role"))?
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -121,6 +125,7 @@ impl AbstractServers for MongoDb {
"roles": &role_id "roles": &role_id
} }
}, },
None,
) )
.await .await
.map_err(|_| create_database_error!("update_many", "server_members"))?; .map_err(|_| create_database_error!("update_many", "server_members"))?;
@@ -135,6 +140,7 @@ impl AbstractServers for MongoDb {
"role_permissions.".to_owned() + role_id: 1_i32 "role_permissions.".to_owned() + role_id: 1_i32
} }
}, },
None,
) )
.await .await
.map_err(|_| create_database_error!("update_one", "channels"))?; .map_err(|_| create_database_error!("update_one", "channels"))?;
@@ -149,6 +155,7 @@ impl AbstractServers for MongoDb {
"roles.".to_owned() + role_id: 1_i32 "roles.".to_owned() + role_id: 1_i32
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -181,9 +188,12 @@ impl MongoDb {
// Find all channels // Find all channels
let channels: Vec<String> = self let channels: Vec<String> = self
.col::<Document>("channels") .col::<Document>("channels")
.find(doc! { .find(
"server": server_id doc! {
}) "server": server_id
},
None,
)
.await .await
.map_err(|_| create_database_error!("find", "channels"))? .map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -215,15 +225,19 @@ impl MongoDb {
} }
} }
}, },
None,
) )
.await .await
.map_err(|_| create_database_error!("update_many", "emojis"))?; .map_err(|_| create_database_error!("update_many", "emojis"))?;
// Delete all channels. // Delete all channels.
self.col::<Document>("channels") self.col::<Document>("channels")
.delete_many(doc! { .delete_many(
"server": &server_id doc! {
}) "server": &server_id
},
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", "channels"))?; .map_err(|_| create_database_error!("delete_many", "channels"))?;
@@ -234,9 +248,12 @@ impl MongoDb {
// Delete members and bans. // Delete members and bans.
for with in &["server_members", "server_bans"] { for with in &["server_members", "server_bans"] {
self.col::<Document>(with) self.col::<Document>(with)
.delete_many(doc! { .delete_many(
"_id.server": &server_id doc! {
}) "_id.server": &server_id
},
None,
)
.await .await
.map_err(|_| create_database_error!("delete_many", with))?; .map_err(|_| create_database_error!("delete_many", with))?;
} }
@@ -53,8 +53,8 @@ impl AbstractUserSettings for MongoDb {
doc! { doc! {
"$set": set "$set": set
}, },
UpdateOptions::builder().upsert(true).build(),
) )
.with_options(UpdateOptions::builder().upsert(true).build())
.await .await
.map(|_| ()) .map(|_| ())
.map_err(|_| create_database_error!("update_one", "user_settings")) .map_err(|_| create_database_error!("update_one", "user_settings"))
+5 -21
View File
@@ -3,12 +3,11 @@ use std::{collections::HashSet, str::FromStr, time::Duration};
use crate::{events::client::EventV1, Database, File, RatelimitEvent, AMQP}; use crate::{events::client::EventV1, Database, File, RatelimitEvent, AMQP};
use authifier::config::{EmailVerificationConfig, Template}; use authifier::config::{EmailVerificationConfig, Template};
use futures::future::join_all;
use iso8601_timestamp::Timestamp; use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use revolt_config::{config, FeaturesLimits}; use revolt_config::{config, FeaturesLimits};
use revolt_models::v0::{self, UserFlags, UserBadges}; use revolt_models::v0::{self, UserFlags};
use revolt_presence::filter_online; use revolt_presence::filter_online;
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
use serde_json::json; use serde_json::json;
@@ -349,16 +348,15 @@ impl User {
) -> Result<Vec<v0::User>> { ) -> Result<Vec<v0::User>> {
let online_ids = filter_online(ids).await; let online_ids = filter_online(ids).await;
Ok(join_all( Ok(db
db
.fetch_users(ids) .fetch_users(ids)
.await? .await?
.into_iter() .into_iter()
.map(|user| async { .map(|user| {
let is_online = online_ids.contains(&user.id); let is_online = online_ids.contains(&user.id);
user.into_known(perspective, is_online).await user.into_known(perspective, is_online)
}) })
).await) .collect())
} }
/// Find a free discriminator for a given username /// Find a free discriminator for a given username
@@ -809,18 +807,4 @@ impl User {
) )
.await .await
} }
/// Gets the user's badges along with calculating any dynamic badges
pub async fn get_badges(&self) -> u32 {
let config = config().await;
let badges = self.badges.unwrap_or_default() as u32;
if let Some(cutoff) = config.api.users.early_adopter_cutoff {
if Ulid::from_string(&self.id).unwrap().timestamp_ms() < cutoff {
return badges + UserBadges::EarlyAdopter as u32
};
};
badges
}
} }
@@ -49,9 +49,12 @@ impl AbstractUsers for MongoDb {
/// Fetch a session from the database by token /// Fetch a session from the database by token
async fn fetch_session_by_token(&self, token: &str) -> Result<Session> { async fn fetch_session_by_token(&self, token: &str) -> Result<Session> {
self.col::<Session>("sessions") self.col::<Session>("sessions")
.find_one(doc! { .find_one(
"token": token doc! {
}) "token": token
},
None,
)
.await .await
.map_err(|_| create_database_error!("find_one", "sessions"))? .map_err(|_| create_database_error!("find_one", "sessions"))?
.ok_or_else(|| create_error!(InvalidSession)) .ok_or_else(|| create_error!(InvalidSession))
@@ -61,11 +64,14 @@ impl AbstractUsers for MongoDb {
async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> { async fn fetch_users<'a>(&self, ids: &'a [String]) -> Result<Vec<User>> {
Ok(self Ok(self
.col::<User>(COL) .col::<User>(COL)
.find(doc! { .find(
"_id": { doc! {
"$in": ids "_id": {
} "$in": ids
}) }
},
None,
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { .filter_map(|s| async {
@@ -88,10 +94,10 @@ impl AbstractUsers for MongoDb {
Ok(self Ok(self
.col::<UserDocument>(COL) .col::<UserDocument>(COL)
.find(doc! { .find(
"username": username doc! {
}) "username": username
.with_options( },
FindOptions::builder() FindOptions::builder()
.collation( .collation(
Collation::builder() Collation::builder()
@@ -116,13 +122,15 @@ impl AbstractUsers for MongoDb {
async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> { async fn fetch_mutual_user_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self Ok(self
.col::<DocumentId>(COL) .col::<DocumentId>(COL)
.find(doc! { .find(
"$and": [ doc! {
{ "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } }, "$and": [
{ "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } } { "relations": { "$elemMatch": { "_id": &user_a, "status": "Friend" } } },
] { "relations": { "$elemMatch": { "_id": &user_b, "status": "Friend" } } }
}) ]
.with_options(FindOptions::builder().projection(doc! { "_id": 1 }).build()) },
FindOptions::builder().projection(doc! { "_id": 1 }).build(),
)
.await .await
.map_err(|_| create_database_error!("find", COL))? .map_err(|_| create_database_error!("find", COL))?
.filter_map(|s| async { s.ok() }) .filter_map(|s| async { s.ok() })
@@ -135,15 +143,17 @@ impl AbstractUsers for MongoDb {
async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> { async fn fetch_mutual_channel_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self Ok(self
.col::<DocumentId>("channels") .col::<DocumentId>("channels")
.find(doc! { .find(
"channel_type": { doc! {
"$in": ["Group", "DirectMessage"] "channel_type": {
"$in": ["Group", "DirectMessage"]
},
"recipients": {
"$all": [ user_a, user_b ]
}
}, },
"recipients": { FindOptions::builder().projection(doc! { "_id": 1 }).build(),
"$all": [ user_a, user_b ] )
}
})
.with_options(FindOptions::builder().projection(doc! { "_id": 1 }).build())
.await .await
.map_err(|_| create_database_error!("find", "channels"))? .map_err(|_| create_database_error!("find", "channels"))?
.filter_map(|s| async { s.ok() }) .filter_map(|s| async { s.ok() })
@@ -156,46 +166,49 @@ impl AbstractUsers for MongoDb {
async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> { async fn fetch_mutual_server_ids(&self, user_a: &str, user_b: &str) -> Result<Vec<String>> {
Ok(self Ok(self
.col::<DocumentId>("server_members") .col::<DocumentId>("server_members")
.aggregate(vec![ .aggregate(
doc! { vec![
"$match": { doc! {
"_id.user": user_a "$match": {
} "_id.user": user_a
}, }
doc! { },
"$lookup": { doc! {
"from": "server_members", "$lookup": {
"as": "members", "from": "server_members",
"let": { "as": "members",
"server": "$_id.server" "let": {
}, "server": "$_id.server"
"pipeline": [ },
{ "pipeline": [
"$match": { {
"$expr": { "$match": {
"$and": [ "$expr": {
{ "$eq": [ "$_id.user", user_b ] }, "$and": [
{ "$eq": [ "$_id.server", "$$server" ] } { "$eq": [ "$_id.user", user_b ] },
] { "$eq": [ "$_id.server", "$$server" ] }
]
}
} }
} }
} ]
]
}
},
doc! {
"$match": {
"members": {
"$size": 1_i32
} }
} },
}, doc! {
doc! { "$match": {
"$project": { "members": {
"_id": "$_id.server" "$size": 1_i32
} }
}, }
]) },
doc! {
"$project": {
"_id": "$_id.server"
}
},
],
None,
)
.await .await
.map_err(|_| create_database_error!("aggregate", "server_members"))? .map_err(|_| create_database_error!("aggregate", "server_members"))?
.filter_map(|s| async { s.ok() }) .filter_map(|s| async { s.ok() })
@@ -271,6 +284,7 @@ impl AbstractUsers for MongoDb {
} }
} }
}], }],
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -291,6 +305,7 @@ impl AbstractUsers for MongoDb {
} }
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
@@ -314,6 +329,7 @@ impl AbstractUsers for MongoDb {
"subscription": 1 "subscription": 1
} }
}, },
None,
) )
.await .await
.map(|_| ()) .map(|_| ())
+9 -55
View File
@@ -14,7 +14,6 @@ use validator::HasLen;
use revolt_result::Result; use revolt_result::Result;
use super::DelayedTask; use super::DelayedTask;
use crate::Channel::{TextChannel, VoiceChannel};
/// Enumeration of possible events /// Enumeration of possible events
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
@@ -121,22 +120,17 @@ pub async fn handle_ack_event(
); );
// find all the users we'll be notifying // find all the users we'll be notifying
messages.iter().for_each(|(_, _, recipents, _)| { messages
users.extend(recipents.iter()); .iter()
}); .for_each(|(_, _, recipents, _)| users.extend(recipents.iter()));
debug!("Found {} users to notify.", users.len()); debug!("Found {} users to notify.", users.len());
for user in users { for user in users {
let message_ids: Vec<String> = messages let message_ids: Vec<String> = messages
.iter() .iter()
.filter_map(|(_, message, recipients, _)| { .filter(|(_, _, recipients, _)| recipients.contains(user))
if recipients.contains(user) { .map(|(_, message, _, _)| message.id.clone())
Some(message.id.clone())
} else {
None
}
})
.collect(); .collect();
if !message_ids.is_empty() { if !message_ids.is_empty() {
@@ -146,18 +140,13 @@ pub async fn handle_ack_event(
debug!("Added {} mentions for user {}", message_ids.len(), &user); debug!("Added {} mentions for user {}", message_ids.len(), &user);
} }
let mut mass_mentions = vec![]; for (push, _, recipients, silenced) in messages {
if *silenced || recipients.is_empty() || push.is_none() {
for (push, message, recipients, silenced) in messages {
if *silenced
|| push.is_none()
|| (recipients.is_empty() && !message.contains_mass_push_mention())
{
debug!( debug!(
"Rejecting push: silenced: {}, recipient count: {}, push exists: {:?}", "Rejecting push: silenced: {}, recipient count: {}, push exists: {:?}",
*silenced, *silenced,
recipients.length(), recipients.length(),
push.is_some() push
); );
continue; continue;
} }
@@ -173,35 +162,6 @@ pub async fn handle_ack_event(
{ {
revolt_config::capture_error(&err); revolt_config::capture_error(&err);
} }
if message.contains_mass_push_mention() {
mass_mentions.push(push.clone().unwrap());
}
}
if !mass_mentions.is_empty() {
debug!(
"Sending mass mention push event to AMQP; channel {}",
&mass_mentions[0].message.channel
);
let channel = db
.fetch_channel(&mass_mentions[0].message.channel)
.await
.expect("Failed to fetch channel from db");
match channel {
TextChannel { server, .. } | VoiceChannel { server, .. } => {
if let Err(err) =
amqp.mass_mention_message_sent(server, mass_mentions).await
{
revolt_config::capture_error(&err);
}
}
_ => {
panic!("Unknown channel type when sending mass mention event");
}
}
} }
} }
}; };
@@ -232,7 +192,7 @@ pub async fn worker(db: Database, amqp: AMQP) {
revolt_config::capture_error(&err); revolt_config::capture_error(&err);
error!("{err:?} for {event:?}. ({user:?}, {channel})"); error!("{err:?} for {event:?}. ({user:?}, {channel})");
} else { } else {
debug!("User {user:?} ack in {channel} with {event:?}"); info!("User {user:?} ack in {channel} with {event:?}");
} }
} }
} }
@@ -264,12 +224,6 @@ pub async fn worker(db: Database, amqp: AMQP) {
// add the new message to the list of messages to be processed. // add the new message to the list of messages to be processed.
existing.append(new_data); existing.append(new_data);
// if the message contains a mass mention, do not delay it any further.
if new_data[0].1.contains_mass_push_mention() {
task.run_immediately();
continue;
}
// put a cap on the amount of messages that can be queued, for particularly active channels // put a cap on the amount of messages that can be queued, for particularly active channels
if (existing.length() as u16) if (existing.length() as u16)
< revolt_config::config() < revolt_config::config()
+1 -9
View File
@@ -26,7 +26,6 @@ pub fn start_workers(db: Database, amqp: AMQP) {
/// Task with additional information on when it should run /// Task with additional information on when it should run
pub struct DelayedTask<T> { pub struct DelayedTask<T> {
pub data: T, pub data: T,
run_now: bool,
last_updated: Instant, last_updated: Instant,
first_seen: Instant, first_seen: Instant,
} }
@@ -42,7 +41,6 @@ impl<T> DelayedTask<T> {
pub fn new(data: T) -> Self { pub fn new(data: T) -> Self {
DelayedTask { DelayedTask {
data, data,
run_now: false,
last_updated: Instant::now(), last_updated: Instant::now(),
first_seen: Instant::now(), first_seen: Instant::now(),
} }
@@ -53,15 +51,9 @@ impl<T> DelayedTask<T> {
self.last_updated = Instant::now() self.last_updated = Instant::now()
} }
/// Flag the task to run right away, regardless of the time
pub fn run_immediately(&mut self) {
self.run_now = true
}
/// Check if a task should run yet /// Check if a task should run yet
pub fn should_run(&self) -> bool { pub fn should_run(&self) -> bool {
self.run_now self.first_seen.elapsed().as_secs() > EXPIRE_CONSTANT
|| self.first_seen.elapsed().as_secs() > EXPIRE_CONSTANT
|| self.last_updated.elapsed().as_secs() > SAVE_CONSTANT || self.last_updated.elapsed().as_secs() > SAVE_CONSTANT
} }
} }
+16 -38
View File
@@ -483,7 +483,6 @@ impl crate::Message {
edited: self.edited, edited: self.edited,
embeds: self.embeds, embeds: self.embeds,
mentions: self.mentions, mentions: self.mentions,
role_mentions: self.role_mentions,
replies: self.replies, replies: self.replies,
reactions: self.reactions, reactions: self.reactions,
interactions: self.interactions.into(), interactions: self.interactions.into(),
@@ -512,7 +511,6 @@ impl From<crate::PartialMessage> for PartialMessage {
edited: value.edited, edited: value.edited,
embeds: value.embeds, embeds: value.embeds,
mentions: value.mentions, mentions: value.mentions,
role_mentions: value.role_mentions,
replies: value.replies, replies: value.replies,
reactions: value.reactions, reactions: value.reactions,
interactions: value.interactions.map(Into::into), interactions: value.interactions.map(Into::into),
@@ -994,8 +992,6 @@ impl crate::User {
(RelationshipStatus::None, false) (RelationshipStatus::None, false)
}; };
let badges = self.get_badges().await;
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator, discriminator: self.discriminator,
@@ -1014,7 +1010,7 @@ impl crate::User {
} else { } else {
vec![] vec![]
}, },
badges, badges: self.badges.unwrap_or_default() as u32,
online: can_see_profile online: can_see_profile
&& revolt_presence::is_online(&self.id).await && revolt_presence::is_online(&self.id).await
&& !matches!( && !matches!(
@@ -1025,7 +1021,7 @@ impl crate::User {
}) })
), ),
status: if can_see_profile { status: if can_see_profile {
self.status.and_then(|status| status.into(true)) self.status.map(|status| status.into())
} else { } else {
None None
}, },
@@ -1040,7 +1036,7 @@ impl crate::User {
/// Convert user object into user model assuming mutual connection /// Convert user object into user model assuming mutual connection
/// ///
/// Relations will never be included, i.e. when we process ourselves /// Relations will never be included, i.e. when we process ourselves
pub async fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User pub fn into_known<'a, P>(self, perspective: P, is_online: bool) -> User
where where
P: Into<Option<&'a crate::User>>, P: Into<Option<&'a crate::User>>,
{ {
@@ -1070,15 +1066,13 @@ impl crate::User {
(RelationshipStatus::None, false) (RelationshipStatus::None, false)
}; };
let badges = self.get_badges().await;
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator, discriminator: self.discriminator,
display_name: self.display_name, display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()), avatar: self.avatar.map(|file| file.into()),
relations: vec![], relations: vec![],
badges, badges: self.badges.unwrap_or_default() as u32,
online: can_see_profile online: can_see_profile
&& is_online && is_online
&& !matches!( && !matches!(
@@ -1089,7 +1083,7 @@ impl crate::User {
}) })
), ),
status: if can_see_profile { status: if can_see_profile {
self.status.and_then(|status| status.into(true)) self.status.map(|status| status.into())
} else { } else {
None None
}, },
@@ -1102,16 +1096,14 @@ impl crate::User {
} }
/// Convert user object into user model without presence information /// Convert user object into user model without presence information
pub async fn into_known_static<'a>(self, is_online: bool) -> User { pub fn into_known_static<'a>(self, is_online: bool) -> User {
let badges = self.get_badges().await;
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator, discriminator: self.discriminator,
display_name: self.display_name, display_name: self.display_name,
avatar: self.avatar.map(|file| file.into()), avatar: self.avatar.map(|file| file.into()),
relations: vec![], relations: vec![],
badges, badges: self.badges.unwrap_or_default() as u32,
online: is_online online: is_online
&& !matches!( && !matches!(
self.status, self.status,
@@ -1120,7 +1112,7 @@ impl crate::User {
.. ..
}) })
), ),
status: self.status.and_then(|status| status.into(true)), status: self.status.map(|status| status.into()),
flags: self.flags.unwrap_or_default() as u32, flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged, privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()), bot: self.bot.map(|bot| bot.into()),
@@ -1130,8 +1122,6 @@ impl crate::User {
} }
pub async fn into_self(self, force_online: bool) -> User { pub async fn into_self(self, force_online: bool) -> User {
let badges = self.get_badges().await;
User { User {
username: self.username, username: self.username,
discriminator: self.discriminator, discriminator: self.discriminator,
@@ -1146,7 +1136,7 @@ impl crate::User {
.collect() .collect()
}) })
.unwrap_or_default(), .unwrap_or_default(),
badges, badges: self.badges.unwrap_or_default() as u32,
online: (force_online || revolt_presence::is_online(&self.id).await) online: (force_online || revolt_presence::is_online(&self.id).await)
&& !matches!( && !matches!(
self.status, self.status,
@@ -1155,7 +1145,7 @@ impl crate::User {
.. ..
}) })
), ),
status: self.status.and_then(|status| status.into(true)), status: self.status.map(|status| status.into()),
flags: self.flags.unwrap_or_default() as u32, flags: self.flags.unwrap_or_default() as u32,
privileged: self.privileged, privileged: self.privileged,
bot: self.bot.map(|bot| bot.into()), bot: self.bot.map(|bot| bot.into()),
@@ -1206,7 +1196,7 @@ impl From<crate::PartialUser> for PartialUser {
.collect() .collect()
}), }),
badges: value.badges.map(|badges| badges as u32), badges: value.badges.map(|badges| badges as u32),
status: value.status.and_then(|status| status.into(false)), status: value.status.map(|status| status.into()),
flags: value.flags.map(|flags| flags as u32), flags: value.flags.map(|flags| flags as u32),
privileged: value.privileged, privileged: value.privileged,
bot: value.bot.map(|bot| bot.into()), bot: value.bot.map(|bot| bot.into()),
@@ -1295,23 +1285,11 @@ impl From<Presence> for crate::Presence {
} }
} }
impl crate::UserStatus { impl From<crate::UserStatus> for UserStatus {
fn into(self, discard_invisible: bool) -> Option<UserStatus> { fn from(value: crate::UserStatus) -> Self {
let status = UserStatus { UserStatus {
text: self.text, text: value.text,
presence: self.presence.and_then(|presence| { presence: value.presence.map(|presence| presence.into()),
if discard_invisible && presence == crate::Presence::Invisible {
None
} else {
Some(presence.into())
}
}),
};
if status.text.is_none() && status.presence.is_none() {
None
} else {
Some(status)
} }
} }
} }
@@ -94,28 +94,9 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
} }
} }
pub async fn from_channel_id(self, channel_id: String) -> BulkDatabasePermissionQuery<'z> {
let channel = self
.database
.fetch_channel(channel_id.as_str())
.await
.expect("Valid channel id");
drop(channel_id);
BulkDatabasePermissionQuery {
channel: Some(channel),
..self
}
}
pub fn members(self, members: &'z [Member]) -> BulkDatabasePermissionQuery { pub fn members(self, members: &'z [Member]) -> BulkDatabasePermissionQuery {
BulkDatabasePermissionQuery { BulkDatabasePermissionQuery {
members: Some(members.to_owned()), members: Some(members.to_owned()),
cached_member_perms: None,
users: None,
cached_members: None,
cached_users: None,
..self ..self
} }
} }
@@ -123,10 +104,6 @@ impl<'z> BulkDatabasePermissionQuery<'z> {
pub fn users(self, users: &'z [User]) -> BulkDatabasePermissionQuery { pub fn users(self, users: &'z [User]) -> BulkDatabasePermissionQuery {
BulkDatabasePermissionQuery { BulkDatabasePermissionQuery {
users: Some(users.to_owned()), users: Some(users.to_owned()),
cached_member_perms: None,
members: None,
cached_members: None,
cached_users: None,
..self ..self
} }
} }
+453 -74
View File
@@ -1,93 +1,464 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html> <html>
<head> <head>
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge"> <!-- Compiled with Bootstrap Email version: 1.3.1 -->
<meta name="x-apple-disable-message-reformatting"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="x-apple-disable-message-reformatting" />
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta
name="format-detection"
content="telephone=no, date=no, address=no, email=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css"> <style type="text/css">
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}} body,
table,
td {
font-family: Helvetica, Arial, sans-serif !important;
}
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 150%;
}
a {
text-decoration: none;
}
* {
color: inherit;
}
a[x-apple-data-detectors],
u + #body a,
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
img {
-ms-interpolation-mode: bicubic;
}
table:not([class^="s-"]) {
font-family: Helvetica, Arial, sans-serif;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border-spacing: 0px;
border-collapse: collapse;
}
table:not([class^="s-"]) td {
border-spacing: 0px;
border-collapse: collapse;
}
@media screen and (max-width: 600px) {
.w-full,
.w-full > tbody > tr > td {
width: 100% !important;
}
.w-24,
.w-24 > tbody > tr > td {
width: 96px !important;
}
.p-lg-10:not(table),
.p-lg-10:not(.btn) > tbody > tr > td,
.p-lg-10.btn td a {
padding: 0 !important;
}
.p-3:not(table),
.p-3:not(.btn) > tbody > tr > td,
.p-3.btn td a {
padding: 12px !important;
}
.p-6:not(table),
.p-6:not(.btn) > tbody > tr > td,
.p-6.btn td a {
padding: 24px !important;
}
*[class*="s-lg-"] > tbody > tr > td {
font-size: 0 !important;
line-height: 0 !important;
height: 0 !important;
}
.s-4 > tbody > tr > td {
font-size: 16px !important;
line-height: 16px !important;
height: 16px !important;
}
.s-6 > tbody > tr > td {
font-size: 24px !important;
line-height: 24px !important;
height: 24px !important;
}
.s-10 > tbody > tr > td {
font-size: 40px !important;
line-height: 40px !important;
height: 40px !important;
}
}
</style> </style>
</head> </head>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc"> <body
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc"> class="bg-light"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<table
class="bg-light body"
valign="top"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<tbody> <tbody>
<tr> <tr>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc"> <td
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;"> valign="top"
style="line-height: 24px; font-size: 16px; margin: 0"
align="left"
bgcolor="#f7fafc"
>
<table
class="container"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
>
<tbody> <tbody>
<tr> <tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;"> <td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<!--[if (gte mso 9)|(IE)]> <!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation"> <table align="center" role="presentation">
<tbody> <tbody>
<tr> <tr>
<td width="600"> <td width="600">
<![endif]--> <![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;"> <table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40"> <td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;"> <table
class="ax-center"
role="presentation"
align="center"
border="0"
cellpadding="0"
cellspacing="0"
style="margin: 0 auto"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96"> style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<img
alt="Revolt Logo"
class="w-24"
src="https://app.revolt.chat/assets/logo_round.png"
style="
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
display: block;
width: 96px;
border-style: none;
border-width: 0;
"
width="96"
/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40"> <td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff"> <table
class="card p-6 p-lg-10 space-y-4"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
width: 100%;
overflow: hidden;
border: 1px solid #e2e8f0;
"
bgcolor="#ffffff"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff"> <td
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Account Deletion</h1> style="
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
padding: 40px;
"
align="left"
bgcolor="#ffffff"
>
<h1
class="h3 fw-700"
style="
padding-top: 0;
padding-bottom: 0;
font-weight: 700 !important;
vertical-align: baseline;
font-size: 28px;
line-height: 33.6px;
margin: 0;
"
align="left"
>
Account Deletion
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left"> <p
You requested to have your account deleted, if you did not perform class=""
this action please take measures to secure your account immediately. style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
You requested to have your account
deleted, if you did not perform this
action please take measures to secure your
account immediately.
</p> </p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;"> <table
class="btn btn-primary p-3 fw-700"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
font-weight: 700 !important;
"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd"> <td
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Confirm</a> style="
line-height: 24px;
font-size: 16px;
border-radius: 6px;
font-weight: 700 !important;
margin: 0;
"
align="center"
bgcolor="#0d6efd"
>
<a
href="{{url}}"
style="
color: #ffffff;
font-size: 16px;
font-family: Helvetica, Arial,
sans-serif;
text-decoration: none;
border-radius: 6px;
line-height: 20px;
display: block;
font-weight: 700 !important;
white-space: nowrap;
background-color: #0d6efd;
padding: 12px;
border: 1px solid #0d6efd;
"
>Confirm</a
>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -96,58 +467,66 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> <td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="text-muted text-center" style="color: #718096;" align="center"> <div
This email is intended for {{email}}<br> class="text-muted text-center"
Sent from Revolt<br> style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe Made in Europe
</div> </div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> <td
&#160; style="
</td> line-height: 24px;
</tr> font-size: 24px;
</tbody> width: 100%;
</table> height: 24px;
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> margin: 0;
<tbody> "
<tr> align="left"
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> width="100%"
&#160; height="24"
</td> >
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<div class="text-muted text-xs" style="color: #718096; font-size: 12px; line-height: 14.4px;">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br>
Registered Company Number: 16260658<br>
Registered Office:<br>
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br>
Holborn, United Kingdom, EC1N 8DX
</div>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160; &#160;
</td> </td>
</tr> </tr>
@@ -25,14 +25,6 @@
Sent from Revolt<br /> Sent from Revolt<br />
Made in Europe Made in Europe
</div> </div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div> </div>
</body> </body>
</html> </html>
@@ -5,10 +5,3 @@ Please navigate to: {{url}}
This email is intended for {{email}} This email is intended for {{email}}
Sent by Revolt Sent by Revolt
Made in Europe Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
Registration Number: 16260658
Registered Office:
Suite 5703 Unit 3A, 34-35 Hatton Garden,
Holborn, United Kingdom, EC1N 8DX
@@ -1,9 +0,0 @@
You requested to have your account deleted, if you did not perform this action please take measures to secure your account immediately.
Please navigate to: {{url}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html
@@ -1,53 +0,0 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
/* Add custom classes and styles that you want inlined here */
</style>
</head>
<body class="bg-light">
<div class="container">
<img
alt="Revolt Logo"
class="ax-center my-10 w-24"
src="https://app.revolt.chat/assets/logo_round.png"
/>
<div class="card p-6 p-lg-10 space-y-4">
<h1 class="h3 fw-700">Notice of Terms &amp; Policy Change</h1>
<p>
This email is to notify you that we're updating all of our policy
documents.
</p>
<p>
You can read our updated Terms of Service and Privacy Policy below:<br />
Effective. 24th March 2025
</p>
<a class="p-3 fw-700" href="https://revolt.chat/terms"
>Terms of Service</a
>
<a class="p-3 fw-700" href="https://revolt.chat/terms"
>Privacy Policy</a
>
<p>
Our Community Guidelines have also been updated to help us better
protect and safeguard the platform. You can check them out here:
</p>
<a class="p-3 fw-700" href="https://revolt.chat/terms"
>Community Guidelines</a
>
</div>
<div class="text-muted text-center my-6">
Sent by Revolt<br />
Made in Europe
</div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div>
</body>
</html>
+452 -72
View File
@@ -1,90 +1,462 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html> <html>
<head> <head>
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge"> <!-- Compiled with Bootstrap Email version: 1.3.1 -->
<meta name="x-apple-disable-message-reformatting"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="x-apple-disable-message-reformatting" />
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta
name="format-detection"
content="telephone=no, date=no, address=no, email=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css"> <style type="text/css">
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}} body,
table,
td {
font-family: Helvetica, Arial, sans-serif !important;
}
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 150%;
}
a {
text-decoration: none;
}
* {
color: inherit;
}
a[x-apple-data-detectors],
u + #body a,
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
img {
-ms-interpolation-mode: bicubic;
}
table:not([class^="s-"]) {
font-family: Helvetica, Arial, sans-serif;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border-spacing: 0px;
border-collapse: collapse;
}
table:not([class^="s-"]) td {
border-spacing: 0px;
border-collapse: collapse;
}
@media screen and (max-width: 600px) {
.w-full,
.w-full > tbody > tr > td {
width: 100% !important;
}
.w-24,
.w-24 > tbody > tr > td {
width: 96px !important;
}
.p-lg-10:not(table),
.p-lg-10:not(.btn) > tbody > tr > td,
.p-lg-10.btn td a {
padding: 0 !important;
}
.p-3:not(table),
.p-3:not(.btn) > tbody > tr > td,
.p-3.btn td a {
padding: 12px !important;
}
.p-6:not(table),
.p-6:not(.btn) > tbody > tr > td,
.p-6.btn td a {
padding: 24px !important;
}
*[class*="s-lg-"] > tbody > tr > td {
font-size: 0 !important;
line-height: 0 !important;
height: 0 !important;
}
.s-4 > tbody > tr > td {
font-size: 16px !important;
line-height: 16px !important;
height: 16px !important;
}
.s-6 > tbody > tr > td {
font-size: 24px !important;
line-height: 24px !important;
height: 24px !important;
}
.s-10 > tbody > tr > td {
font-size: 40px !important;
line-height: 40px !important;
height: 40px !important;
}
}
</style> </style>
</head> </head>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc"> <body
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc"> class="bg-light"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<table
class="bg-light body"
valign="top"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<tbody> <tbody>
<tr> <tr>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc"> <td
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;"> valign="top"
style="line-height: 24px; font-size: 16px; margin: 0"
align="left"
bgcolor="#f7fafc"
>
<table
class="container"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
>
<tbody> <tbody>
<tr> <tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;"> <td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<!--[if (gte mso 9)|(IE)]> <!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation"> <table align="center" role="presentation">
<tbody> <tbody>
<tr> <tr>
<td width="600"> <td width="600">
<![endif]--> <![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;"> <table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40"> <td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;"> <table
class="ax-center"
role="presentation"
align="center"
border="0"
cellpadding="0"
cellspacing="0"
style="margin: 0 auto"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td
<img class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96"> style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<img
alt="Revolt Logo"
class="w-24"
src="https://app.revolt.chat/assets/logo_round.png"
style="
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
display: block;
width: 96px;
border-style: none;
border-width: 0;
"
width="96"
/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40"> <td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff"> <table
class="card p-6 p-lg-10 space-y-4"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
width: 100%;
overflow: hidden;
border: 1px solid #e2e8f0;
"
bgcolor="#ffffff"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff"> <td
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Password Reset</h1> style="
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
padding: 40px;
"
align="left"
bgcolor="#ffffff"
>
<h1
class="h3 fw-700"
style="
padding-top: 0;
padding-bottom: 0;
font-weight: 700 !important;
vertical-align: baseline;
font-size: 28px;
line-height: 33.6px;
margin: 0;
"
align="left"
>
Password Reset
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">You requested a password reset, click below to continue.</p> <p
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> class=""
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
You requested a password reset, click
below to continue.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;"> <table
class="btn btn-primary p-3 fw-700"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
font-weight: 700 !important;
"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd"> <td
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Reset</a> style="
line-height: 24px;
font-size: 16px;
border-radius: 6px;
font-weight: 700 !important;
margin: 0;
"
align="center"
bgcolor="#0d6efd"
>
<a
href="{{url}}"
style="
color: #ffffff;
font-size: 16px;
font-family: Helvetica, Arial,
sans-serif;
text-decoration: none;
border-radius: 6px;
line-height: 20px;
display: block;
font-weight: 700 !important;
white-space: nowrap;
background-color: #0d6efd;
padding: 12px;
border: 1px solid #0d6efd;
"
>Reset</a
>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -93,58 +465,66 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> <td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="text-muted text-center" style="color: #718096;" align="center"> <div
This email is intended for {{email}}<br> class="text-muted text-center"
Sent from Revolt<br> style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe Made in Europe
</div> </div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> <td
&#160; style="
</td> line-height: 24px;
</tr> font-size: 24px;
</tbody> width: 100%;
</table> height: 24px;
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> margin: 0;
<tbody> "
<tr> align="left"
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> width="100%"
&#160; height="24"
</td> >
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<div class="text-muted text-xs" style="color: #718096; font-size: 12px; line-height: 14.4px;">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br>
Registered Company Number: 16260658<br>
Registered Office:<br>
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br>
Holborn, United Kingdom, EC1N 8DX
</div>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160; &#160;
</td> </td>
</tr> </tr>
@@ -21,14 +21,6 @@
Sent from Revolt<br /> Sent from Revolt<br />
Made in Europe Made in Europe
</div> </div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div> </div>
</body> </body>
</html> </html>
-7
View File
@@ -5,10 +5,3 @@ Please navigate to: {{url}}
This email is intended for {{email}} This email is intended for {{email}}
Sent by Revolt Sent by Revolt
Made in Europe Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
Registration Number: 16260658
Registered Office:
Suite 5703 Unit 3A, 34-35 Hatton Garden,
Holborn, United Kingdom, EC1N 8DX
@@ -1,9 +0,0 @@
You requested a password reset, if you did not perform this action you can safely ignore this email.
Please navigate to: {{url}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html
+505 -82
View File
@@ -1,80 +1,408 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html> <html>
<head> <head>
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge"> <!-- Compiled with Bootstrap Email version: 1.5.1 -->
<meta name="x-apple-disable-message-reformatting"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="x-apple-disable-message-reformatting" />
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta
name="format-detection"
content="telephone=no, date=no, address=no, email=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css"> <style type="text/css">
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}} body,
table,
td {
font-family: Helvetica, Arial, sans-serif !important;
}
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 150%;
}
a {
text-decoration: none;
}
* {
color: inherit;
}
a[x-apple-data-detectors],
u + #body a,
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
img {
-ms-interpolation-mode: bicubic;
}
table:not([class^="s-"]) {
font-family: Helvetica, Arial, sans-serif;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border-spacing: 0px;
border-collapse: collapse;
}
table:not([class^="s-"]) td {
border-spacing: 0px;
border-collapse: collapse;
}
@media screen and (max-width: 600px) {
.w-full,
.w-full > tbody > tr > td {
width: 100% !important;
}
.w-24,
.w-24 > tbody > tr > td {
width: 96px !important;
}
.p-lg-10:not(table),
.p-lg-10:not(.btn) > tbody > tr > td,
.p-lg-10.btn td a {
padding: 0 !important;
}
.p-6:not(table),
.p-6:not(.btn) > tbody > tr > td,
.p-6.btn td a {
padding: 24px !important;
}
*[class*="s-lg-"] > tbody > tr > td {
font-size: 0 !important;
line-height: 0 !important;
height: 0 !important;
}
.s-4 > tbody > tr > td {
font-size: 16px !important;
line-height: 16px !important;
height: 16px !important;
}
.s-6 > tbody > tr > td {
font-size: 24px !important;
line-height: 24px !important;
height: 24px !important;
}
.s-10 > tbody > tr > td {
font-size: 40px !important;
line-height: 40px !important;
height: 40px !important;
}
}
</style> </style>
</head> </head>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc"> <body
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc"> class="bg-light"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<table
class="bg-light body"
valign="top"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<tbody> <tbody>
<tr> <tr>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc"> <td
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;"> valign="top"
style="line-height: 24px; font-size: 16px; margin: 0"
align="left"
bgcolor="#f7fafc"
>
<table
class="container"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
>
<tbody> <tbody>
<tr> <tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;"> <td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<!--[if (gte mso 9)|(IE)]> <!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation"> <table align="center" role="presentation">
<tbody> <tbody>
<tr> <tr>
<td width="600"> <td width="600">
<![endif]--> <![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;"> <table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40"> <td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;"> <table
class="ax-center"
role="presentation"
align="center"
border="0"
cellpadding="0"
cellspacing="0"
style="margin: 0 auto"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96"> style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<img
alt="Revolt Logo"
class="w-24"
src="https://app.revolt.chat/assets/logo_round.png"
style="
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
display: block;
width: 96px;
border-style: none;
border-width: 0;
"
width="96"
/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40"> <td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff"> <table
class="card p-6 p-lg-10 space-y-4"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
width: 100%;
overflow: hidden;
border: 1px solid #e2e8f0;
"
bgcolor="#ffffff"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff"> <td
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Account Suspended</h1> style="
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
padding: 40px;
"
align="left"
bgcolor="#ffffff"
>
<h1
class="h3 fw-700"
style="
padding-top: 0;
padding-bottom: 0;
font-weight: 700 !important;
vertical-align: baseline;
font-size: 28px;
line-height: 33.6px;
margin: 0;
"
align="left"
>
Account Suspended
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">Your account has been suspended, for one or more reasons:</p> <p
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> class=""
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
Your account has been suspended, for one
or more reasons:
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
@@ -83,98 +411,193 @@
<ul class=""> <ul class="">
{{list}} {{list}}
</ul> </ul>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p style="display: {{duration_display}}; line-height: 24px; font-size: 16px; width: 100%; margin: 0;" class="" align="left"> <p
You will be able to use your account again in {{duration}} days. style="display: {{duration_display}}; line-height: 24px; font-size: 16px; width: 100%; margin: 0;"
class=""
align="left"
>
You will be able to use your account again
in {{duration}} days.
</p> </p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left"> <p
Further violations may result in a permanent ban depending on class=""
severity, please abide by the style="
<a href="https://revolt.chat/aup" style="color: #0d6efd;">Acceptable Usage Policy</a>. line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
Further violations may result in a
permanent ban depending on severity,
please abide by the
<a
href="https://revolt.chat/aup"
style="color: #0d6efd"
>Acceptable Usage Policy</a
>.
</p> </p>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">Ban evasion is prohibited and will be dealt with accordingly.</p> <p
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
Ban evasion is prohibited and will be
dealt with accordingly.
</p>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> <td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="text-muted text-center" style="color: #718096;" align="center"> <div
This email is intended for {{email}}<br> class="text-muted text-center"
Sent from Revolt<br> style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe Made in Europe
</div> </div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> <td
&#160; style="
</td> line-height: 24px;
</tr> font-size: 24px;
</tbody> width: 100%;
</table> height: 24px;
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> margin: 0;
<tbody> "
<tr> align="left"
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> width="100%"
&#160; height="24"
</td> >
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<div class="text-muted text-xs" style="color: #718096; font-size: 12px; line-height: 14.4px;">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br>
Registered Company Number: 16260658<br>
Registered Office:<br>
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br>
Holborn, United Kingdom, EC1N 8DX
</div>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160; &#160;
</td> </td>
</tr> </tr>
@@ -33,14 +33,6 @@
Sent from Revolt<br /> Sent from Revolt<br />
Made in Europe Made in Europe
</div> </div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div> </div>
</body> </body>
</html> </html>
@@ -10,10 +10,3 @@ Ban evasion is prohibited and will be dealt with accordingly.
This email is intended for {{email}} This email is intended for {{email}}
Sent by Revolt Sent by Revolt
Made in Europe Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
Registration Number: 16260658
Registered Office:
Suite 5703 Unit 3A, 34-35 Hatton Garden,
Holborn, United Kingdom, EC1N 8DX
@@ -1,8 +0,0 @@
Your account has been suspended, for one or more reasons:
{{list}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html
+452 -72
View File
@@ -1,90 +1,462 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html> <html>
<head> <head>
<!-- Compiled with Bootstrap Email version: 1.5.1 --><meta http-equiv="x-ua-compatible" content="ie=edge"> <!-- Compiled with Bootstrap Email version: 1.3.1 -->
<meta name="x-apple-disable-message-reformatting"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="x-apple-disable-message-reformatting" />
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta
name="format-detection"
content="telephone=no, date=no, address=no, email=no"
/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css"> <style type="text/css">
body,table,td{font-family:Helvetica,Arial,sans-serif !important}.ExternalClass{width:100%}.ExternalClass,.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td,.ExternalClass div{line-height:150%}a{text-decoration:none}*{color:inherit}a[x-apple-data-detectors],u+#body a,#MessageViewBody a{color:inherit;text-decoration:none;font-size:inherit;font-family:inherit;font-weight:inherit;line-height:inherit}img{-ms-interpolation-mode:bicubic}table:not([class^=s-]){font-family:Helvetica,Arial,sans-serif;mso-table-lspace:0pt;mso-table-rspace:0pt;border-spacing:0px;border-collapse:collapse}table:not([class^=s-]) td{border-spacing:0px;border-collapse:collapse}@media screen and (max-width: 600px){.w-full,.w-full>tbody>tr>td{width:100% !important}.w-24,.w-24>tbody>tr>td{width:96px !important}.p-lg-10:not(table),.p-lg-10:not(.btn)>tbody>tr>td,.p-lg-10.btn td a{padding:0 !important}.p-3:not(table),.p-3:not(.btn)>tbody>tr>td,.p-3.btn td a{padding:12px !important}.p-6:not(table),.p-6:not(.btn)>tbody>tr>td,.p-6.btn td a{padding:24px !important}*[class*=s-lg-]>tbody>tr>td{font-size:0 !important;line-height:0 !important;height:0 !important}.s-4>tbody>tr>td{font-size:16px !important;line-height:16px !important;height:16px !important}.s-6>tbody>tr>td{font-size:24px !important;line-height:24px !important;height:24px !important}.s-10>tbody>tr>td{font-size:40px !important;line-height:40px !important;height:40px !important}} body,
table,
td {
font-family: Helvetica, Arial, sans-serif !important;
}
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 150%;
}
a {
text-decoration: none;
}
* {
color: inherit;
}
a[x-apple-data-detectors],
u + #body a,
#MessageViewBody a {
color: inherit;
text-decoration: none;
font-size: inherit;
font-family: inherit;
font-weight: inherit;
line-height: inherit;
}
img {
-ms-interpolation-mode: bicubic;
}
table:not([class^="s-"]) {
font-family: Helvetica, Arial, sans-serif;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
border-spacing: 0px;
border-collapse: collapse;
}
table:not([class^="s-"]) td {
border-spacing: 0px;
border-collapse: collapse;
}
@media screen and (max-width: 600px) {
.w-full,
.w-full > tbody > tr > td {
width: 100% !important;
}
.w-24,
.w-24 > tbody > tr > td {
width: 96px !important;
}
.p-lg-10:not(table),
.p-lg-10:not(.btn) > tbody > tr > td,
.p-lg-10.btn td a {
padding: 0 !important;
}
.p-3:not(table),
.p-3:not(.btn) > tbody > tr > td,
.p-3.btn td a {
padding: 12px !important;
}
.p-6:not(table),
.p-6:not(.btn) > tbody > tr > td,
.p-6.btn td a {
padding: 24px !important;
}
*[class*="s-lg-"] > tbody > tr > td {
font-size: 0 !important;
line-height: 0 !important;
height: 0 !important;
}
.s-4 > tbody > tr > td {
font-size: 16px !important;
line-height: 16px !important;
height: 16px !important;
}
.s-6 > tbody > tr > td {
font-size: 24px !important;
line-height: 24px !important;
height: 24px !important;
}
.s-10 > tbody > tr > td {
font-size: 40px !important;
line-height: 40px !important;
height: 40px !important;
}
}
</style> </style>
</head> </head>
<body class="bg-light" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc"> <body
<table class="bg-light body" valign="top" role="presentation" border="0" cellpadding="0" cellspacing="0" style="outline: 0; width: 100%; min-width: 100%; height: 100%; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; font-family: Helvetica, Arial, sans-serif; line-height: 24px; font-weight: normal; font-size: 16px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: #000000; margin: 0; padding: 0; border-width: 0;" bgcolor="#f7fafc"> class="bg-light"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<table
class="bg-light body"
valign="top"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
outline: 0;
width: 100%;
min-width: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
font-family: Helvetica, Arial, sans-serif;
line-height: 24px;
font-weight: normal;
font-size: 16px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #000000;
margin: 0;
padding: 0;
border-width: 0;
"
bgcolor="#f7fafc"
>
<tbody> <tbody>
<tr> <tr>
<td valign="top" style="line-height: 24px; font-size: 16px; margin: 0;" align="left" bgcolor="#f7fafc"> <td
<table class="container" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;"> valign="top"
style="line-height: 24px; font-size: 16px; margin: 0"
align="left"
bgcolor="#f7fafc"
>
<table
class="container"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
>
<tbody> <tbody>
<tr> <tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;"> <td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<!--[if (gte mso 9)|(IE)]> <!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation"> <table align="center" role="presentation">
<tbody> <tbody>
<tr> <tr>
<td width="600"> <td width="600">
<![endif]--> <![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;"> <table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40"> <td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;"> <table
class="ax-center"
role="presentation"
align="center"
border="0"
cellpadding="0"
cellspacing="0"
style="margin: 0 auto"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left"> <td
<img alt="Revolt Logo" class="w-24" src="https://app.revolt.chat/assets/logo_round.png" style="height: auto; line-height: 100%; outline: none; text-decoration: none; display: block; width: 96px; border-style: none; border-width: 0;" width="96"> style="
line-height: 24px;
font-size: 16px;
margin: 0;
"
align="left"
>
<img
alt="Revolt Logo"
class="w-24"
src="https://app.revolt.chat/assets/logo_round.png"
style="
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
display: block;
width: 96px;
border-style: none;
border-width: 0;
"
width="96"
/>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40"> <td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="card p-6 p-lg-10 space-y-4" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; width: 100%; overflow: hidden; border: 1px solid #e2e8f0;" bgcolor="#ffffff"> <table
class="card p-6 p-lg-10 space-y-4"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
width: 100%;
overflow: hidden;
border: 1px solid #e2e8f0;
"
bgcolor="#ffffff"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; width: 100%; margin: 0; padding: 40px;" align="left" bgcolor="#ffffff"> <td
<h1 class="h3 fw-700" style="padding-top: 0; padding-bottom: 0; font-weight: 700 !important; vertical-align: baseline; font-size: 28px; line-height: 33.6px; margin: 0;" align="left">Almost there!</h1> style="
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
padding: 40px;
"
align="left"
bgcolor="#ffffff"
>
<h1
class="h3 fw-700"
style="
padding-top: 0;
padding-bottom: 0;
font-weight: 700 !important;
vertical-align: baseline;
font-size: 28px;
line-height: 33.6px;
margin: 0;
"
align="left"
>
Almost there!
</h1>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p class="" style="line-height: 24px; font-size: 16px; width: 100%; margin: 0;" align="left">To complete your sign up, we just need to verify your email.</p> <p
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> class=""
style="
line-height: 24px;
font-size: 16px;
width: 100%;
margin: 0;
"
align="left"
>
To complete your sign up, we just need to
verify your email.
</p>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16"> <td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="btn btn-primary p-3 fw-700" role="presentation" border="0" cellpadding="0" cellspacing="0" style="border-radius: 6px; border-collapse: separate !important; font-weight: 700 !important;"> <table
class="btn btn-primary p-3 fw-700"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="
border-radius: 6px;
border-collapse: separate !important;
font-weight: 700 !important;
"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 16px; border-radius: 6px; font-weight: 700 !important; margin: 0;" align="center" bgcolor="#0d6efd"> <td
<a href="{{url}}" style="color: #ffffff; font-size: 16px; font-family: Helvetica, Arial, sans-serif; text-decoration: none; border-radius: 6px; line-height: 20px; display: block; font-weight: 700 !important; white-space: nowrap; background-color: #0d6efd; padding: 12px; border: 1px solid #0d6efd;">Confirm</a> style="
line-height: 24px;
font-size: 16px;
border-radius: 6px;
font-weight: 700 !important;
margin: 0;
"
align="center"
bgcolor="#0d6efd"
>
<a
href="{{url}}"
style="
color: #ffffff;
font-size: 16px;
font-family: Helvetica, Arial,
sans-serif;
text-decoration: none;
border-radius: 6px;
line-height: 20px;
display: block;
font-weight: 700 !important;
white-space: nowrap;
background-color: #0d6efd;
padding: 12px;
border: 1px solid #0d6efd;
"
>Confirm</a
>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -93,58 +465,66 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> <td
style="
line-height: 24px;
font-size: 24px;
width: 100%;
height: 24px;
margin: 0;
"
align="left"
width="100%"
height="24"
>
&#160; &#160;
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div class="text-muted text-center" style="color: #718096;" align="center"> <div
This email is intended for {{email}}<br> class="text-muted text-center"
Sent from Revolt<br> style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe Made in Europe
</div> </div>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> <table
class="s-6 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> <td
&#160; style="
</td> line-height: 24px;
</tr> font-size: 24px;
</tbody> width: 100%;
</table> height: 24px;
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%"> margin: 0;
<tbody> "
<tr> align="left"
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24"> width="100%"
&#160; height="24"
</td> >
</tr>
</tbody>
</table>
<table class="ax-center" role="presentation" align="center" border="0" cellpadding="0" cellspacing="0" style="margin: 0 auto;">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 16px; margin: 0;" align="left">
<div class="text-muted text-xs" style="color: #718096; font-size: 12px; line-height: 14.4px;">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br>
Registered Company Number: 16260658<br>
Registered Office:<br>
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br>
Holborn, United Kingdom, EC1N 8DX
</div>
</td>
</tr>
</tbody>
</table>
<table class="s-6 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 24px; font-size: 24px; width: 100%; height: 24px; margin: 0;" align="left" width="100%" height="24">
&#160; &#160;
</td> </td>
</tr> </tr>
@@ -22,14 +22,6 @@
Sent from Revolt<br /> Sent from Revolt<br />
Made in Europe Made in Europe
</div> </div>
<div class="text-muted text-xs my-6 ax-center">
Revolt Platforms Ltd. is a company incorporated and registered under the
laws of England and Wales.<br />
Registered Company Number: 16260658<br />
Registered Office:<br />
Suite 5703 Unit 3A, 34-35 Hatton Garden,<br />
Holborn, United Kingdom, EC1N 8DX
</div>
</div> </div>
</body> </body>
</html> </html>
@@ -6,10 +6,3 @@ Please navigate to: {{url}}
This email is intended for {{email}} This email is intended for {{email}}
Sent by Revolt Sent by Revolt
Made in Europe Made in Europe
Revolt Platforms Ltd. is a company incorporated and registered under the laws of England and Wales.
Registration Number: 16260658
Registered Office:
Suite 5703 Unit 3A, 34-35 Hatton Garden,
Holborn, United Kingdom, EC1N 8DX
@@ -1,10 +0,0 @@
Almost there!
To complete your sign up, we just need to verify your email.
Please navigate to: {{url}}
This email is intended for {{email}}
This email has no association with Revolt or Revolt Platforms Ltd.
Learn more about third party instances here:
https://developers.revolt.chat/faq.html
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-files" name = "revolt-files"
version = "0.8.4" version = "0.8.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,10 +20,10 @@ typenum = "1.17.0"
aws-config = "1.5.5" aws-config = "1.5.5"
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] } aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
revolt-config = { version = "0.8.4", path = "../config", features = [ revolt-config = { version = "0.8.2", path = "../config", features = [
"report-macros", "report-macros",
] } ] }
revolt-result = { version = "0.8.4", path = "../result" } revolt-result = { version = "0.8.2", path = "../result" }
# image processing # image processing
jxl-oxide = "0.8.1" jxl-oxide = "0.8.1"
+4 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.8.4" version = "0.8.2"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -20,14 +20,13 @@ default = ["serde", "partials", "rocket"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.8.4", path = "../config" } revolt-config = { version = "0.8.2", path = "../config" }
revolt-permissions = { version = "0.8.4", path = "../permissions" } revolt-permissions = { version = "0.8.2", path = "../permissions" }
# Utility # Utility
regex = "1.11" regex = "1"
indexmap = "1.9.3" indexmap = "1.9.3"
once_cell = "1.17.1" once_cell = "1.17.1"
num_enum = "0.6.1"
# Rocket # Rocket
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false } rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
+5 -12
View File
@@ -1,6 +1,8 @@
use std::time::SystemTime; use std::time::SystemTime;
use indexmap::{IndexMap, IndexSet}; use indexmap::{IndexMap, IndexSet};
use once_cell::sync::Lazy;
use regex::Regex;
use revolt_config::config; use revolt_config::config;
#[cfg(feature = "validator")] #[cfg(feature = "validator")]
@@ -13,6 +15,9 @@ use iso8601_timestamp::Timestamp;
use super::{Channel, Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR}; use super::{Channel, Embed, File, Member, MessageWebhook, User, Webhook, RE_COLOUR};
pub static RE_MENTION: Lazy<Regex> =
Lazy::new(|| Regex::new(r"<@([0-9A-HJKMNP-TV-Z]{26})>").unwrap());
auto_derived_partial!( auto_derived_partial!(
/// Message /// Message
pub struct Message { pub struct Message {
@@ -53,9 +58,6 @@ auto_derived_partial!(
/// Array of user ids mentioned in this message /// Array of user ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub mentions: Option<Vec<String>>, pub mentions: Option<Vec<String>>,
/// Array of role ids mentioned in this message
#[serde(skip_serializing_if = "Option::is_none")]
pub role_mentions: Option<Vec<String>>,
/// Array of message ids this message is replying to /// Array of message ids this message is replying to
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>, pub replies: Option<Vec<String>>,
@@ -237,10 +239,6 @@ auto_derived!(
pub id: String, pub id: String,
/// Whether this reply should mention the message's author /// Whether this reply should mention the message's author
pub mention: bool, pub mention: bool,
/// Whether to error if the referenced message doesn't exist.
/// Otherwise, send a message without this reply.
/// Default is true.
pub fail_if_not_exists: Option<bool>,
} }
/// Message to send /// Message to send
@@ -369,11 +367,6 @@ auto_derived!(
pub enum MessageFlags { pub enum MessageFlags {
/// Message will not send push / desktop notifications /// Message will not send push / desktop notifications
SuppressNotifications = 1, SuppressNotifications = 1,
/// Message will mention all users who can see the channel
MentionsEveryone = 2,
/// Message will mention all users who are online and can see the channel.
/// This cannot be true if MentionsEveryone is true
MentionsOnline = 3,
} }
/// Optional fields on message /// Optional fields on message
-7
View File
@@ -1,7 +0,0 @@
[package]
name = "revolt-parser"
version = "0.1.0"
edition = "2021"
[dependencies]
logos = { version = "*" }
-245
View File
@@ -1,245 +0,0 @@
use std::collections::{HashSet, VecDeque};
use logos::Logos;
#[derive(Debug, Clone, Logos, PartialEq)]
#[logos(skip "\n")]
pub enum MessageToken {
#[token("\\")]
Escape,
#[regex("(```[^`\n]*)|(``)|`", |lex| lex.slice().to_owned().chars().filter(|&c| c == '`').count())]
CodeblockMarker(usize),
#[regex("<@[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
UserMention(String),
#[regex("<%[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}>", |lex| lex.slice()[2..lex.slice().len() - 1].to_owned())]
RoleMention(String),
#[token("@everyone")]
MentionEveryone,
#[token("@online")]
MentionOnline
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct MessageResults {
pub user_mentions: HashSet<String>,
pub role_mentions: HashSet<String>,
pub mentions_everyone: bool,
pub mentions_online: bool
}
struct MessageParserIterator<I> {
inner: I,
temp: VecDeque<MessageToken>
}
impl<I: Iterator<Item = MessageToken>> Iterator for MessageParserIterator<I> {
type Item = MessageToken;
fn next(&mut self) -> Option<Self::Item> {
if !self.temp.is_empty() {
self.temp.pop_front()
} else {
let token = self.inner.next();
if token == Some(MessageToken::Escape) {
self.inner.next();
token
} else if let Some(MessageToken::CodeblockMarker(ty)) = token {
loop {
let next_token = self.inner.next();
if next_token == Some(MessageToken::CodeblockMarker(ty)) {
self.temp.clear();
self.temp.push_back(MessageToken::CodeblockMarker(ty));
break next_token
} else if let Some(token) = next_token {
self.temp.push_back(token);
} else {
break Some(MessageToken::CodeblockMarker(ty))
}
}
} else {
token
}
}
}
}
pub fn parse_message_iter(text: &str) -> impl Iterator<Item = MessageToken> + '_ {
MessageParserIterator {
inner: MessageToken::lexer(text).flatten(),
temp: VecDeque::new()
}
}
pub fn parse_message(text: &str) -> MessageResults {
let mut results = MessageResults::default();
for token in parse_message_iter(text) {
match token {
MessageToken::Escape => {}
MessageToken::CodeblockMarker(_) => {},
MessageToken::UserMention(id) => { results.user_mentions.insert(id); },
MessageToken::RoleMention(id) => { results.role_mentions.insert(id); },
MessageToken::MentionEveryone => results.mentions_everyone = true,
MessageToken::MentionOnline => results.mentions_online = true,
};
};
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_nodes() {
let output = parse_message_iter("Hello everyone").collect::<Vec<_>>();
assert_eq!(output.len(), 0);
}
#[test]
fn test_simple_user_mention() {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
}
#[test]
fn test_simple_role_mention() {
let output = parse_message_iter("Hello <%01FD58YK5W7QRV5H3D64KTQYX3>.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
}
#[test]
fn test_mention_everyone() {
let output = parse_message_iter("Hello @everyone.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::MentionEveryone);
}
#[test]
fn test_mention_online() {
let output = parse_message_iter("Hello @online.").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::MentionOnline);
}
#[test]
fn test_everything() {
let output = parse_message_iter("Hello <@01FD58YK5W7QRV5H3D64KTQYX3>, <%01FD58YK5W7QRV5H3D64KTQYX3>, @everyone and @online.").collect::<Vec<_>>();
assert_eq!(output.len(), 4);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
#[test]
fn test_everything_no_spaces() {
let output = parse_message_iter("<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
assert_eq!(output.len(), 4);
assert_eq!(output[0], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[1], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::MentionEveryone);
assert_eq!(output[3], MessageToken::MentionOnline);
}
#[test]
fn test_codeblock_no_mentions() {
let output = parse_message_iter("```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online\n```").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::CodeblockMarker(3));
}
#[test]
fn test_uncontained_codeblock_should_mention() {
let output = parse_message_iter("```\n<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
assert_eq!(output.len(), 5);
assert_eq!(output[0], MessageToken::CodeblockMarker(3));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline);
}
#[test]
fn test_inline_codeblock_no_mentions() {
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online`").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
}
#[test]
fn test_uncontained_inline_codeblock_should_mention() {
let output = parse_message_iter("`<@01FD58YK5W7QRV5H3D64KTQYX3><%01FD58YK5W7QRV5H3D64KTQYX3>@everyone@online").collect::<Vec<_>>();
assert_eq!(output.len(), 5);
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[1], MessageToken::UserMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[2], MessageToken::RoleMention("01FD58YK5W7QRV5H3D64KTQYX3".to_string()));
assert_eq!(output[3], MessageToken::MentionEveryone);
assert_eq!(output[4], MessageToken::MentionOnline);
}
#[test]
fn test_double_inline_codeblock() {
let output = parse_message_iter("``this should not ping @everyone``").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(2));
assert_eq!(output[1], MessageToken::CodeblockMarker(2));
}
#[test]
fn test_double_inline_codeblock_with_backticks_inside() {
let output = parse_message_iter("``this `should` not `ping` @everyone``").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(2));
assert_eq!(output[1], MessageToken::CodeblockMarker(2));
}
#[test]
fn test_in_middle() {
let output = parse_message_iter("i am not pinging `@everyone`.").collect::<Vec<_>>();
assert_eq!(output.len(), 2);
assert_eq!(output[0], MessageToken::CodeblockMarker(1));
assert_eq!(output[1], MessageToken::CodeblockMarker(1));
}
#[test]
fn test_escaped_codeblock() {
let output = parse_message_iter("i am ~~not~~ pinging \\`@everyone` ok.").collect::<Vec<_>>();
assert_eq!(output.len(), 3);
assert_eq!(output[0], MessageToken::Escape);
assert_eq!(output[1], MessageToken::MentionEveryone);
assert_eq!(output[2], MessageToken::CodeblockMarker(1));
}
#[test]
fn test_escape_mention() {
let output = parse_message_iter("i wont ping \\@everyone").collect::<Vec<_>>();
assert_eq!(output.len(), 1);
assert_eq!(output[0], MessageToken::Escape);
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.8.4" version = "0.8.2"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -21,7 +21,7 @@ async-std = { version = "1.8.0", features = ["attributes"] }
[dependencies] [dependencies]
# Core # Core
revolt-result = { version = "0.8.4", path = "../result" } revolt-result = { version = "0.8.2", path = "../result" }
# Utility # Utility
auto_ops = "0.3.0" auto_ops = "0.3.0"
@@ -90,14 +90,8 @@ pub enum ChannelPermission {
/// Move members between voice channels /// Move members between voice channels
MoveMembers = 1 << 35, MoveMembers = 1 << 35,
// * Channel permissions two electric boogaloo
/// Mention everyone and online members
MentionEveryone = 1 << 37,
/// Mention roles
MentionRoles = 1 << 38,
// * Misc. permissions // * Misc. permissions
// % Bits 38 to 52: free area // % Bits 36 to 52: free area
// % Bits 53 to 64: do not use // % Bits 53 to 64: do not use
// * Grant all permissions // * Grant all permissions
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-presence" name = "revolt-presence"
version = "0.8.4" version = "0.8.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-result" name = "revolt-result"
version = "0.8.4" version = "0.8.2"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+1 -3
View File
@@ -1,4 +1,5 @@
use axum::{http::StatusCode, response::IntoResponse, Json}; use axum::{http::StatusCode, response::IntoResponse, Json};
use rocket::http::Status;
use crate::{Error, ErrorType}; use crate::{Error, ErrorType};
@@ -51,7 +52,6 @@ impl IntoResponse for Error {
ErrorType::ReachedMaximumBots => StatusCode::BAD_REQUEST, ErrorType::ReachedMaximumBots => StatusCode::BAD_REQUEST,
ErrorType::IsBot => StatusCode::BAD_REQUEST, ErrorType::IsBot => StatusCode::BAD_REQUEST,
ErrorType::IsNotBot => StatusCode::BAD_REQUEST,
ErrorType::BotIsPrivate => StatusCode::FORBIDDEN, ErrorType::BotIsPrivate => StatusCode::FORBIDDEN,
ErrorType::CannotReportYourself => StatusCode::BAD_REQUEST, ErrorType::CannotReportYourself => StatusCode::BAD_REQUEST,
@@ -75,8 +75,6 @@ impl IntoResponse for Error {
ErrorType::NotFound => StatusCode::NOT_FOUND, ErrorType::NotFound => StatusCode::NOT_FOUND,
ErrorType::NoEffect => StatusCode::OK, ErrorType::NoEffect => StatusCode::OK,
ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST, ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST,
ErrorType::InvalidFlagValue => StatusCode::BAD_REQUEST,
ErrorType::FeatureDisabled { .. } => StatusCode::BAD_REQUEST,
ErrorType::ProxyError => StatusCode::BAD_REQUEST, ErrorType::ProxyError => StatusCode::BAD_REQUEST,
ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY, ErrorType::FileTooSmall => StatusCode::UNPROCESSABLE_ENTITY,
-7
View File
@@ -121,7 +121,6 @@ pub enum ErrorType {
// ? Bot related errors // ? Bot related errors
ReachedMaximumBots, ReachedMaximumBots,
IsBot, IsBot,
IsNotBot,
BotIsPrivate, BotIsPrivate,
// ? User safety related errors // ? User safety related errors
@@ -149,7 +148,6 @@ pub enum ErrorType {
InvalidCredentials, InvalidCredentials,
InvalidProperty, InvalidProperty,
InvalidSession, InvalidSession,
InvalidFlagValue,
NotAuthenticated, NotAuthenticated,
DuplicateNonce, DuplicateNonce,
NotFound, NotFound,
@@ -170,11 +168,6 @@ pub enum ErrorType {
// ? Legacy errors // ? Legacy errors
VosoUnavailable, VosoUnavailable,
// ? Feature flag disabled in the config
FeatureDisabled {
feature: String,
},
} }
#[macro_export] #[macro_export]
-3
View File
@@ -42,7 +42,6 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotInGroup => Status::NotFound, ErrorType::NotInGroup => Status::NotFound,
ErrorType::AlreadyPinned => Status::BadRequest, ErrorType::AlreadyPinned => Status::BadRequest,
ErrorType::NotPinned => Status::BadRequest, ErrorType::NotPinned => Status::BadRequest,
ErrorType::InvalidFlagValue => Status::BadRequest,
ErrorType::UnknownServer => Status::NotFound, ErrorType::UnknownServer => Status::NotFound,
ErrorType::InvalidRole => Status::NotFound, ErrorType::InvalidRole => Status::NotFound,
@@ -58,7 +57,6 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::ReachedMaximumBots => Status::BadRequest, ErrorType::ReachedMaximumBots => Status::BadRequest,
ErrorType::IsBot => Status::BadRequest, ErrorType::IsBot => Status::BadRequest,
ErrorType::IsNotBot => Status::BadRequest,
ErrorType::BotIsPrivate => Status::Forbidden, ErrorType::BotIsPrivate => Status::Forbidden,
ErrorType::CannotReportYourself => Status::BadRequest, ErrorType::CannotReportYourself => Status::BadRequest,
@@ -82,7 +80,6 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotFound => Status::NotFound, ErrorType::NotFound => Status::NotFound,
ErrorType::NoEffect => Status::Ok, ErrorType::NoEffect => Status::Ok,
ErrorType::FailedValidation { .. } => Status::BadRequest, ErrorType::FailedValidation { .. } => Status::BadRequest,
ErrorType::FeatureDisabled { .. } => Status::BadRequest,
ErrorType::ProxyError => Status::BadRequest, ErrorType::ProxyError => Status::BadRequest,
ErrorType::FileTooSmall => Status::UnprocessableEntity, ErrorType::FileTooSmall => Status::UnprocessableEntity,
+5 -5
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-crond" name = "revolt-crond"
version = "0.8.4" version = "0.8.2"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
edition = "2021" edition = "2021"
@@ -16,7 +16,7 @@ log = "0.4"
tokio = { version = "1" } tokio = { version = "1" }
# Core # Core
revolt-database = { version = "0.8.4", path = "../../core/database" } revolt-database = { version = "0.8.2", path = "../../core/database" }
revolt-result = { version = "0.8.4", path = "../../core/result" } revolt-result = { version = "0.8.2", path = "../../core/result" }
revolt-config = { version = "0.8.4", path = "../../core/config" } revolt-config = { version = "0.8.2", path = "../../core/config" }
revolt-files = { version = "0.8.4", path = "../../core/files" } revolt-files = { version = "0.8.2", path = "../../core/files" }
+5 -10
View File
@@ -1,18 +1,14 @@
[package] [package]
name = "revolt-pushd" name = "revolt-pushd"
version = "0.8.4" version = "0.8.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
revolt-config = { version = "0.8.4", path = "../../core/config" } revolt-config = { version = "0.8.2", path = "../../core/config" }
revolt-database = { version = "0.8.4", path = "../../core/database" } revolt-database = { version = "0.8.2", path = "../../core/database" }
revolt-models = { version = "0.8.4", path = "../../core/models", features = [ revolt-models = { version = "0.8.2", path = "../../core/models", features = [
"validator", "validator",
] } ] }
revolt-presence = { version = "0.8.1", path = "../../core/presence", features = [
"redis-is-patched",
] }
amqprs = { version = "1.7.0" } amqprs = { version = "1.7.0" }
fcm_v1 = "0.3.0" fcm_v1 = "0.3.0"
@@ -23,10 +19,9 @@ tokio = "1.39.2"
async-trait = "0.1.81" async-trait = "0.1.81"
ulid = "1.0.0" ulid = "1.0.0"
authifier = "1.0.10" authifier = "1.0.8"
log = "0.4.11" log = "0.4.11"
pretty_env_logger = "0.4.0"
#serialization #serialization
serde_json = "1" serde_json = "1"
+4 -1
View File
@@ -1,5 +1,8 @@
# Build Stage # Build Stage
FROM ghcr.io/revoltchat/base:latest AS builder # BASE_IMAGE is the compiled workspace base image. Defaults to the upstream
# revolt base, but CI overrides it with our own base built from repo.
ARG BASE_IMAGE=ghcr.io/revoltchat/base:latest
FROM ${BASE_IMAGE} AS builder
FROM debian:12 AS debian FROM debian:12 AS debian
# Bundle Stage # Bundle Stage
Binary file not shown.
@@ -71,14 +71,8 @@ impl AsyncConsumer for AckConsumer {
#[allow(clippy::disallowed_methods)] #[allow(clippy::disallowed_methods)]
let unreads = self.db.fetch_unread_mentions(&payload.user_id).await; let unreads = self.db.fetch_unread_mentions(&payload.user_id).await;
debug!("Processing unreads for {:}", &payload.user_id);
if let Ok(u) = &unreads { if let Ok(u) = &unreads {
if u.is_empty() { if u.is_empty() {
debug!(
"Discarding unread task (no mentions found) for {:}",
&payload.user_id
);
return; return;
} }
} else { } else {
@@ -101,10 +95,6 @@ impl AsyncConsumer for AckConsumer {
.collect(); .collect();
if apple_sessions.is_empty() { if apple_sessions.is_empty() {
debug!(
"Discarding unread task (no apn sessions found) for {:}",
&payload.user_id
);
return; return;
} }
@@ -1,275 +0,0 @@
use std::{
collections::{HashMap, HashSet},
hash::RandomState,
};
use crate::consumers::inbound::internal::*;
use amqprs::{
channel::{BasicPublishArguments, Channel},
connection::Connection,
consumer::AsyncConsumer,
BasicProperties, Deliver,
};
use async_trait::async_trait;
use revolt_database::{
events::rabbit::*, util::bulk_permissions::BulkDatabasePermissionQuery, Database, Member,
MessageFlagsValue,
};
use revolt_models::v0::{MessageFlags, PushNotification};
pub struct MassMessageConsumer {
#[allow(dead_code)]
db: Database,
authifier_db: authifier::Database,
conn: Option<Connection>,
channel: Option<Channel>,
}
impl Channeled for MassMessageConsumer {
fn get_connection(&self) -> Option<&Connection> {
if self.conn.is_none() {
None
} else {
Some(self.conn.as_ref().unwrap())
}
}
fn get_channel(&self) -> Option<&Channel> {
if self.channel.is_none() {
None
} else {
Some(self.channel.as_ref().unwrap())
}
}
fn set_connection(&mut self, conn: Connection) {
self.conn = Some(conn);
}
fn set_channel(&mut self, channel: Channel) {
self.channel = Some(channel)
}
}
impl MassMessageConsumer {
pub fn new(db: Database, authifier_db: authifier::Database) -> MassMessageConsumer {
MassMessageConsumer {
db,
authifier_db,
conn: None,
channel: None,
}
}
async fn fire_notification_for_users(&mut self, push: &PushNotification, users: &[String]) {
if let Ok(sessions) = self
.authifier_db
.find_sessions_with_subscription(users)
.await
{
let config = revolt_config::config().await;
for session in sessions {
if let Some(sub) = session.subscription {
let mut sendable = PayloadToService {
notification: PayloadKind::MessageNotification(push.clone()),
token: sub.auth,
user_id: session.user_id,
session_id: session.id,
extras: HashMap::new(),
};
let args: BasicPublishArguments;
if sub.endpoint == "apn" {
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.apn.queue.as_str(),
)
.finish();
} else if sub.endpoint == "fcm" {
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.fcm.queue.as_str(),
)
.finish();
} else {
// web push (vapid)
args = BasicPublishArguments::new(
config.pushd.exchange.as_str(),
config.pushd.vapid.queue.as_str(),
)
.finish();
sendable.extras.insert("p265dh".to_string(), sub.p256dh);
sendable
.extras
.insert("endpoint".to_string(), sub.endpoint.clone());
}
let payload = serde_json::to_string(&sendable).unwrap();
publish_message(self, payload.into(), args).await;
}
}
}
}
}
#[allow(unused_variables)]
#[async_trait]
impl AsyncConsumer for MassMessageConsumer {
/// This consumer handles adding mentions for all the users affected by a mass mention ping, and then sends out push notifications
async fn consume(
&mut self,
channel: &Channel,
deliver: Deliver,
basic_properties: BasicProperties,
content: Vec<u8>,
) {
let config = revolt_config::config().await;
let content = String::from_utf8(content).unwrap();
let payload: MassMessageSentPayload = serde_json::from_str(content.as_str()).unwrap();
debug!("Received mass message event");
// We should only ever receive clumped messages from a single channel, so it's safe to reuse this many times.
let mut query: Option<BulkDatabasePermissionQuery<'_>> = None;
let query_db = self.db.clone();
for push in payload.notifications {
if query.is_none() {
query = Some(
BulkDatabasePermissionQuery::from_server_id(&query_db, &payload.server_id)
.await
.from_channel_id(push.channel.id().to_string()) // wrong channel model, so fetch the right one
.await,
);
}
let existing_mentions: HashSet<String, RandomState> =
if let Some(ref mentions) = push.message.mentions {
HashSet::from_iter(mentions.iter().cloned())
} else {
HashSet::new()
};
// KNOWN QUIRK: if you mention @online and role(s), the offline members with the role(s) wont get pinged
if let Some(ref query) = query {
let flags = MessageFlagsValue(push.message.flags);
if flags.has(MessageFlags::MentionsEveryone) {
let mut db_query = self
.db
.fetch_all_members_chunked(&payload.server_id)
.await
.expect("Failed to fetch members from database");
let mut exhausted = false;
let ack_chnl = vec![push.channel.id().to_string()];
loop {
let mut chunk: Vec<Member> = vec![];
for _ in 0..config.pushd.mass_mention_chunk_size {
if let Some(member) = db_query.next().await {
chunk.push(member);
} else {
exhausted = true;
break;
}
}
let userids: Vec<String> =
chunk.iter().map(|member| member.id.user.clone()).collect();
debug!("Userids in chunk: {:?}", userids);
if let Err(err) = self
.db
.add_mention_to_many_unreads(push.channel.id(), &userids, &ack_chnl)
.await
{
revolt_config::capture_error(&err);
}
// ignore anyone in this list
let online_users = revolt_presence::filter_online(&userids).await;
let target_users: Vec<String> = userids
.iter()
.filter(|id| {
!online_users.contains(*id) && !existing_mentions.contains(*id)
})
.cloned()
.collect();
debug!(
"Userids after filter: {:?} (online: {:?}",
target_users, online_users
);
self.fire_notification_for_users(&push, &target_users).await;
if exhausted {
break;
}
}
} else if let Some(roles) = &push.message.role_mentions {
// role mentions
let _role_members = self
.db
.fetch_all_members_with_roles_chunked(&payload.server_id, roles)
.await;
debug!("role members: {:?}", _role_members);
if _role_members.is_err() {
revolt_config::capture_error(&_role_members.err().unwrap());
return;
}
let mut role_members = _role_members.unwrap();
let mut chunk = vec![];
let mut exhausted = false;
while !exhausted {
chunk.clear();
for _ in 0..config.pushd.mass_mention_chunk_size {
if let Some(member) = role_members.next().await {
chunk.push(member);
} else {
exhausted = true;
break;
}
}
let mut q = query.clone().members(&chunk);
let viewing_members: Vec<String> = q
.members_can_see_channel()
.await
.iter()
.filter_map(|(uid, viewable)| {
if *viewable && !existing_mentions.contains(uid) {
Some(uid.clone())
} else {
None
}
})
.collect();
debug!("viewing members: {:?}", viewing_members);
let online = revolt_presence::filter_online(&viewing_members).await;
debug!("online: {:?}", online);
let targets: Vec<String> = viewing_members
.iter()
.filter(|m| !online.contains(*m))
.cloned()
.collect();
debug!("targets: {:?}", targets);
self.fire_notification_for_users(&push, &targets).await;
}
}
}
}
}
}
@@ -111,7 +111,7 @@ impl AsyncConsumer for MessageConsumer {
config.pushd.vapid.queue.as_str(), config.pushd.vapid.queue.as_str(),
) )
.finish(); .finish();
sendable.extras.insert("p265dh".to_string(), sub.p256dh); sendable.extras.insert("p256dh".to_string(), sub.p256dh);
sendable sendable
.extras .extras
.insert("endpoint".to_string(), sub.endpoint.clone()); .insert("endpoint".to_string(), sub.endpoint.clone());
@@ -3,5 +3,4 @@ pub mod fr_accepted;
pub mod fr_received; pub mod fr_received;
pub mod generic; pub mod generic;
mod internal; mod internal;
pub mod mass_mention;
pub mod message; pub mod message;
@@ -81,7 +81,7 @@ impl ApnsOutboundConsumer {
} }
} }
debug!("Got badge count for APN: {}", mention_count); println!("Got badge count for APN: {}", mention_count);
return Some(mention_count); return Some(mention_count);
} }
@@ -188,10 +188,6 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(), data: BTreeMap::new(),
}; };
debug!(
"Sending friend request received for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
@@ -234,10 +230,6 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(), data: BTreeMap::new(),
}; };
debug!(
"Sending friend request accept for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
PayloadKind::Generic(alert) => { PayloadKind::Generic(alert) => {
@@ -267,10 +259,6 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(), data: BTreeMap::new(),
}; };
debug!(
"Sending generic notification for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
@@ -306,10 +294,6 @@ impl AsyncConsumer for ApnsOutboundConsumer {
channel_name: alert.channel.name().unwrap_or(&title), channel_name: alert.channel.name().unwrap_or(&title),
}; };
debug!(
"Sending message notification for user: {:}",
&payload.user_id
);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
PayloadKind::BadgeUpdate(badge) => { PayloadKind::BadgeUpdate(badge) => {
@@ -323,7 +307,6 @@ impl AsyncConsumer for ApnsOutboundConsumer {
data: BTreeMap::new(), data: BTreeMap::new(),
}; };
debug!("Sending badge update for user: {:}", &payload.user_id);
resp = self.client.send(apn_payload).await; resp = self.client.send(apn_payload).await;
} }
} }
@@ -338,10 +321,6 @@ impl AsyncConsumer for ApnsOutboundConsumer {
}), }),
.. ..
}) => { }) => {
info!(
"Removing APNS subscription id {:} (user: {:}) due to invalid token",
&payload.session_id, &payload.user_id
);
if let Err(err) = self if let Err(err) = self
.db .db
.remove_push_subscription_by_session_id(&payload.session_id) .remove_push_subscription_by_session_id(&payload.session_id)
@@ -55,11 +55,24 @@ impl AsyncConsumer for VapidOutboundConsumer {
let config = revolt_config::config().await; let config = revolt_config::config().await;
// A VAPID web push payload must carry the subscription endpoint and the
// p256dh public key in `extras`. Dropped malformed requests instead
// of panicking.
let (Some(endpoint), Some(p256dh)) =
(payload.extras.get("endpoint"), payload.extras.get("p256dh"))
else {
log::warn!(
"Dropping VAPID push for session {}: payload missing endpoint/p256dh in extras.",
payload.session_id
);
return;
};
let subscription = SubscriptionInfo { let subscription = SubscriptionInfo {
endpoint: payload.extras.get("endpoint").unwrap().clone(), endpoint: endpoint.clone(),
keys: SubscriptionKeys { keys: SubscriptionKeys {
auth: payload.token, auth: payload.token,
p256dh: payload.extras.get("p256dh").unwrap().clone(), p256dh: p256dh.clone(),
}, },
}; };
+6 -20
View File
@@ -1,6 +1,3 @@
#[macro_use]
extern crate log;
use amqprs::{ use amqprs::{
channel::{ channel::{
BasicConsumeArguments, Channel, ExchangeDeclareArguments, QueueBindArguments, BasicConsumeArguments, Channel, ExchangeDeclareArguments, QueueBindArguments,
@@ -17,7 +14,7 @@ mod consumers;
use consumers::{ use consumers::{
inbound::{ inbound::{
ack::AckConsumer, fr_accepted::FRAcceptedConsumer, fr_received::FRReceivedConsumer, ack::AckConsumer, fr_accepted::FRAcceptedConsumer, fr_received::FRReceivedConsumer,
generic::GenericConsumer, mass_mention::MassMessageConsumer, message::MessageConsumer, generic::GenericConsumer, message::MessageConsumer,
}, },
outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer}, outbound::{apn::ApnsOutboundConsumer, fcm::FcmOutboundConsumer, vapid::VapidOutboundConsumer},
}; };
@@ -25,7 +22,6 @@ use consumers::{
#[tokio::main(flavor = "multi_thread", worker_threads = 2)] #[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() { async fn main() {
let config = config().await; let config = config().await;
pretty_env_logger::init();
// Setup database // Setup database
let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap(); let db = revolt_database::DatabaseInfo::Auto.connect().await.unwrap();
@@ -100,17 +96,6 @@ async fn main() {
.await, .await,
); );
connections.push(
make_queue_and_consume(
&config,
&config.pushd.mass_mention_queue,
config.pushd.get_mass_mention_routing_key().as_str(),
None,
MassMessageConsumer::new(db.clone(), authifier.clone()),
)
.await,
);
if !config.pushd.apn.pkcs8.is_empty() { if !config.pushd.apn.pkcs8.is_empty() {
connections.push( connections.push(
make_queue_and_consume( make_queue_and_consume(
@@ -238,10 +223,11 @@ where
.manual_ack(false) .manual_ack(false)
.finish(); .finish();
let routing_key = channel.basic_consume(consumer, args).await.unwrap(); channel.basic_consume(consumer, args).await.unwrap();
info!( log::info!(
"Consuming routing key {} as queue {}, tag {}", "Consuming routing key {} as queue {}",
routing_key, queue_name, routing_key routing_key,
queue_name
); );
(channel, connection) (channel, connection)
} }
+3 -3
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.8.4" version = "0.8.2"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
@@ -55,7 +55,7 @@ lettre = "0.10.0-alpha.4"
rocket = { version = "0.5.1", default-features = false, features = ["json"] } rocket = { version = "0.5.1", default-features = false, features = ["json"] }
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" } rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", rev = "072d90359b23e9b291df6b672c07c93de9c46011" }
rocket_empty = { version = "0.1.1", features = ["schema"] } rocket_empty = { version = "0.1.1", features = ["schema"] }
rocket_authifier = { version = "1.0.10" } rocket_authifier = { version = "1.0.9" }
rocket_prometheus = "0.10.0-rc.3" rocket_prometheus = "0.10.0-rc.3"
# spec generation # spec generation
@@ -66,7 +66,7 @@ revolt_rocket_okapi = { version = "0.10.0", features = ["swagger"] }
amqprs = { version = "1.7.0" } amqprs = { version = "1.7.0" }
# core # core
authifier = "1.0.10" authifier = "1.0.9"
revolt-config = { path = "../core/config" } revolt-config = { path = "../core/config" }
revolt-database = { path = "../core/database", features = [ revolt-database = { path = "../core/database", features = [
"rocket-impl", "rocket-impl",
@@ -99,26 +99,5 @@ mod test {
// TEST: member leaves group (no delete) // TEST: member leaves group (no delete)
// TEST: no effect with saved messages // TEST: no effect with saved messages
// TEST: DM set to inactive // TEST: DM set to inactive
// TEST: server channel deleted
#[rocket::async_test]
async fn success_delete_channel() {
let mut harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (_, channels) = harness.new_server(&user).await;
let response = TestHarness::with_session(
session,
harness
.client
.delete(format!("/channels/{}", channels[0].id())),
)
.await;
assert_eq!(response.status(), Status::NoContent);
drop(response);
harness
.wait_for_event(channels[0].id(), |event| match event {
EventV1::ChannelDelete { id, .. } => id == channels[0].id(),
_ => false,
})
.await;
}
} }
@@ -1,4 +1,7 @@
use revolt_database::{util::{permissions::DatabasePermissionQuery, reference::Reference}, Channel, Database, PartialMessage, SystemMessage, User, AMQP}; use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, PartialMessage, SystemMessage, User, AMQP,
};
use revolt_models::v0::MessageAuthor; use revolt_models::v0::MessageAuthor;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
@@ -19,12 +22,10 @@ pub async fn message_pin(
) -> Result<EmptyResponse> { ) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
if !matches!(channel, Channel::DirectMessage { .. }) { let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); calculate_channel_permissions(&mut query)
calculate_channel_permissions(&mut query) .await
.await .throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
}
let mut message = msg.as_message_in_channel(db, channel.id()).await?; let mut message = msg.as_message_in_channel(db, channel.id()).await?;
@@ -84,7 +84,7 @@ pub async fn message_send(
// Create model user / members // Create model user / members
let model_user = user let model_user = user
.clone() .clone()
.into_known_static(revolt_presence::is_online(&user.id).await).await; .into_known_static(revolt_presence::is_online(&user.id).await);
let model_member: Option<v0::Member> = query let model_member: Option<v0::Member> = query
.member_ref() .member_ref()
@@ -117,11 +117,10 @@ mod test {
use crate::{rocket, util::test::TestHarness}; use crate::{rocket, util::test::TestHarness};
use revolt_database::{ use revolt_database::{
util::{idempotency::IdempotencyKey, reference::Reference}, util::{idempotency::IdempotencyKey, reference::Reference},
Channel, Member, Message, MessageFlagsValue, PartialChannel, PartialMember, Role, Server, Channel, Member, Message, PartialChannel, PartialMember, Role, Server,
}; };
use revolt_models::v0::{self, DataCreateServerChannel, MessageFlags}; use revolt_models::v0::{self, DataCreateServerChannel};
use revolt_permissions::{ChannelPermission, OverrideField}; use revolt_permissions::{ChannelPermission, OverrideField};
use revolt_result::ErrorType;
#[rocket::async_test] #[rocket::async_test]
async fn message_mention_constraints() { async fn message_mention_constraints() {
@@ -324,352 +323,4 @@ mod test {
"Mention was scrubbed when the user can see the channel" "Mention was scrubbed when the user can see the channel"
); );
} }
#[rocket::async_test]
async fn message_reply() {
let harness = TestHarness::new().await;
let (_, _, user) = harness.new_user().await;
let (server, channels) = harness.new_server(&user).await;
let channel = harness.new_channel(&server).await;
let (_, member, message) = harness.new_message(&user, &server, channels).await;
// Send a message with a reply
// Should succeed
let message_with_reply = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some("Message with reply".to_string()),
nonce: None,
attachments: None,
replies: Some(vec![v0::ReplyIntent {
id: message.id.clone(),
mention: false,
fail_if_not_exists: Some(true),
}]),
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("1".to_string()),
false,
false,
)
.await
.expect("Failed to create message with reply");
assert!(
message_with_reply.replies.is_some(),
"Message replies is None",
);
let replies = message_with_reply.replies.unwrap();
assert!(!replies.is_empty(), "Message replies is empty",);
assert_eq!(replies[0], message.id, "Message reply ID does not match",);
// Delete the message
message
.clone()
.delete(&harness.db)
.await
.expect("Failed to delete message");
// Attempt to create messages with a reply to a deleted message
// fail_if_not_exists is set to false
// Should send the message without a reply
let message_with_missing_reply = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some("Message with missing reply".to_string()),
nonce: None,
attachments: None,
replies: Some(vec![v0::ReplyIntent {
id: message.id.clone(),
mention: false,
fail_if_not_exists: Some(false),
}]),
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("3".to_string()),
false,
false,
)
.await
.expect("Failed to create message with missing reply");
assert!(
message_with_missing_reply.replies.is_none()
|| message_with_missing_reply.replies.unwrap().is_empty(),
"Message replies exist when they shouldn't",
);
// fail_if_not_exists is set to true
// Should fail to send the message
Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some("Message with missing reply".to_string()),
nonce: None,
attachments: None,
replies: Some(vec![v0::ReplyIntent {
id: message.id.clone(),
mention: false,
fail_if_not_exists: Some(true),
}]),
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("4".to_string()),
false,
false,
)
.await
.expect_err("Created message with missing reply and true fail");
// fail_if_not_exists is not set
// Should fail to send the message
Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some("Message with missing reply".to_string()),
nonce: None,
attachments: None,
replies: Some(vec![v0::ReplyIntent {
id: message.id.clone(),
mention: false,
fail_if_not_exists: None,
}]),
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&harness.db, Some(&user)).await),
Some(user.clone().into(&harness.db, Some(&user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("4".to_string()),
false,
false,
)
.await
.expect_err("Created message with missing reply and none fail");
}
#[rocket::async_test]
async fn mass_mentions_test() {
let harness = TestHarness::new().await;
let (_, _, user) = harness.new_user().await;
let (_, _, other_user) = harness.new_user().await;
let (server, _) = harness.new_server(&user).await;
let channel = harness.new_channel(&server).await;
let (role_id, mut role) = harness
.new_role(
&server,
1,
Some(OverrideField {
a: (ChannelPermission::MentionEveryone as i64)
| (ChannelPermission::MentionRoles as i64),
d: 0,
}),
)
.await;
let (mut other_member, _) = Member::create(&harness.db, &server, &other_user, None)
.await
.expect("Failed to add test member");
// Send a message with an everyone and role mention.
// Should fail
let bad_message_with_mentions = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some(format!("Mentioning @everyone and role <%{}>", &role_id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(
&other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(
other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(other_member.clone().into()),
other_user.limits().await,
IdempotencyKey::unchecked_from_string("1".to_string()),
false,
true,
)
.await
.expect_err("Should not have created message with everyone and role pings");
assert!(
matches!(
bad_message_with_mentions.error_type,
ErrorType::MissingPermission { .. }
),
"Intentional permissions error did not return MissingPermission"
);
// Send a mass mention inside a codeblock.
// Should be undetected and therefor pass
let message_with_codeblock = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some(format!("Mentioning `@everyone` and role `<%{}>`", &role_id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(
&other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(
other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(other_member.clone().into()),
other_user.limits().await,
IdempotencyKey::unchecked_from_string("1".to_string()),
false,
true,
)
.await
.expect("Failed to create message with everyone and role pings in codeblocks");
assert!(
message_with_codeblock.flags.is_none()
|| !MessageFlagsValue(message_with_codeblock.flags.unwrap())
.has(MessageFlags::MentionsEveryone),
"Message flags mentions everyone when inside codeblock",
);
assert!(
message_with_codeblock.role_mentions.is_none(),
"Role mentions detected when inside codeblock"
);
other_member.roles.push(role_id.clone());
harness
.db
.update_member(
&other_member.id,
&PartialMember {
avatar: None,
id: None,
joined_at: None,
nickname: None,
roles: Some(vec![role_id.clone()]),
timeout: None,
},
vec![],
)
.await
.expect("Failed to add role to user");
// Send a message with an everyone and role mention.
// Should succeed
let message_with_mentions = Message::create_from_api(
&harness.db,
Some(&harness.amqp),
channel.clone(),
v0::DataMessageSend {
content: Some(format!("Mentioning @everyone and role <%{}>", &role_id)),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(
&other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(
other_user
.clone()
.into(&harness.db, Some(&other_user))
.await,
),
Some(other_member.clone().into()),
other_user.limits().await,
IdempotencyKey::unchecked_from_string("1".to_string()),
false,
true,
)
.await
.expect("Failed to create message with everyone and role pings");
assert!(
message_with_mentions.flags.is_some(),
"Message flags is None",
);
assert!(
MessageFlagsValue(message_with_mentions.flags.unwrap())
.has(MessageFlags::MentionsEveryone),
"Message flags does not mention everyone. Flag value is {}",
message_with_mentions.flags.unwrap()
);
assert!(
message_with_mentions.role_mentions.is_some(),
"Message has no role mentions"
);
}
} }
@@ -1,4 +1,7 @@
use revolt_database::{util::{permissions::DatabasePermissionQuery, reference::Reference}, Channel, Database, FieldsMessage, PartialMessage, SystemMessage, User, AMQP}; use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference},
Database, FieldsMessage, PartialMessage, SystemMessage, User, AMQP,
};
use revolt_models::v0::MessageAuthor; use revolt_models::v0::MessageAuthor;
use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; use revolt_permissions::{calculate_channel_permissions, ChannelPermission};
use revolt_result::{create_error, Result}; use revolt_result::{create_error, Result};
@@ -19,12 +22,10 @@ pub async fn message_unpin(
) -> Result<EmptyResponse> { ) -> Result<EmptyResponse> {
let channel = target.as_channel(db).await?; let channel = target.as_channel(db).await?;
if !matches!(channel, Channel::DirectMessage { .. }) { let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel);
let mut query = DatabasePermissionQuery::new(db, &user).channel(&channel); calculate_channel_permissions(&mut query)
calculate_channel_permissions(&mut query) .await
.await .throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
.throw_if_lacking_channel_permission(ChannelPermission::ManageMessages)?;
}
let mut message = msg.as_message_in_channel(db, channel.id()).await?; let mut message = msg.as_message_in_channel(db, channel.id()).await?;
@@ -27,6 +27,11 @@ pub async fn create_emoji(
}) })
})?; })?;
// Bots cannot manage emojis
if user.bot.is_some() {
return Err(create_error!(IsBot));
}
// Validate we have permission to write into parent // Validate we have permission to write into parent
match &data.parent { match &data.parent {
v0::EmojiParent::Server { id } => { v0::EmojiParent::Server { id } => {
@@ -18,6 +18,10 @@ pub async fn delete_emoji(
user: User, user: User,
emoji_id: Reference, emoji_id: Reference,
) -> Result<EmptyResponse> { ) -> Result<EmptyResponse> {
// Bots cannot manage emoji
if user.bot.is_some() {
return Err(create_error!(IsBot));
}
// Fetch the emoji // Fetch the emoji
let emoji = emoji_id.as_emoji(db).await?; let emoji = emoji_id.as_emoji(db).await?;
@@ -76,169 +76,3 @@ pub async fn fetch(db: &State<Database>, target: Reference) -> Result<Json<v0::I
} }
})) }))
} }
#[cfg(test)]
mod test {
use crate::{rocket, util::test::TestHarness};
use revolt_database::{Channel, Server};
use revolt_models::v0::{
DataCreateGroup, DataCreateServerChannel, Invite, InviteResponse, LegacyServerChannelType,
};
use rocket::http::Status;
#[rocket::async_test]
async fn success_fetch_group_invite() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let group = Channel::create_group(
&harness.db,
DataCreateGroup {
..Default::default()
},
user.id.clone(),
)
.await
.expect("`Channel`");
let create_response = TestHarness::with_session(
session,
harness
.client
.post(format!("/channels/{}/invites", group.id())),
)
.await;
assert_eq!(create_response.status(), Status::Ok);
let invite_from_create: Invite = create_response.into_json().await.expect("`Invite`");
let invite_code = match invite_from_create {
Invite::Group { code, .. } => code,
_ => unreachable!(),
};
let response = harness
.client
.get(format!("/invites/{}", invite_code))
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let invite_response: InviteResponse = response.into_json().await.expect("`FetchInvite`");
match invite_response {
InviteResponse::Group {
code,
channel_id,
user_name,
..
} => {
assert_eq!(code, invite_code);
assert_eq!(channel_id, group.id());
assert_eq!(user_name, user.username);
}
_ => unreachable!(),
}
}
#[rocket::async_test]
async fn fail_fetch_missing_invite() {
let harness = TestHarness::new().await;
let response = harness
.client
.get(format!("/invites/{}", TestHarness::rand_string()))
.dispatch()
.await;
assert_eq!(response.status(), Status::NotFound);
}
#[rocket::async_test]
async fn success_fetch_text_channel_invite() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (_, channels) = harness.new_server(&user).await;
let channel = channels.first().expect("Server Channel");
let create_response = TestHarness::with_session(
session,
harness
.client
.post(format!("/channels/{}/invites", channel.id())),
)
.await;
assert_eq!(create_response.status(), Status::Ok);
let invite_from_create: Invite = create_response.into_json().await.expect("`Invite`");
let invite_code = match invite_from_create {
Invite::Server { code, .. } => code,
_ => unreachable!(),
};
let response = harness
.client
.get(format!("/invites/{}", invite_code))
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let invite_response: InviteResponse = response.into_json().await.expect("`FetchInvite`");
match invite_response {
InviteResponse::Server {
code,
channel_id,
user_name,
..
} => {
assert_eq!(code, invite_code);
assert_eq!(channel_id, channel.id());
assert_eq!(user_name, user.username);
}
_ => unreachable!(),
};
}
#[rocket::async_test]
async fn success_fetch_voice_channel_invite() {
let harness = TestHarness::new().await;
let (_, session, user) = harness.new_user().await;
let (server, _) = harness.new_server(&user).await;
let server_mut: &mut Server = &mut server.clone();
let channel = Channel::create_server_channel(
&harness.db,
server_mut,
DataCreateServerChannel {
channel_type: LegacyServerChannelType::Voice,
name: "Voice Channel".to_string(),
description: None,
nsfw: Some(false),
},
true,
)
.await
.expect("Failed to make new channel");
let create_response = TestHarness::with_session(
session,
harness
.client
.post(format!("/channels/{}/invites", channel.id())),
)
.await;
assert_eq!(create_response.status(), Status::Ok);
let invite_from_create: Invite = create_response.into_json().await.expect("`Invite`");
let invite_code = match invite_from_create {
Invite::Server { code, .. } => code,
_ => unreachable!(),
};
let response = harness
.client
.get(format!("/invites/{}", invite_code))
.dispatch()
.await;
assert_eq!(response.status(), Status::Ok);
let invite_response: InviteResponse = response.into_json().await.expect("`FetchInvite`");
match invite_response {
InviteResponse::Server {
code,
channel_id,
user_name,
..
} => {
assert_eq!(code, invite_code);
assert_eq!(channel_id, channel.id());
assert_eq!(user_name, user.username);
}
_ => unreachable!(),
};
}
}
@@ -788,7 +788,9 @@ pub async fn webhook_execute_github(
r#ref, r#ref,
.. ..
}) => { }) => {
let branch = r#ref.split('/').skip(2).collect::<Vec<_>>().join("/"); let Some(branch) = r#ref.split('/').nth(2) else {
return Ok(());
};
if forced { if forced {
let description = format!( let description = format!(
@@ -815,22 +817,19 @@ pub async fn webhook_execute_github(
commits.len(), commits.len(),
compare compare
); );
let commit_description = shorten_text( let commit_description = commits
&commits .into_iter()
.into_iter() .map(|commit| {
.map(|commit| { format!(
format!( "[`{}`]({}) {} - {}",
"[`{}`]({}) {} - {}", &commit.id[0..=7],
&commit.id[0..=7], commit.url,
commit.url, shorten_text(&commit.message, 50),
shorten_text(&commit.message, 50), commit.author.name
commit.author.name )
) })
}) .collect::<Vec<String>>()
.collect::<Vec<String>>() .join("\n");
.join("\n"),
1000
);
SendableEmbed { SendableEmbed {
title: Some(event.sender.login), title: Some(event.sender.login),
+2 -106
View File
@@ -5,14 +5,9 @@ use authifier::{
use futures::StreamExt; use futures::StreamExt;
use rand::Rng; use rand::Rng;
use redis_kiss::redis::aio::PubSub; use redis_kiss::redis::aio::PubSub;
use revolt_database::{ use revolt_database::{events::client::EventV1, Database, User, AMQP};
events::client::EventV1, Channel, Database, Member, Message, Server, User, AMQP,
};
use revolt_database::{util::idempotency::IdempotencyKey, Role};
use revolt_models::v0; use revolt_models::v0;
use revolt_permissions::OverrideField; use rocket::local::asynchronous::Client;
use rocket::http::Header;
use rocket::local::asynchronous::{Client, LocalRequest, LocalResponse};
pub struct TestHarness { pub struct TestHarness {
pub client: Client, pub client: Client,
@@ -109,105 +104,6 @@ impl TestHarness {
(account, session, user) (account, session, user)
} }
pub async fn new_server(&self, user: &User) -> (Server, Vec<Channel>) {
Server::create(
&self.db,
v0::DataCreateServer {
name: "Test Server".to_string(),
..Default::default()
},
user,
true,
)
.await
.expect("Failed to create test server")
}
pub async fn new_role(
&self,
server: &Server,
rank: i64,
overrides: Option<OverrideField>,
) -> (String, Role) {
let role = Role {
name: TestHarness::rand_string(),
permissions: overrides.unwrap_or(OverrideField { a: 0, d: 0 }),
rank,
colour: None,
hoist: false,
};
let id = role
.create(&self.db, &server.id)
.await
.expect("Failed to create test role");
(id, role)
}
pub async fn new_channel(&self, server: &Server) -> Channel {
Channel::create_server_channel(
&self.db,
&mut server.clone(),
v0::DataCreateServerChannel {
channel_type: v0::LegacyServerChannelType::Text,
name: "Test Channel".to_string(),
description: None,
nsfw: Some(false),
},
true,
)
.await
.expect("Failed to make test channel")
}
pub async fn new_message(
&self,
user: &User,
server: &Server,
channels: Vec<Channel>,
) -> (Channel, Member, Message) {
let (member, channels) = Member::create(&self.db, server, user, Some(channels))
.await
.expect("Failed to create member");
let channel = &channels[0];
let message = Message::create_from_api(
&self.db,
None,
channel.clone(),
v0::DataMessageSend {
content: Some("Test message".to_string()),
nonce: None,
attachments: None,
replies: None,
embeds: None,
masquerade: None,
interactions: None,
flags: None,
},
v0::MessageAuthor::User(&user.clone().into(&self.db, Some(user)).await),
Some(user.clone().into(&self.db, Some(user)).await),
Some(member.clone().into()),
user.limits().await,
IdempotencyKey::unchecked_from_string("0".to_string()),
false,
false,
)
.await
.expect("Failed to create message");
(channel.clone(), member, message)
}
pub async fn with_session<'c>(
session: Session,
request: LocalRequest<'c>,
) -> LocalResponse<'c> {
request
.header(Header::new("x-session-token", session.token.to_string()))
.dispatch()
.await
}
pub async fn wait_for_event<F>(&mut self, topic: &str, predicate: F) -> EventV1 pub async fn wait_for_event<F>(&mut self, topic: &str, predicate: F) -> EventV1
where where
F: Fn(&EventV1) -> bool, F: Fn(&EventV1) -> bool,
+5 -6
View File
@@ -1,8 +1,7 @@
[package] [package]
name = "revolt-autumn" name = "revolt-autumn"
version = "0.8.4" version = "0.8.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
# ID generation # ID generation
@@ -43,12 +42,12 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Core crates # Core crates
revolt-files = { version = "0.8.4", path = "../../core/files" } revolt-files = { version = "0.8.2", path = "../../core/files" }
revolt-config = { version = "0.8.4", path = "../../core/config" } revolt-config = { version = "0.8.2", path = "../../core/config" }
revolt-database = { version = "0.8.4", path = "../../core/database", features = [ revolt-database = { version = "0.8.2", path = "../../core/database", features = [
"axum-impl", "axum-impl",
] } ] }
revolt-result = { version = "0.8.4", path = "../../core/result", features = [ revolt-result = { version = "0.8.2", path = "../../core/result", features = [
"utoipa", "utoipa",
"axum", "axum",
] } ] }
+5 -6
View File
@@ -1,8 +1,7 @@
[package] [package]
name = "revolt-january" name = "revolt-january"
version = "0.8.4" version = "0.8.2"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
# Utility # Utility
@@ -32,13 +31,13 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Core crates # Core crates
revolt-config = { version = "0.8.4", path = "../../core/config" } revolt-config = { version = "0.8.2", path = "../../core/config" }
revolt-models = { version = "0.8.4", path = "../../core/models" } revolt-models = { version = "0.8.2", path = "../../core/models" }
revolt-result = { version = "0.8.4", path = "../../core/result", features = [ revolt-result = { version = "0.8.2", path = "../../core/result", features = [
"utoipa", "utoipa",
"axum", "axum",
] } ] }
revolt-files = { version = "0.8.4", path = "../../core/files" } revolt-files = { version = "0.8.2", path = "../../core/files" }
# Axum / web server # Axum / web server
axum = { version = "0.7.5" } axum = { version = "0.7.5" }
+72 -113
View File
@@ -9,11 +9,6 @@
# The values provided in this template are the default values that will be used # The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration # when any section or field is not specified in your own configuration
# Root options
# The graph table configures how the dependency graph is constructed and thus
# which crates the checks are performed against
[graph]
# If 1 or more target triples (and optionally, target_features) are specified, # If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`. # only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific # This means, if a particular package is only ever used as a target specific
@@ -25,83 +20,85 @@
targets = [ targets = [
# The triple can be any string, but only the target triples built in to # The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions # rustc (as of 1.40) can be checked against actual config expressions
#"x86_64-unknown-linux-musl", #{ triple = "x86_64-unknown-linux-musl" },
# You can also specify which target_features you promise are enabled for a # You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against # particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture. # the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, #{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
] ]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories` # This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here: # More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html # https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories] [advisories]
# The path where the advisory databases are cloned/fetched into # The path where the advisory database is cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs" db-path = "~/.cargo/advisory-db"
# The url(s) of the advisory databases to use # The url(s) of the advisory databases to use
#db-urls = ["https://github.com/rustsec/advisory-db"] db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still # A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered. # output a note when they are encountered.
ignore = [ ignore = [
#"RUSTSEC-0000-0000", #"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
] ]
# If this is true, then cargo deny will use the git executable to fetch advisory database. # Threshold for security vulnerabilities, any vulnerability with a CVSS score
# If this is false, then it uses a built-in git library. # lower than the range specified will be ignored. Note that ignored advisories
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. # will still output a note when they are encountered.
# See Git Authentication for more information about setting up git authentication. # * None - CVSS Score 0.0
#git-fetch-with-cli = true # * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# This section is considered when running `cargo deny check licenses` # This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here: # More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html # https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses] [licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "warn"
# List of explicitly allowed licenses # List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses # See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. # [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [ allow = [
# any non-copyleft licenses are okay!
"MIT", "MIT",
"ISC", "ISC",
"0BSD", "0BSD",
"Zlib",
"CC0-1.0", "CC0-1.0",
"MPL-2.0",
"Apache-2.0", "Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause", "BSD-2-Clause",
"BSD-3-Clause", "BSD-3-Clause",
"OpenSSL", #"Apache-2.0 WITH LLVM-exception",
] ]
# List of explicitly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "warn"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "neither"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text. # The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the # The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file. # canonical license text of a valid SPDX license file.
@@ -111,34 +108,29 @@ confidence-threshold = 0.8
# aren't accepted for every possible crate as with the normal allow list # aren't accepted for every possible crate as with the normal allow list
exceptions = [ exceptions = [
# Each entry is the crate and version constraint, and its specific allow # Each entry is the crate and version constraint, and its specific allow
{ allow = ["AGPL-3.0"], crate = "revolt-database" }, # list
{ allow = ["AGPL-3.0"], crate = "revolt-presence" }, #{ allow = ["Zlib"], name = "adler32", version = "*" },
{ allow = ["AGPL-3.0"], crate = "revolt-files" },
{ allow = ["AGPL-3.0"], crate = "revolt-pushd" },
{ allow = ["AGPL-3.0"], crate = "revolt-crond" },
{ allow = ["AGPL-3.0"], crate = "revolt-delta" },
{ allow = ["AGPL-3.0"], crate = "revolt-bonfire" },
{ allow = ["AGPL-3.0"], crate = "revolt-autumn" },
{ allow = ["AGPL-3.0"], crate = "revolt-january" },
] ]
# Some crates don't have (easily) machine readable licensing information, # Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the # adding a clarification entry for it allows you to manually specify the
# licensing information # licensing information
[[licenses.clarify]] #[[licenses.clarify]]
# The package spec the clarification applies to # The name of the crate the clarification applies to
crate = "ring" #name = "ring"
# The optional version constraint for the crate
#version = "*"
# The SPDX expression for the license requirements of the crate # The SPDX expression for the license requirements of the crate
expression = "MIT AND ISC AND OpenSSL" #expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for # One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used # the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored # when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors # and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration # depending on the rest of your configuration
license-files = [ #license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents # Each entry is a crate relative path, and the (opaque) hash of its contents
{ path = "LICENSE", hash = 0xbd0eed23 }, #{ path = "LICENSE", hash = 0xbd0eed23 }
] #]
[licenses.private] [licenses.private]
# If true, ignores workspace crates that aren't published, or are only # If true, ignores workspace crates that aren't published, or are only
@@ -167,63 +159,30 @@ wildcards = "allow"
# * simplest-path - The path to the version with the fewest edges is highlighted # * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used # * all - Both lowest-version and simplest-path are used
highlight = "all" highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overridden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overridden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care! # List of crates that are allowed. Use with care!
allow = [ allow = [
#"ansi_term@0.11.0", #{ name = "ansi_term", version = "=0.11.0" },
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
] ]
# List of crates to deny # List of crates to deny
deny = [ deny = [
#"ansi_term@0.11.0", # Each entry the name of a crate and a version range. If version is
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" }, # not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
# Wrapper crates can optionally be specified to allow the crate when it # Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate # is a direct dependency of the otherwise banned crate
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] }, #{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
] ]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#crate = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection. # Certain crates/versions that will be skipped when doing duplicate detection.
skip = [ skip = [
#"ansi_term@0.11.0", #{ name = "ansi_term", version = "=0.11.0" },
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
] ]
# Similarly to `skip` allows you to skip certain crates during duplicate # Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive # detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is # dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite. # by default infinite
skip-tree = [ skip-tree = [
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies #{ name = "ansi_term", version = "=0.11.0", depth = 20 },
#{ crate = "ansi_term@0.11.0", depth = 20 },
] ]
# This section is considered when running `cargo deny check sources`. # This section is considered when running `cargo deny check sources`.
@@ -243,9 +202,9 @@ allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = [] allow-git = []
[sources.allow-org] [sources.allow-org]
# github.com organizations to allow git sources for # 1 or more github.com organizations to allow git sources for
github = [] github = ["insertish"]
# gitlab.com organizations to allow git sources for # 1 or more gitlab.com organizations to allow git sources for
gitlab = [] # gitlab = []
# bitbucket.org organizations to allow git sources for # 1 or more bitbucket.org organizations to allow git sources for
bitbucket = [] # bitbucket = [""]
+4 -4
View File
@@ -54,9 +54,9 @@ deps() {
tee crates/core/result/src/lib.rs tee crates/core/result/src/lib.rs
if [ -z "$TARGETARCH" ]; then if [ -z "$TARGETARCH" ]; then
cargo build -j 24 --locked --release cargo build --locked --release
else else
cargo build -j 24 --locked --release --target "${BUILD_TARGET}" cargo build --locked --release --target "${BUILD_TARGET}"
fi fi
} }
@@ -74,9 +74,9 @@ apps() {
crates/core/result/src/lib.rs crates/core/result/src/lib.rs
if [ -z "$TARGETARCH" ]; then if [ -z "$TARGETARCH" ]; then
cargo build -j 24 --locked --release cargo build --locked --release
else else
cargo build -j 24 --locked --release --target "${BUILD_TARGET}" cargo build --locked --release --target "${BUILD_TARGET}"
mv target _target && mv _target/"${BUILD_TARGET}" target mv target _target && mv _target/"${BUILD_TARGET}" target
fi fi
} }
-93
View File
@@ -1,93 +0,0 @@
# a test script that generates a ton of users for debugging use
# note that you'll need to comment out the ratelimiter in delta/src/main.rs
# and keep the number relatively low or requests will time out (the beefier the machine the more you can handle).
# this script assumes mailhog is running, and uses that to automate "emails".
# In the real world, antispam will catch this and nuke you to hell and back.
# But it works fine in a dev env!
# requires httpx
import asyncio
import os
import re
import uuid
import httpx
API_URL: str = os.getenv("API_URL") # type: ignore
MAILHOG_API: str = os.getenv("MAILHOG_API") # type: ignore
COUNT = int(os.getenv("COUNT")) # type: ignore # cbf to deal with type checking
INVITE: str = os.getenv("INVITE") # type: ignore
assert API_URL and MAILHOG_API and COUNT and INVITE
API_URL = API_URL.strip("/")
MAILHOG_API = MAILHOG_API.strip("/")
async def filter_hog(client: httpx.AsyncClient, email: str) -> str:
"""
returns the token provided by the mail server.
This script assumes the use of mailhog.
"""
resp = await client.get(MAILHOG_API + "/api/v2/search", params={"kind": "to", "query": email}, follow_redirects=True, timeout=60)
if resp.status_code != 200:
raise Exception(resp.status_code, resp.content)
data = resp.json()
if not data["items"]:
raise Exception("No message found")
message_id = data["items"][0]["ID"]
body = data["items"][0]["Content"]["Body"].replace("\r", "")
token = re.search("/login/verify(=\n/|/\n=|/=\n)(?P<token>[^\n]+)", body, re.MULTILINE)
if not token:
raise Exception("No token found")
ret = token.group("token")
await client.delete(MAILHOG_API + f"/api/v1/messages/{message_id}", timeout=60)
return ret
async def task() -> None:
_id = str(uuid.uuid4())[:4]
email = f"{_id}@example.com"
async with httpx.AsyncClient() as client:
resp = await client.post(API_URL + "/auth/account/create", json={"email": email, "password": _id*3, "invite": INVITE}, timeout=60)
if resp.status_code != 204:
raise Exception(resp.status_code, resp.content)
token = await filter_hog(client, email)
resp = await client.post(API_URL + f"/auth/account/verify/{token}", timeout=60)
if resp.status_code != 200:
raise Exception("verify", resp.status_code, resp.content)
ticket = resp.json()["ticket"]
userid = ticket["_id"]
resp = await client.post(API_URL + "/auth/session/login", json={"email": email, "password": _id*3, "friendly_name": "Not A Client"}, timeout=60)
if resp.status_code != 200:
raise Exception("session", resp.status_code, resp.content)
session = resp.json()
token = session["token"]
resp = await client.post(API_URL + "/onboard/complete", json={"username": _id}, headers={"x-session-token": token}, timeout=60) # complete onboarding to allow creating a session
if resp.status_code != 200:
raise Exception("onboard", resp.status_code, resp.content)
resp = await client.post(API_URL + f"/invites/{INVITE}", headers={"x-session-token": token}, timeout=60)
if resp.status_code != 200:
raise Exception("invite", resp.status_code, resp.content)
print(f"Created account and session for {email} with ID: {userid}")
return userid
async def main():
tasks = [asyncio.create_task(task()) for _ in range(COUNT)]
print(await asyncio.gather(*tasks))
asyncio.run(main())