Compare commits

...
14 Commits
74 changed files with 5417 additions and 801 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
Rocket.toml
Revolt.toml
Revolt.*.toml
target
.data
+1 -2
View File
@@ -1,6 +1,5 @@
{
"editor.formatOnSave": true,
"rust-analyzer.checkOnSave.command": "clippy",
"nixEnvSelector.suggestion": false,
"nixEnvSelector.nixFile": "${workspaceRoot}/default.nix"
"nixEnvSelector.suggestion": false
}
Generated
+1166 -56
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM --platform="${BUILDPLATFORM}" rust:1.80.1-slim-bookworm
FROM --platform="${BUILDPLATFORM}" rust:1.77.2-slim-bookworm
USER 0:0
WORKDIR /home/rust/src
@@ -28,6 +28,7 @@ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps
+2 -1
View File
@@ -1,5 +1,5 @@
# Build Stage
FROM rust:1.80.1-slim-bookworm
FROM rust:1.77.2-slim-bookworm
USER 0:0
WORKDIR /home/rust/src
@@ -24,6 +24,7 @@ COPY crates/core/permissions/Cargo.toml ./crates/core/permissions/
COPY crates/core/presence/Cargo.toml ./crates/core/presence/
COPY crates/core/result/Cargo.toml ./crates/core/result/
COPY crates/services/autumn/Cargo.toml ./crates/services/autumn/
COPY crates/services/january/Cargo.toml ./crates/services/january/
RUN sh /tmp/build-image-layer.sh deps
# Build all apps
+28 -26
View File
@@ -19,6 +19,9 @@ Note: `january`, `autumn`, and `vortex` are yet to be moved into this monorepo.
Rust 1.76 or higher.
> [!CAUTION]
> The events server has a significant performance regression between Rust 1.77.2 and 1.78.0 onwards, see [issue #341](https://github.com/revoltchat/backend/issues/341).
## Development Guide
Before contributing, make yourself familiar with [our contribution guidelines](https://developers.revolt.chat/contrib.html) and the [technical documentation for this project](https://revoltchat.github.io/backend/).
@@ -33,6 +36,20 @@ Before getting started, you'll want to install:
> A **default.nix** is available for Nix users!
> Just run `nix-shell` and continue.
As a heads-up, the development environment uses the following ports:
| Service | Port |
| ------------------------- | :------------: |
| MongoDB | 14017 |
| Redis | 14079 |
| MinIO | 14009 |
| Maildev | 14025<br>14080 |
| Revolt Web App | 14701 |
| `crates/delta` | 14702 |
| `crates/bonfire` | 14703 |
| `crates/services/autumn` | 14704 |
| `crates/services/january` | 14705 |
Now you can clone and build the project:
```bash
@@ -41,33 +58,14 @@ cd revolt-backend
cargo build
```
If you want to run the API and event servers:
A default configuration `Revolt.toml` is present in this project that is suited for development.
If you'd like to change anything, create a `Revolt.overrides.toml` file to overwrite it.
You may need to configure the legacy environment options:
```bash
# create environment file (will be deprecated in future)
cp .env.example .env
# (optionally) copy the default configuration file
cp crates/core/config/Revolt.toml Revolt.toml
# configure as necessary...
```
You may want to copy the following configuration:
```toml
# Revolt.toml
[database]
mongodb = "mongodb://localhost"
redis = "redis://localhost"
[hosts]
app = "http://local.revolt.chat"
api = "http://local.revolt.chat:8000"
events = "ws://local.revolt.chat:9000"
autumn = "http://local.revolt.chat:3000"
january = "http://local.revolt.chat:7000"
voso_legacy = ""
voso_legacy_ws = ""
```
Then continue:
@@ -80,6 +78,10 @@ docker compose up -d
cargo run --bin revolt-delta
# run the events server
cargo run --bin revolt-bonfire
# run the file server
cargo run --bin revolt-autumn
# run th proxy server
cargo run --bin revolt-january
# hint:
# mold -run <cargo build, cargo run, etc...>
@@ -96,10 +98,10 @@ git clone --recursive https://github.com/revoltchat/revite
cd revite
yarn
yarn build:deps
yarn dev --port 3001
yarn dev --port 14701
```
Then go to https://local.revolt.chat:3001
Then go to https://local.revolt.chat:14701
## Deployment Guide
+199
View File
@@ -0,0 +1,199 @@
# ⚠️ This configuration is intended for development environment.
# If you'd like to override anything, create a Revolt.override.toml
[database]
# MongoDB connection URL
# Defaults to the container name specified in self-hosted
mongodb = "mongodb://localhost:14017"
# Redis connection URL
# Defaults to the container name specified in self-hosted
redis = "redis://localhost:14079/"
[hosts]
# Web locations of various services
# Defaults assume all services are reverse-proxied
# See https://github.com/revoltchat/self-hosted/blob/master/Caddyfile
#
# Remember to change these to https/wss where appropriate in production!
app = "http://local.revolt.chat:14701"
api = "http://local.revolt.chat:14702"
events = "ws://local.revolt.chat:14703"
autumn = "http://local.revolt.chat:14704"
january = "http://local.revolt.chat:14705"
voso_legacy = ""
voso_legacy_ws = ""
[api]
[api.registration]
# Whether an invite should be required for registration
# See https://github.com/revoltchat/self-hosted#making-your-instance-invite-only
invite_only = false
[api.smtp]
# Email server configuration for verification
# Defaults to no email verification (host field is empty)
host = "localhost"
username = "smtp"
password = "smtp"
from_address = "development@revolt.chat"
reply_to = "support@revolt.chat"
port = 14025
use_tls = false
[api.vapid]
# Generate your own keys:
# 1. Run `openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem`
# 2. Find `private_key` using `base64 vapid_private.pem`
# 3. Find `public_key` using `openssl ec -in vapid_private.pem -outform DER|tail -c 65|base64|tr '/+' '_-'|tr -d '\n'`
private_key = "LS0tLS1CRUdJTiBFQyBQUklWQVRFIEtFWS0tLS0tCk1IY0NBUUVFSUJSUWpyTWxLRnBiVWhsUHpUbERvcEliYk1yeVNrNXpKYzVYVzIxSjJDS3hvQW9HQ0NxR1NNNDkKQXdFSG9VUURRZ0FFWnkrQkg2TGJQZ2hEa3pEempXOG0rUXVPM3pCajRXT1phdkR6ZU00c0pqbmFwd1psTFE0WAp1ZDh2TzVodU94QWhMQlU3WWRldVovWHlBdFpWZmNyQi9BPT0KLS0tLS1FTkQgRUMgUFJJVkFURSBLRVktLS0tLQo"
public_key = "BGcvgR-i2z4IQ5Mw841vJvkLjt8wY-FjmWrw83jOLCY52qcGZS0OF7nfLzuYbjsQISwVO2HXrmf18gLWVX3Kwfw="
[api.fcm]
# Google Firebase Cloud Messaging Service Account Key
# Obtained from the cloud messaging console
key_type = ""
project_id = ""
private_key_id = ""
private_key = ""
client_email = ""
client_id = ""
auth_uri = ""
token_uri = ""
auth_provider_x509_cert_url = ""
client_x509_cert_url = ""
[api.apn]
# Apple Push Notifications keys for sending notifications
sandbox = false
pkcs8 = ""
key_id = ""
team_id = ""
[api.security]
# Authifier Shield API key
authifier_shield_key = ""
# Legacy voice server management token
voso_legacy_token = ""
# Whether services are behind the Cloudflare network
trust_cloudflare = false
[api.security.captcha]
# hCaptcha configuration
hcaptcha_key = ""
hcaptcha_sitekey = ""
[api.workers]
# Maximum concurrent connections (to proxy server)
max_concurrent_connections = 50
[files]
# Encryption key for stored files
# Generate your own key using `openssl rand -base64 32`
encryption_key = "qcuMA+ssxhMyKaNAKBGFfryfFtUH8NDlamQyDwGW6fU="
# Quality used for lossy WebP previews (set to 100 for lossless)
webp_quality = 80.0
# Mime types that cannot be uploaded or served
#
# Example for Windows executables and Android installation files:
# ["application/vnd.microsoft.portable-executable", "application/vnd.android.package-archive"]
blocked_mime_types = []
# ClamAV service
# hostname:port
clamd_host = ""
[files.limit]
# Minimum image resolution
min_resolution = [1, 1]
# Maximum MP of images
max_mega_pixels = 40
# Maximum pixel side of an image
max_pixel_side = 10_000
[files.preview]
# Maximum image resolution
attachments = [1280, 1280]
avatars = [128, 128]
backgrounds = [1280, 720]
icons = [128, 128]
banners = [480, 480]
emojis = [128, 128]
[files.s3]
# Configuration for S3
# Defaults included for MinIO + self-hosted setup
#
# Backblaze B2:
# - endpoint is listed on the "Buckets" page
# - region is `eu-central-003` string from endpoint URL
# - access_key_id is keyID generated on the "Application Keys" page
# - secret_access_key is token generated on the "Application Keys" page
# - default_bucket matches the name of the bucket you've created
# S3 protocol endpoint
endpoint = "http://localhost:14009"
# S3 region name
region = "minio"
# S3 protocol key ID
access_key_id = "minioautumn"
# S3 protocol access key
secret_access_key = "minioautumn"
# Bucket to upload to by default
default_bucket = "revolt-uploads"
[features]
# Feature gate options
webhooks_enabled = false
[features.limits]
[features.limits.global]
group_size = 100
message_embeds = 5
message_replies = 5
message_reactions = 20
server_emoji = 100
server_roles = 200
server_channels = 200
# How many days since creation a user is considered new
new_user_days = 3
# Maximum permissible body size in bytes for uploads
# (should be greater than any one file upload limit)
body_limit_size = 20_000_000
[features.limits.new_user]
outgoing_friend_requests = 5
bots = 2
message_length = 2000
message_attachments = 5
servers = 100
attachment_size = 20_000_000
avatar_size = 4_000_000
background_size = 6_000_000
icon_size = 2_500_000
banner_size = 6_000_000
emoji_size = 500_000
[features.limits.default]
outgoing_friend_requests = 10
bots = 5
message_length = 2000
message_attachments = 5
servers = 100
attachment_size = 20_000_000
avatar_size = 4_000_000
background_size = 6_000_000
icon_size = 2_500_000
banner_size = 6_000_000
emoji_size = 500_000
[sentry]
# Configuration for Sentry error reporting
api = ""
events = ""
+55
View File
@@ -0,0 +1,55 @@
services:
# Redis
redis:
image: eqalpha/keydb
ports:
- "14079:6379"
# MongoDB
database:
image: mongo
ports:
- "14017:27017"
volumes:
- ./.data/db:/data/db
# MinIO
minio:
image: minio/minio
command: server /data
environment:
MINIO_ROOT_USER: minioautumn
MINIO_ROOT_PASSWORD: minioautumn
volumes:
- ./.data/minio:/data
ports:
- "14009:9000"
restart: always
# Create buckets for minio.
createbuckets:
image: minio/mc
depends_on:
- minio
environment:
MINIO_ROOT_USER: minioautumn
MINIO_ROOT_PASSWORD: minioautumn
entrypoint: >
/bin/sh -c "
/usr/bin/mc config host add minio http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD;
while ! /usr/bin/mc ready minio; do echo 'Waiting minio...' && sleep 1; done;
/usr/bin/mc mb minio/revolt-uploads;
exit 0;
"
# Mock SMTP server
maildev:
image: soulteary/maildev
ports:
- "14025:8080"
- "14080:8080"
environment:
MAILDEV_SMTP_PORT: 25
MAILDEV_WEB_PORT: 8080
MAILDEV_INCOMING_USER: smtp
MAILDEV_INCOMING_PASS: smtp
+1
View File
@@ -20,5 +20,6 @@ serde = { version = "1", features = ["derive"] }
async-std = "1.12.0"
revolt-config = { version = "0.7.16", path = "../../core/config" }
revolt-result = { version = "0.7.16", path = "../../core/result" }
revolt-database = { version = "0.7.16", path = "../../core/database" }
+18
View File
@@ -18,6 +18,12 @@ export declare interface Err {
location: string;
}
/**
* Initialises background tasks and logging, must be called before anything else!
* Can be called multiple times!
*/
export declare function init();
/**
* Gets a new handle to the Revolt database
* @returns {Database} Handle
@@ -64,3 +70,15 @@ export declare function proc_channels_create_dm(
userA: string,
userB: string
): Promise<Channel & { error: Err }>;
/**
* Suspend a user
* @param {string} user User
* @param {number} duration Duration (in days), set to 0 for indefinite
* @param {string} reason Pipe-separated list of reasons (e.g. reason1|reason2|reason3)
*/
export declare function proc_users_suspend(
user: OpaqueUser,
duration: number,
reason: string
): Promise<{ error: Err }>;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "revolt-nodejs-bindings",
"version": "0.7.15",
"version": "0.7.15-rev0.0.3",
"description": "Node.js bindings for the Revolt software",
"main": "index.node",
"scripts": {
+33 -2
View File
@@ -1,9 +1,35 @@
#[macro_use]
extern crate serde;
use std::sync::OnceLock;
use neon::prelude::*;
use revolt_database::{Database, DatabaseInfo};
fn js_init(mut cx: FunctionContext) -> JsResult<JsUndefined> {
static INIT: OnceLock<()> = OnceLock::new();
if INIT.get().is_none() {
INIT.get_or_init(|| {
async_std::task::block_on(async {
revolt_config::configure!(api);
match DatabaseInfo::Auto.connect().await {
Ok(db) => {
let authifier_db = db.clone().to_authifier().await.database;
revolt_database::tasks::start_workers(db, authifier_db);
Ok(())
}
Err(err) => Err(err),
}
})
.or_else(|err| cx.throw_error(err))
.unwrap();
});
}
Ok(cx.undefined())
}
struct DatabaseBinding(Database, Channel);
impl Finalize for DatabaseBinding {}
impl DatabaseBinding {
@@ -136,6 +162,9 @@ macro_rules! shim {
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
// initialise required background stuff
cx.export_function("init", js_init)?;
// database & model stuff
cx.export_function("database", js_database)?;
cx.export_function("model_data", js_data)?;
@@ -178,10 +207,12 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> {
shim!(
cx,
proc_users_suspend,
,
duration JsNumber 1
reason JsString 2,
user User 0,
|db| async move {
user.suspend(&db).await
let duration = duration as usize;
user.suspend(&db, if duration == 0 { None } else { Some(duration) }, Some(reason.split('|').map(|x| x.to_owned()).collect())).await
},
&user,
);
+1
View File
@@ -7,4 +7,5 @@ COPY --from=builder /home/rust/src/target/release/revolt-bonfire ./
EXPOSE 9000
USER nonroot
ENV HOST=0.0.0.0:9000
CMD ["./revolt-bonfire"]
+2 -2
View File
@@ -22,8 +22,8 @@ async fn main() {
clear_region(None).await;
// Setup a TCP listener to accept WebSocket connections on.
// By default, we bind to port 9000 on all interfaces.
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:9000".into());
// By default, we bind to port 14703 on all interfaces.
let bind = env::var("HOST").unwrap_or_else(|_| "0.0.0.0:14703".into());
info!("Listening on host {bind}");
let try_socket = TcpListener::bind(bind).await;
let listener = try_socket.expect("Failed to bind");
+17 -7
View File
@@ -34,9 +34,9 @@ host = ""
username = ""
password = ""
from_address = "noreply@example.com"
reply_to = "noreply@example.com"
port = 587
use_tls = true
# reply_to = "noreply@example.com"
# port = 587
# use_tls = true
[api.vapid]
# Generate your own keys:
@@ -88,6 +88,16 @@ max_concurrent_connections = 50
# Encryption key for stored files
# Generate your own key using `openssl rand -base64 32`
encryption_key = "qcuMA+ssxhMyKaNAKBGFfryfFtUH8NDlamQyDwGW6fU="
# Quality used for lossy WebP previews (set to 100 for lossless)
webp_quality = 80.0
# Mime types that cannot be uploaded or served
#
# Example for Windows executables and Android installation files:
# ["application/vnd.microsoft.portable-executable", "application/vnd.android.package-archive"]
blocked_mime_types = []
# ClamAV service
# hostname:port
clamd_host = ""
[files.limit]
# Minimum image resolution
@@ -118,13 +128,13 @@ emojis = [128, 128]
# - default_bucket matches the name of the bucket you've created
# S3 protocol endpoint
endpoint = ""
endpoint = "http://minio:9000"
# S3 region name
region = ""
region = "minio"
# S3 protocol key ID
access_key_id = ""
access_key_id = "minioautumn"
# S3 protocol access key
secret_access_key = ""
secret_access_key = "minioautumn"
# Bucket to upload to by default
default_bucket = "revolt-uploads"
+29
View File
@@ -126,6 +126,34 @@ pub struct Api {
pub workers: ApiWorkers,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FilesLimit {
pub min_resolution: [usize; 2],
pub max_mega_pixels: usize,
pub max_pixel_side: usize,
}
#[derive(Deserialize, Debug, Clone)]
pub struct FilesS3 {
pub endpoint: String,
pub region: String,
pub access_key_id: String,
pub secret_access_key: String,
pub default_bucket: String,
}
#[derive(Deserialize, Debug, Clone)]
pub struct Files {
pub encryption_key: String,
pub webp_quality: f32,
pub blocked_mime_types: Vec<String>,
pub clamd_host: String,
pub limit: FilesLimit,
pub preview: HashMap<String, [usize; 2]>,
pub s3: FilesS3,
}
#[derive(Deserialize, Debug, Clone)]
pub struct GlobalLimits {
pub group_size: usize,
@@ -186,6 +214,7 @@ pub struct Settings {
pub database: Database,
pub hosts: Hosts,
pub api: Api,
pub files: Files,
pub features: Features,
pub sentry: Sentry,
}
+92
View File
@@ -1,6 +1,14 @@
mod mongodb;
mod reference;
use authifier::config::Captcha;
use authifier::config::EmailVerificationConfig;
use authifier::config::ResolveIp;
use authifier::config::SMTPSettings;
use authifier::config::Shield;
use authifier::config::Template;
use authifier::config::Templates;
use authifier::Authifier;
use rand::Rng;
use revolt_config::config;
@@ -87,3 +95,87 @@ impl DatabaseInfo {
})
}
}
impl Database {
/// Create an Authifier reference
pub async fn to_authifier(self) -> Authifier {
let config = config().await;
let mut auth_config = authifier::Config {
email_verification: if !config.api.smtp.host.is_empty() {
EmailVerificationConfig::Enabled {
smtp: SMTPSettings {
from: config.api.smtp.from_address,
host: config.api.smtp.host,
username: config.api.smtp.username,
password: config.api.smtp.password,
reply_to: Some(
config
.api
.smtp
.reply_to
.unwrap_or("support@revolt.chat".into()),
),
port: config.api.smtp.port,
use_tls: config.api.smtp.use_tls,
},
expiry: Default::default(),
templates: Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("../../templates/verify.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: Some(include_str!("../../templates/verify.html").into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("../../templates/reset.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: Some(include_str!("../../templates/reset.html").into()),
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("../../templates/deletion.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: Some(include_str!("../../templates/deletion.html").into()),
},
welcome: None,
},
}
} else {
EmailVerificationConfig::Disabled
},
..Default::default()
};
auth_config.invite_only = config.api.registration.invite_only;
if !config.api.security.captcha.hcaptcha_key.is_empty() {
auth_config.captcha = Captcha::HCaptcha {
secret: config.api.security.captcha.hcaptcha_key,
};
}
if !config.api.security.authifier_shield_key.is_empty() {
auth_config.shield = Shield::Enabled {
api_key: config.api.security.authifier_shield_key,
strict: false,
};
}
if config.api.security.trust_cloudflare {
auth_config.resolve_ip = ResolveIp::Cloudflare;
}
Authifier {
database: match self {
Database::Reference(_) => Default::default(),
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
},
config: auth_config,
event_channel: Some(crate::tasks::authifier_relay::sender()),
}
}
}
@@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::Arc};
use futures::lock::Mutex;
use crate::{
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, Invite, Member,
Bot, Channel, ChannelCompositeKey, ChannelUnread, Emoji, File, FileHash, Invite, Member,
MemberCompositeKey, Message, RatelimitEvent, Report, Server, ServerBan, Snapshot, User,
UserSettings, Webhook,
};
@@ -18,6 +18,7 @@ database_derived!(
pub channel_unreads: Arc<Mutex<HashMap<ChannelCompositeKey, ChannelUnread>>>,
pub channel_webhooks: Arc<Mutex<HashMap<String, Webhook>>>,
pub emojis: Arc<Mutex<HashMap<String, Emoji>>>,
pub file_hashes: Arc<Mutex<HashMap<String, FileHash>>>,
pub files: Arc<Mutex<HashMap<String, File>>>,
pub messages: Arc<Mutex<HashMap<String, Message>>>,
pub ratelimit_events: Arc<Mutex<HashMap<String, RatelimitEvent>>>,
@@ -0,0 +1,5 @@
mod model;
mod ops;
pub use model::*;
pub use ops::*;
@@ -0,0 +1,53 @@
use iso8601_timestamp::Timestamp;
auto_derived_partial!(
/// File hash
pub struct FileHash {
/// Sha256 hash of the file
#[serde(rename = "_id")]
pub id: String,
/// Sha256 hash of file after it has been processed
pub processed_hash: String,
/// When this file was created in system
pub created_at: Timestamp,
/// The bucket this file is stored in
pub bucket_id: String,
/// The path at which this file exists in
pub path: String,
/// Cryptographic nonce used to encrypt this file
pub iv: String,
/// Parsed metadata of this file
pub metadata: Metadata,
/// Raw content type of this file
pub content_type: String,
/// Size of this file (in bytes)
pub size: isize,
},
"PartialFile"
);
auto_derived!(
/// Metadata associated with a file
#[serde(tag = "type")]
#[derive(Default)]
pub enum Metadata {
/// File is just a generic uncategorised file
#[default]
File,
/// File contains textual data and should be displayed as such
Text,
/// File is an image with specific dimensions
Image {
width: isize,
height: isize,
// animated: bool // TODO: https://docs.rs/image/latest/image/trait.AnimationDecoder.html
},
/// File is a video with specific dimensions
Video { width: isize, height: isize },
/// File is audio
Audio,
}
);
@@ -0,0 +1,12 @@
use revolt_result::Result;
use crate::FileHash;
mod mongodb;
mod reference;
#[async_trait]
pub trait AbstractAttachmentHashes: Sync + Send {
/// Fetch an attachment hash entry by sha256 hash.
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash>;
}
@@ -0,0 +1,24 @@
use revolt_result::Result;
use crate::FileHash;
use crate::MongoDb;
use super::AbstractAttachmentHashes;
static COL: &str = "attachment_hashes";
#[async_trait]
impl AbstractAttachmentHashes for MongoDb {
/// Fetch an attachment hash entry by sha256 hash.
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash> {
query!(
self,
find_one,
COL,
doc! {
"_id": hash
}
)?
.ok_or_else(|| create_error!(NotFound))
}
}
@@ -0,0 +1,23 @@
use revolt_result::Result;
use crate::FileHash;
use crate::ReferenceDb;
use super::AbstractAttachmentHashes;
#[async_trait]
impl AbstractAttachmentHashes for ReferenceDb {
/// Fetch an attachment hash entry by sha256 hash.
async fn fetch_attachment_hash(&self, hash: &str) -> Result<FileHash> {
let hashes = self.file_hashes.lock().await;
if let Some(file) = hashes.get(hash) {
if file.id == hash {
Ok(file.clone())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
}
+45 -22
View File
@@ -1,5 +1,6 @@
use crate::Database;
use crate::{Database, FileHash, Metadata};
use iso8601_timestamp::Timestamp;
use revolt_result::Result;
auto_derived_partial!(
@@ -12,12 +13,16 @@ auto_derived_partial!(
pub tag: String,
/// Original filename
pub filename: String,
/// Parsed metadata of this file
pub metadata: Metadata,
/// Raw content type of this file
pub content_type: String,
/// Size of this file (in bytes)
pub size: isize,
/// Hash of this file
pub hash: Option<String>, // these are Option<>s to not break file uploads on legacy Autumn
/// When this file was uploaded
pub uploaded_at: Option<Timestamp>, // these are Option<>s to not break file uploads on legacy Autumn
/// ID of user who uploaded this file
pub uploaded_id: Option<String>, // these are Option<>s to not break file uploads on legacy Autumn
/// What the file was used for
pub used_for: Option<FileUsedFor>,
/// Whether this file was deleted
#[serde(skip_serializing_if = "Option::is_none")]
@@ -26,6 +31,14 @@ auto_derived_partial!(
#[serde(skip_serializing_if = "Option::is_none")]
pub reported: Option<bool>,
// !!! DEPRECATED:
/// Parsed metadata of this file
pub metadata: Metadata,
/// Raw content type of this file
pub content_type: String,
/// Size of this file (in bytes)
pub size: isize,
// TODO: migrate this mess to having:
// - author_id
// - parent: Parent { Message(id), User(id), etc }
@@ -44,25 +57,35 @@ auto_derived_partial!(
);
auto_derived!(
/// Metadata associated with a file
#[serde(tag = "type")]
#[derive(Default)]
pub enum Metadata {
/// File is just a generic uncategorised file
#[default]
File,
/// File contains textual data and should be displayed as such
Text,
/// File is an image with specific dimensions
Image { width: isize, height: isize },
/// File is a video with specific dimensions
Video { width: isize, height: isize },
/// File is audio
Audio,
/// Type of object file was used for
pub enum FileUsedForType {
// TODO: changing this requires a db migration
message,
serverBanner,
emoji,
userAvatar,
userProfileBackground,
legacyGroupIcon,
channelIcon,
serverIcon,
}
/// Information about what the file was used for
pub struct FileUsedFor {
/// Type of the object
#[serde(rename = "type")]
pub object_type: FileUsedForType,
/// ID of the object
pub id: String,
}
);
impl File {
/// Get the hash entry for this file
pub async fn as_hash(&self, db: &Database) -> Result<FileHash> {
db.fetch_attachment_hash(self.hash.as_ref().unwrap()).await
}
/// Use a file for a message attachment
pub async fn use_attachment(db: &Database, id: &str, parent: &str) -> Result<File> {
db.find_and_use_attachment(id, "attachments", "message", parent)
@@ -10,6 +10,9 @@ pub trait AbstractAttachments: Sync + Send {
/// Insert attachment into database.
async fn insert_attachment(&self, attachment: &File) -> Result<()>;
/// Fetch an attachment by its id.
async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result<File>;
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
@@ -15,6 +15,20 @@ impl AbstractAttachments for MongoDb {
query!(self, insert_one, COL, &attachment).map(|_| ())
}
/// Fetch an attachment by its id.
async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result<File> {
query!(
self,
find_one,
COL,
doc! {
"_id": file_id,
"tag": tag
}
)?
.ok_or_else(|| create_error!(NotFound))
}
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
@@ -18,6 +18,20 @@ impl AbstractAttachments for ReferenceDb {
}
}
/// Fetch an attachment by its id.
async fn fetch_attachment(&self, tag: &str, file_id: &str) -> Result<File> {
let files = self.files.lock().await;
if let Some(file) = files.get(file_id) {
if file.tag == tag {
Ok(file.clone())
} else {
Err(create_error!(NotFound))
}
} else {
Err(create_error!(NotFound))
}
}
/// Find an attachment by its details and mark it as used by a given parent.
async fn find_and_use_attachment(
&self,
+3
View File
@@ -5,6 +5,7 @@ mod channel_unreads;
mod channel_webhooks;
mod channels;
mod emojis;
mod file_hashes;
mod files;
mod messages;
mod ratelimit_events;
@@ -23,6 +24,7 @@ pub use channel_unreads::*;
pub use channel_webhooks::*;
pub use channels::*;
pub use emojis::*;
pub use file_hashes::*;
pub use files::*;
pub use messages::*;
pub use ratelimit_events::*;
@@ -46,6 +48,7 @@ pub trait AbstractDatabase:
+ channel_unreads::AbstractChannelUnreads
+ channel_webhooks::AbstractWebhooks
+ emojis::AbstractEmojis
+ file_hashes::AbstractAttachmentHashes
+ files::AbstractAttachments
+ messages::AbstractMessages
+ ratelimit_events::AbstractRatelimitEvents
+107 -5
View File
@@ -2,12 +2,15 @@ use std::{collections::HashSet, str::FromStr, time::Duration};
use crate::{events::client::EventV1, Database, File, RatelimitEvent};
use authifier::config::{EmailVerificationConfig, Template};
use iso8601_timestamp::Timestamp;
use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use revolt_config::{config, FeaturesLimits};
use revolt_models::v0;
use revolt_models::v0::{self, UserFlags};
use revolt_presence::filter_online;
use revolt_result::{create_error, Result};
use serde_json::json;
use ulid::Ulid;
auto_derived_partial!(
@@ -49,6 +52,10 @@ auto_derived_partial!(
/// Bot information
#[serde(skip_serializing_if = "Option::is_none")]
pub bot: Option<BotInformation>,
/// Time until user is unsuspended
#[serde(skip_serializing_if = "Option::is_none")]
pub suspended_until: Option<Timestamp>,
},
"PartialUser"
);
@@ -62,6 +69,10 @@ auto_derived!(
ProfileContent,
ProfileBackground,
DisplayName,
// internal fields
Suspension,
None,
}
/// User's relationship with another user (or themselves)
@@ -165,6 +176,7 @@ impl Default for User {
flags: Default::default(),
privileged: Default::default(),
bot: Default::default(),
suspended_until: Default::default(),
}
}
}
@@ -659,17 +671,106 @@ impl User {
}
}
FieldsUser::DisplayName => self.display_name = None,
FieldsUser::Suspension => self.suspended_until = None,
FieldsUser::None => {}
}
}
/// Suspend the user
pub async fn suspend(&mut self, db: &Database) -> Result<()> {
// Remove sessions (logout all)
// Mark user as suspended
// Disable account
///
/// - If a duration is specified, the user will be automatically unsuspended after the given time.
/// - If a reason is specified, an email will be sent.
pub async fn suspend(
&mut self,
db: &Database,
duration_days: Option<usize>,
reason: Option<Vec<String>>,
) -> Result<()> {
let authifier = db.clone().to_authifier().await;
let mut account = authifier
.database
.find_account(&self.id)
.await
.map_err(|_| create_error!(InternalError))?;
account
.disable(&authifier)
.await
.map_err(|_| create_error!(InternalError))?;
account
.delete_all_sessions(&authifier, None)
.await
.map_err(|_| create_error!(InternalError))?;
self.update(
db,
PartialUser {
flags: Some(UserFlags::SuspendedUntil as i32),
suspended_until: duration_days.and_then(|dur| {
Timestamp::now_utc().checked_add(iso8601_timestamp::Duration::days(dur as i64))
}),
..Default::default()
},
vec![],
)
.await?;
if let Some(reason) = reason {
if let EmailVerificationConfig::Enabled { smtp, .. } =
authifier.config.email_verification
{
smtp.send_email(
account.email.clone(),
// maybe move this to common area?
&Template {
title: "Account Suspension".to_string(),
html: Some(include_str!("../../../templates/suspension.html").to_owned()),
text: include_str!("../../../templates/suspension.txt").to_owned(),
url: Default::default(),
},
json!({
"email": account.email,
"list": reason.join(", "),
"duration": duration_days,
"duration_display": if duration_days.is_some() {
"block"
} else {
"none"
}
}),
)
.map_err(|_| create_error!(InternalError))?;
}
}
Ok(())
}
/// Unsuspend the user
pub async fn unsuspend(&mut self, db: &Database) -> Result<()> {
self.update(
db,
PartialUser {
flags: Some(0),
suspended_until: None,
..Default::default()
},
vec![],
)
.await?;
unimplemented!()
}
/// Permanently ban the user
///
/// - If a reason is specified, an email will be sent.
pub async fn ban(&mut self, _db: &Database, _reason: Option<String>) -> Result<()> {
// Send ban email (if reason provided)
unimplemented!()
}
/// Mark as deleted
pub async fn mark_deleted(&mut self, db: &Database) -> Result<()> {
self.update(
@@ -685,6 +786,7 @@ impl User {
FieldsUser::StatusPresence,
FieldsUser::ProfileContent,
FieldsUser::ProfileBackground,
FieldsUser::Suspension,
],
)
.await
@@ -346,6 +346,8 @@ impl IntoDocumentPath for FieldsUser {
FieldsUser::StatusPresence => "status.presence",
FieldsUser::StatusText => "status.text",
FieldsUser::DisplayName => "display_name",
FieldsUser::Suspension => "suspended_until",
FieldsUser::None => "none",
})
}
}
@@ -0,0 +1,29 @@
use async_std::channel::{unbounded, Receiver, Sender};
use authifier::AuthifierEvent;
use once_cell::sync::Lazy;
use crate::events::client::EventV1;
static Q: Lazy<(Sender<AuthifierEvent>, Receiver<AuthifierEvent>)> = Lazy::new(|| unbounded());
/// Get sender
pub fn sender() -> Sender<AuthifierEvent> {
Q.0.clone()
}
/// Start a new worker
pub async fn worker() {
loop {
let event = Q.1.recv().await.unwrap();
match &event {
AuthifierEvent::CreateSession { .. } | AuthifierEvent::CreateAccount { .. } => {
EventV1::Auth(event).global().await
}
AuthifierEvent::DeleteSession { user_id, .. }
| AuthifierEvent::DeleteAllSessions { user_id, .. } => {
let id = user_id.to_string();
EventV1::Auth(event).private(id).await
}
}
}
}
+4 -2
View File
@@ -9,19 +9,21 @@ const WORKER_COUNT: usize = 5;
pub mod ack;
pub mod apple_notifications;
pub mod authifier_relay;
pub mod last_message_id;
pub mod process_embeds;
pub mod web_push;
/// Spawn background workers
pub async fn start_workers(db: Database, authifier_db: authifier::Database) {
pub fn start_workers(db: Database, authifier_db: authifier::Database) {
task::spawn(authifier_relay::worker());
task::spawn(apple_notifications::worker(db.clone()));
for _ in 0..WORKER_COUNT {
task::spawn(ack::worker(db.clone(), authifier_db.clone()));
task::spawn(last_message_id::worker(db.clone()));
task::spawn(process_embeds::worker(db.clone()));
task::spawn(web_push::worker(authifier_db.clone()));
task::spawn(web_push::worker(db.clone(), authifier_db.clone()));
}
}
+25 -32
View File
@@ -3,7 +3,7 @@ use std::{
time::Duration,
};
use authifier::Database;
use authifier::Database as AuthifierDatabase;
use base64::{
engine::{self},
Engine as _,
@@ -20,6 +20,8 @@ use web_push::{
WebPushClient, WebPushMessageBuilder,
};
use crate::Database;
use super::apple_notifications;
/// Task information
@@ -54,7 +56,7 @@ pub async fn queue(recipients: Vec<String>, payload: PushNotification) {
}
/// Start a new worker
pub async fn worker(db: Database) {
pub async fn worker(db: Database, authifier_db: AuthifierDatabase) {
let config = config().await;
let web_push_client = IsahcWebPushClient::new().unwrap();
@@ -89,48 +91,39 @@ pub async fn worker(db: Database) {
loop {
let task = Q.pop().await;
if let Ok(sessions) = db.find_sessions_with_subscription(&task.recipients).await {
if let Ok(sessions) = authifier_db
.find_sessions_with_subscription(&task.recipients)
.await
{
for session in sessions {
if let Some(sub) = session.subscription {
if sub.endpoint == "fcm" {
// Use Firebase Cloud Messaging
if let Some(client) = &fcm_client {
let PushNotification {
author,
icon,
image,
body,
tag: _,
timestamp,
url: _,
message,
} = &task.payload;
let message = fcm_v1::message::Message {
token: Some(sub.auth),
data: Some(HashMap::from([
(
"author".to_owned(),
serde_json::Value::String(author.clone()),
),
("icon".to_owned(), serde_json::Value::String(icon.clone())),
(
"image".to_owned(),
if let Some(image) = image {
serde_json::Value::String(image.clone())
} else {
serde_json::Value::Null
},
),
("body".to_owned(), serde_json::Value::String(body.clone())),
("timestamp".to_owned(), json!(timestamp)),
("message".to_owned(), json!(&message)),
])),
data: Some(HashMap::from([(
"payload".to_owned(),
serde_json::Value::String(json!(&task.payload).to_string()),
)])),
..Default::default()
};
if let Err(err) = client.send(&message).await {
error!("Failed to send FCM notification! {:?}", err);
if let fcm_v1::Error::FCM(fcm_error) = err {
if fcm_error.contains("404 (Not Found)") {
println!("Unregistering {:?}", session.id);
if let Err(err) = db
.remove_push_subscription_by_session_id(&session.id)
.await
{
revolt_config::capture_error(&err);
}
}
}
} else {
info!("Sent FCM notification to {:?}.", session.id);
}
+12 -2
View File
@@ -1,7 +1,7 @@
use revolt_models::v0::*;
use revolt_permissions::{calculate_user_permissions, UserPermission};
use crate::{util::permissions::DatabasePermissionQuery, Database};
use crate::{util::permissions::DatabasePermissionQuery, Database, FileUsedFor};
impl crate::Bot {
pub fn into_public_bot(self, user: crate::User) -> PublicBot {
@@ -419,6 +419,10 @@ impl From<File> for crate::File {
user_id: value.user_id,
server_id: value.server_id,
object_id: value.object_id,
hash: None,
uploaded_at: None,
uploaded_id: None,
used_for: None,
}
}
}
@@ -1171,6 +1175,7 @@ impl From<User> for crate::User {
flags: Some(value.flags as i32),
privileged: value.privileged,
bot: value.bot.map(Into::into),
suspended_until: None,
}
}
}
@@ -1209,6 +1214,8 @@ impl From<FieldsUser> for crate::FieldsUser {
FieldsUser::StatusPresence => crate::FieldsUser::StatusPresence,
FieldsUser::StatusText => crate::FieldsUser::StatusText,
FieldsUser::DisplayName => crate::FieldsUser::DisplayName,
FieldsUser::Internal => crate::FieldsUser::None,
}
}
}
@@ -1222,6 +1229,9 @@ impl From<crate::FieldsUser> for FieldsUser {
crate::FieldsUser::StatusPresence => FieldsUser::StatusPresence,
crate::FieldsUser::StatusText => FieldsUser::StatusText,
crate::FieldsUser::DisplayName => FieldsUser::DisplayName,
crate::FieldsUser::Suspension => FieldsUser::Internal,
crate::FieldsUser::None => FieldsUser::Internal,
}
}
}
@@ -1338,4 +1348,4 @@ impl From<FieldsMessage> for crate::FieldsMessage {
FieldsMessage::Pinned => crate::FieldsMessage::Pinned,
}
}
}
}
@@ -0,0 +1,554 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 -->
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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">
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>
</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"
>
<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>
<tr>
<td
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>
<tr>
<td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<tbody>
<tr>
<td
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>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160;
</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"
>
<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>
</tr>
</tbody>
</table>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160;
</td>
</tr>
</tbody>
</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"
>
<tbody>
<tr>
<td
style="
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>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</table>
<p
class=""
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>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</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;
"
>
<tbody>
<tr>
<td
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>
</tr>
</tbody>
</table>
</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;
</td>
</tr>
</tbody>
</table>
<div
class="text-muted text-center"
style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
<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;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
@@ -23,7 +23,7 @@
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in the EU
Made in Europe
</div>
</div>
</body>
@@ -4,4 +4,4 @@ Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Revolt
Made in the EU
Made in Europe
+552
View File
@@ -0,0 +1,552 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 -->
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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">
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>
</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"
>
<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>
<tr>
<td
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>
<tr>
<td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<tbody>
<tr>
<td
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>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160;
</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"
>
<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>
</tr>
</tbody>
</table>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160;
</td>
</tr>
</tbody>
</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"
>
<tbody>
<tr>
<td
style="
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>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</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>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</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;
"
>
<tbody>
<tr>
<td
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>
</tr>
</tbody>
</table>
</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;
</td>
</tr>
</tbody>
</table>
<div
class="text-muted text-center"
style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
<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;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
@@ -19,7 +19,7 @@
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in the EU
Made in Europe
</div>
</div>
</body>
@@ -4,4 +4,4 @@ Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Revolt
Made in the EU
Made in Europe
@@ -0,0 +1,625 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.5.1 -->
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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">
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>
</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"
>
<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>
<tr>
<td
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>
<tr>
<td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<tbody>
<tr>
<td
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>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160;
</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"
>
<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>
</tr>
</tbody>
</table>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160;
</td>
</tr>
</tbody>
</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"
>
<tbody>
<tr>
<td
style="
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>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</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>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</table>
<ul class="">
{{list}}
</ul>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</table>
<p
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>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</table>
<p
class=""
style="
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>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</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>
</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;
</td>
</tr>
</tbody>
</table>
<div
class="text-muted text-center"
style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
<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;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
@@ -0,0 +1,38 @@
<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">Account Suspended</h1>
<p>Your account has been suspended, for one or more reasons:</p>
<ul>
{{list}}
</ul>
<p style="display:{{duration_display}}">
You will be able to use your account again in {{duration}} days.
</p>
<p>
Further violations may result in a permanent ban depending on
severity, please abide by the
<a href="https://revolt.chat/aup">Acceptable Usage Policy</a>.
</p>
<p>Ban evasion is prohibited and will be dealt with accordingly.</p>
</div>
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
</div>
</body>
</html>
@@ -0,0 +1,12 @@
Your account has been suspended, for one or more reasons:
{{list}}
You will be able to use your account again in {{duration}} days.
Further violations may result in a permanent ban depending on severity, please abide by the Acceptable Usage Policy (https://revolt.chat/aup).
Ban evasion is prohibited and will be dealt with accordingly.
This email is intended for {{email}}
Sent by Revolt
Made in Europe
+552
View File
@@ -0,0 +1,552 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 -->
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="x-apple-disable-message-reformatting" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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">
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>
</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"
>
<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>
<tr>
<td
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>
<tr>
<td
align="center"
style="
line-height: 24px;
font-size: 16px;
margin: 0;
padding: 0 16px;
"
>
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table
align="center"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%; max-width: 600px; margin: 0 auto"
>
<tbody>
<tr>
<td
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>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160;
</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"
>
<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>
</tr>
</tbody>
</table>
<table
class="s-10 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 40px;
font-size: 40px;
width: 100%;
height: 40px;
margin: 0;
"
align="left"
width="100%"
height="40"
>
&#160;
</td>
</tr>
</tbody>
</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"
>
<tbody>
<tr>
<td
style="
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>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</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>
<table
class="s-4 w-full"
role="presentation"
border="0"
cellpadding="0"
cellspacing="0"
style="width: 100%"
width="100%"
>
<tbody>
<tr>
<td
style="
line-height: 16px;
font-size: 16px;
width: 100%;
height: 16px;
margin: 0;
"
align="left"
width="100%"
height="16"
>
&#160;
</td>
</tr>
</tbody>
</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;
"
>
<tbody>
<tr>
<td
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>
</tr>
</tbody>
</table>
</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;
</td>
</tr>
</tbody>
</table>
<div
class="text-muted text-center"
style="color: #718096"
align="center"
>
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in Europe
</div>
<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;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
@@ -20,7 +20,7 @@
<div class="text-muted text-center my-6">
This email is intended for {{email}}<br />
Sent from Revolt<br />
Made in the EU
Made in Europe
</div>
</div>
</body>
@@ -5,4 +5,4 @@ Please navigate to: {{url}}
This email is intended for {{email}}
Sent by Revolt
Made in the EU
Made in Europe
+5
View File
@@ -7,7 +7,12 @@ authors = ["Paul Makles <me@insrt.uk>"]
description = "Revolt Backend: S3 and encryption subroutines"
[dependencies]
base64 = "0.22.1"
aes-gcm = "0.10.3"
typenum = "1.17.0"
aws-config = "1.5.5"
aws-sdk-s3 = { version = "1.46.0", features = ["behavior-version-latest"] }
revolt-config = { version = "0.7.16", path = "../config" }
revolt-result = { version = "0.7.16", path = "../result" }
+79 -2
View File
@@ -1,3 +1,80 @@
// pub fn
use std::io::Write;
pub fn decrypt(data: &mut [u8]) {}
use aes_gcm::{aead::AeadMutInPlace, Aes256Gcm, Key, KeyInit, Nonce};
use revolt_config::{config, FilesS3};
use revolt_result::{create_error, Result};
use aws_sdk_s3::{
config::{Credentials, Region},
Client, Config,
};
use base64::prelude::*;
pub static AUTHENTICATION_TAG_SIZE_BYTES: usize = 16;
/// Create an S3 client
pub fn create_client(s3_config: FilesS3) -> Client {
let provider_name = "my-creds";
let creds = Credentials::new(
s3_config.access_key_id,
s3_config.secret_access_key,
None,
None,
provider_name,
);
let config = Config::builder()
.region(Region::new(s3_config.region))
.endpoint_url(s3_config.endpoint)
.credentials_provider(creds)
.build();
Client::from_conf(config)
}
/// Create an AES-256-GCM cipher
pub fn create_cipher(key: &str) -> Aes256Gcm {
let key = &BASE64_STANDARD.decode(key).unwrap()[..];
let key: &Key<Aes256Gcm> = key.into();
Aes256Gcm::new(key)
}
/// Fetch a file from S3 (and decrypt it)
pub async fn fetch_from_s3(bucket_id: &str, path: &str, nonce: &str) -> Result<Vec<u8>> {
let config = config().await;
let client = create_client(config.files.s3);
// Send a request for the file
let mut obj = client
.get_object()
.bucket(bucket_id)
.key(path)
.send()
.await
.inspect_err(|err| {
revolt_config::capture_error(err);
})
.map_err(|_| create_error!(InternalError))?;
// Read the file from remote
let mut buf = vec![];
while let Some(bytes) = obj.body.next().await {
let data = bytes.map_err(|_| create_error!(InternalError))?;
buf.write_all(&data)
.map_err(|_| create_error!(InternalError))?;
// is there a more efficient way to do this?
// we just want the Vec<u8>
}
// Recover nonce as bytes
let nonce = &BASE64_STANDARD.decode(nonce).unwrap()[..];
let nonce: &Nonce<typenum::consts::U12> = nonce.into();
// Decrypt the file
create_cipher(&config.files.encryption_key)
.decrypt_in_place(nonce, b"", &mut buf)
.map_err(|_| create_error!(InternalError))?;
Ok(buf)
}
+3 -1
View File
@@ -11,9 +11,10 @@ description = "Revolt Backend: API Models"
[features]
serde = ["dep:serde", "revolt-permissions/serde", "indexmap/serde"]
schemas = ["dep:schemars", "revolt-permissions/schemas"]
utoipa = ["dep:utoipa"]
validator = ["dep:validator"]
rocket = ["dep:rocket"]
partials = ["dep:revolt_optional_struct", "serde", "schemas"]
partials = ["dep:revolt_optional_struct", "serde", "schemas", "utoipa"]
default = ["serde", "partials", "rocket"]
@@ -37,6 +38,7 @@ iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] }
# Spec Generation
schemars = { version = "0.8.8", optional = true, features = ["indexmap1"] }
utoipa = { version = "4.2.3", optional = true }
# Validation
validator = { version = "0.16.0", optional = true, features = ["derive"] }
+5
View File
@@ -6,6 +6,10 @@ extern crate serde;
#[macro_use]
extern crate schemars;
#[cfg(feature = "utoipa")]
#[macro_use]
extern crate utoipa;
#[cfg(feature = "partials")]
#[macro_use]
extern crate revolt_optional_struct;
@@ -18,6 +22,7 @@ macro_rules! auto_derived {
$(
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[derive(Debug, Clone, Eq, PartialEq)]
$item
)+
+4 -1
View File
@@ -88,6 +88,9 @@ auto_derived!(
ProfileContent,
ProfileBackground,
DisplayName,
/// Internal field, ignore this.
Internal,
}
/// User's relationship with another user (or themselves)
@@ -190,7 +193,7 @@ auto_derived!(
#[repr(u32)]
pub enum UserFlags {
/// User has been suspended from the platform
Suspended = 1,
SuspendedUntil = 1,
/// User has deleted their account
Deleted = 2,
/// User was banned off the platform
+6
View File
@@ -11,7 +11,9 @@ description = "Revolt Backend: Result and Error types"
[features]
serde = ["dep:serde"]
schemas = ["dep:schemars"]
utoipa = ["dep:utoipa"]
rocket = ["dep:rocket", "dep:serde_json"]
axum = ["dep:axum", "dep:serde_json"]
okapi = ["dep:revolt_rocket_okapi", "dep:revolt_okapi", "schemas"]
default = ["serde"]
@@ -23,8 +25,12 @@ serde = { version = "1", features = ["derive"], optional = true }
# Spec Generation
schemars = { version = "0.8.8", optional = true }
utoipa = { version = "4.2.3", optional = true }
# Rocket
rocket = { optional = true, version = "0.5.0-rc.2", default-features = false }
revolt_rocket_okapi = { version = "0.9.1", optional = true }
revolt_okapi = { version = "0.9.1", optional = true }
# Axum
axum = { version = "0.7.5", optional = true }
+81
View File
@@ -0,0 +1,81 @@
use axum::{http::StatusCode, response::IntoResponse, Json};
use crate::{Error, ErrorType};
/// HTTP response builder for Error enum
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
let status = match self.error_type {
ErrorType::LabelMe => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::AlreadyOnboarded => StatusCode::FORBIDDEN,
ErrorType::UnknownUser => StatusCode::NOT_FOUND,
ErrorType::InvalidUsername => StatusCode::BAD_REQUEST,
ErrorType::UsernameTaken => StatusCode::CONFLICT,
ErrorType::DiscriminatorChangeRatelimited => StatusCode::TOO_MANY_REQUESTS,
ErrorType::AlreadyFriends => StatusCode::CONFLICT,
ErrorType::AlreadySentRequest => StatusCode::CONFLICT,
ErrorType::Blocked => StatusCode::CONFLICT,
ErrorType::BlockedByOther => StatusCode::FORBIDDEN,
ErrorType::NotFriends => StatusCode::FORBIDDEN,
ErrorType::TooManyPendingFriendRequests { .. } => StatusCode::BAD_REQUEST,
ErrorType::UnknownChannel => StatusCode::NOT_FOUND,
ErrorType::UnknownMessage => StatusCode::NOT_FOUND,
ErrorType::UnknownAttachment => StatusCode::BAD_REQUEST,
ErrorType::CannotEditMessage => StatusCode::FORBIDDEN,
ErrorType::CannotJoinCall => StatusCode::BAD_REQUEST,
ErrorType::TooManyAttachments { .. } => StatusCode::BAD_REQUEST,
ErrorType::TooManyReplies { .. } => StatusCode::BAD_REQUEST,
ErrorType::EmptyMessage => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::PayloadTooLarge => StatusCode::UNPROCESSABLE_ENTITY,
ErrorType::CannotRemoveYourself => StatusCode::BAD_REQUEST,
ErrorType::GroupTooLarge { .. } => StatusCode::FORBIDDEN,
ErrorType::AlreadyInGroup => StatusCode::CONFLICT,
ErrorType::NotInGroup => StatusCode::NOT_FOUND,
ErrorType::AlreadyPinned => StatusCode::BAD_REQUEST,
ErrorType::NotPinned => StatusCode::BAD_REQUEST,
ErrorType::UnknownServer => StatusCode::NOT_FOUND,
ErrorType::InvalidRole => StatusCode::NOT_FOUND,
ErrorType::Banned => StatusCode::FORBIDDEN,
ErrorType::AlreadyInServer => StatusCode::CONFLICT,
ErrorType::TooManyServers { .. } => StatusCode::BAD_REQUEST,
ErrorType::TooManyEmbeds { .. } => StatusCode::BAD_REQUEST,
ErrorType::TooManyEmoji { .. } => StatusCode::BAD_REQUEST,
ErrorType::TooManyChannels { .. } => StatusCode::BAD_REQUEST,
ErrorType::TooManyRoles { .. } => StatusCode::BAD_REQUEST,
ErrorType::ReachedMaximumBots => StatusCode::BAD_REQUEST,
ErrorType::IsBot => StatusCode::BAD_REQUEST,
ErrorType::BotIsPrivate => StatusCode::FORBIDDEN,
ErrorType::CannotReportYourself => StatusCode::BAD_REQUEST,
ErrorType::MissingPermission { .. } => StatusCode::FORBIDDEN,
ErrorType::MissingUserPermission { .. } => StatusCode::FORBIDDEN,
ErrorType::NotElevated => StatusCode::FORBIDDEN,
ErrorType::NotPrivileged => StatusCode::FORBIDDEN,
ErrorType::CannotGiveMissingPermissions => StatusCode::FORBIDDEN,
ErrorType::NotOwner => StatusCode::FORBIDDEN,
ErrorType::DatabaseError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
ErrorType::InvalidOperation => StatusCode::BAD_REQUEST,
ErrorType::InvalidCredentials => StatusCode::UNAUTHORIZED,
ErrorType::InvalidProperty => StatusCode::BAD_REQUEST,
ErrorType::InvalidSession => StatusCode::UNAUTHORIZED,
ErrorType::DuplicateNonce => StatusCode::CONFLICT,
ErrorType::VosoUnavailable => StatusCode::BAD_REQUEST,
ErrorType::NotFound => StatusCode::NOT_FOUND,
ErrorType::NoEffect => StatusCode::OK,
ErrorType::FailedValidation { .. } => StatusCode::BAD_REQUEST,
ErrorType::ProxyError => StatusCode::BAD_REQUEST,
};
(status, Json(&self)).into_response()
}
}
+12
View File
@@ -8,9 +8,16 @@ extern crate serde;
#[macro_use]
extern crate schemars;
#[cfg(feature = "utoipa")]
#[macro_use]
extern crate utoipa;
#[cfg(feature = "rocket")]
pub mod rocket;
#[cfg(feature = "axum")]
pub mod axum;
#[cfg(feature = "okapi")]
pub mod okapi;
@@ -20,6 +27,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Error information
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[derive(Debug, Clone)]
pub struct Error {
/// Type of error and additional information
@@ -42,6 +50,7 @@ impl std::error::Error for Error {}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "type"))]
#[cfg_attr(feature = "schemas", derive(JsonSchema))]
#[cfg_attr(feature = "utoipa", derive(ToSchema))]
#[derive(Debug, Clone)]
pub enum ErrorType {
/// This error was not labeled :(
@@ -145,6 +154,9 @@ pub enum ErrorType {
error: String,
},
// ? Micro-service errors
ProxyError,
// ? Legacy errors
VosoUnavailable,
}
+2
View File
@@ -78,6 +78,8 @@ impl<'r> Responder<'r, 'static> for Error {
ErrorType::NotFound => Status::NotFound,
ErrorType::NoEffect => Status::Ok,
ErrorType::FailedValidation { .. } => Status::BadRequest,
ErrorType::ProxyError => Status::BadRequest,
};
// Serialize the error data structure into JSON.
+4 -92
View File
@@ -10,7 +10,6 @@ pub mod util;
use revolt_config::config;
use revolt_database::events::client::EventV1;
use revolt_database::{Database, MongoDb};
use rocket::{Build, Rocket};
use rocket_cors::{AllowedOrigins, CorsOptions};
use rocket_prometheus::PrometheusMetrics;
@@ -18,11 +17,7 @@ use std::net::Ipv4Addr;
use std::str::FromStr;
use async_std::channel::unbounded;
use authifier::config::{
Captcha, Config as AuthifierConfig, EmailVerificationConfig, ResolveIp, SMTPSettings, Shield,
Template, Templates,
};
use authifier::{Authifier, AuthifierEvent};
use authifier::AuthifierEvent;
use rocket::data::ToByteUnit;
pub async fn web() -> Rocket<Build> {
@@ -40,16 +35,7 @@ pub async fn web() -> Rocket<Build> {
let (sender, receiver) = unbounded();
// Setup Authifier
let authifier = Authifier {
database: match db.clone() {
Database::Reference(_) => Default::default(),
Database::MongoDb(MongoDb(client, _)) => authifier::Database::MongoDb(
authifier::database::MongoDb(client.database("revolt")),
),
},
config: authifier_config().await,
event_channel: Some(sender),
};
let authifier = db.clone().to_authifier().await;
// Launch a listener for Authifier events
async_std::task::spawn(async move {
@@ -68,10 +54,7 @@ pub async fn web() -> Rocket<Build> {
});
// Launch background task workers
async_std::task::spawn(revolt_database::tasks::start_workers(
db.clone(),
authifier.database.clone(),
));
revolt_database::tasks::start_workers(db.clone(), authifier.database.clone());
// Configure CORS
let cors = CorsOptions {
@@ -114,82 +97,11 @@ pub async fn web() -> Rocket<Build> {
.configure(rocket::Config {
limits: rocket::data::Limits::default().limit("string", 5.megabytes()),
address: Ipv4Addr::new(0, 0, 0, 0).into(),
port: 14702,
..Default::default()
})
}
pub async fn authifier_config() -> AuthifierConfig {
let config = config().await;
let mut auth_config = AuthifierConfig {
email_verification: if !config.api.smtp.host.is_empty() {
EmailVerificationConfig::Enabled {
smtp: SMTPSettings {
from: config.api.smtp.from_address,
host: config.api.smtp.host,
username: config.api.smtp.username,
password: config.api.smtp.password,
reply_to: Some(
config
.api
.smtp
.reply_to
.unwrap_or("support@revolt.chat".into()),
),
port: config.api.smtp.port,
use_tls: config.api.smtp.use_tls,
},
expiry: Default::default(),
templates: Templates {
verify: Template {
title: "Verify your Revolt account.".into(),
text: include_str!("templates/verify.txt").into(),
url: format!("{}/login/verify/", config.hosts.app),
html: Some(include_str!("templates/verify.html").into()),
},
reset: Template {
title: "Reset your Revolt password.".into(),
text: include_str!("templates/reset.txt").into(),
url: format!("{}/login/reset/", config.hosts.app),
html: Some(include_str!("templates/reset.html").into()),
},
deletion: Template {
title: "Confirm account deletion.".into(),
text: include_str!("templates/deletion.txt").into(),
url: format!("{}/delete/", config.hosts.app),
html: Some(include_str!("templates/deletion.html").into()),
},
welcome: None,
},
}
} else {
EmailVerificationConfig::Disabled
},
..Default::default()
};
auth_config.invite_only = config.api.registration.invite_only;
if !config.api.security.captcha.hcaptcha_key.is_empty() {
auth_config.captcha = Captcha::HCaptcha {
secret: config.api.security.captcha.hcaptcha_key,
};
}
if !config.api.security.authifier_shield_key.is_empty() {
auth_config.shield = Shield::Enabled {
api_key: config.api.security.authifier_shield_key,
strict: false,
};
}
if config.api.security.trust_cloudflare {
auth_config.resolve_ip = ResolveIp::Cloudflare;
}
auth_config
}
#[launch]
async fn rocket() -> _ {
// Configure logging and environment
-138
View File
@@ -1,138 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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">
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>
</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">
<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>
<tr>
<td 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>
<tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td 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>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</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">
<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>
</tr>
</tbody>
</table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</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">
<tbody>
<tr>
<td style="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>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</table>
<p class="" 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>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</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;">
<tbody>
<tr>
<td 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>
</tr>
</tbody>
</table>
</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;
</td>
</tr>
</tbody>
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in the EU
</div>
<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;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
-138
View File
@@ -1,138 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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">
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>
</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">
<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>
<tr>
<td 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>
<tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td 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>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</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">
<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>
</tr>
</tbody>
</table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</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">
<tbody>
<tr>
<td style="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>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</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>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</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;">
<tbody>
<tr>
<td 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>
</tr>
</tbody>
</table>
</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;
</td>
</tr>
</tbody>
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in the EU
</div>
<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;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
-138
View File
@@ -1,138 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<!-- Compiled with Bootstrap Email version: 1.3.1 --><meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="x-apple-disable-message-reformatting">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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">
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>
</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">
<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>
<tr>
<td 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>
<tr>
<td align="center" style="line-height: 24px; font-size: 16px; margin: 0; padding: 0 16px;">
<!--[if (gte mso 9)|(IE)]>
<table align="center" role="presentation">
<tbody>
<tr>
<td width="600">
<![endif]-->
<table align="center" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tbody>
<tr>
<td 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>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</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">
<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>
</tr>
</tbody>
</table>
<table class="s-10 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 40px; font-size: 40px; width: 100%; height: 40px; margin: 0;" align="left" width="100%" height="40">
&#160;
</td>
</tr>
</tbody>
</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">
<tbody>
<tr>
<td style="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>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</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>
<table class="s-4 w-full" role="presentation" border="0" cellpadding="0" cellspacing="0" style="width: 100%;" width="100%">
<tbody>
<tr>
<td style="line-height: 16px; font-size: 16px; width: 100%; height: 16px; margin: 0;" align="left" width="100%" height="16">
&#160;
</td>
</tr>
</tbody>
</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;">
<tbody>
<tr>
<td 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>
</tr>
</tbody>
</table>
</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;
</td>
</tr>
</tbody>
</table>
<div class="text-muted text-center" style="color: #718096;" align="center">
This email is intended for {{email}}<br>
Sent from Revolt<br>
Made in the EU
</div>
<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;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if (gte mso 9)|(IE)]>
</td>
</tr>
</tbody>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
+34 -2
View File
@@ -4,19 +4,51 @@ version = "0.7.14"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.68"
# Media processing
webp = "0.3.0"
sha2 = "0.10.8"
jxl-oxide = "0.8.1"
kamadak-exif = "0.5.4"
# revolt_little_exif = "0.4.0"
simdutf8 = { version = "0.1.4", features = ["aarch64_neon"] }
image = { version = "0.25.2" } # requires dav1d system library: features = ["avif-native"]
# Content type processing
infer = "0.16.0"
ffprobe = "0.4.0"
imagesize = "0.13.0"
# Utility
lazy_static = "1.5.0"
moka = { version = "0.12.8", features = ["future"] }
# Serialisation
strum_macros = "0.26.4"
serde_json = "1.0.68"
serde = { version = "1.0", features = ["derive"] }
# Async runtime
tokio = { version = "1.0", features = ["full"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Core crates
revolt-files = { version = "0.7.16", path = "../../core/files" }
revolt-config = { version = "0.7.16", path = "../../core/config" }
revolt-database = { version = "0.7.16", path = "../../core/database" }
revolt-result = { version = "0.7.16", path = "../../core/result", features = [
"utoipa",
"axum",
] }
# Axum / web server
tempfile = "3.12.0"
axum-macros = "0.4.1"
axum_typed_multipart = "0.12.1"
axum = { version = "0.7.5", features = ["multipart"] }
# OpenAPI & documentation generation
utoipa-scalar = { version = "0.1.0", features = ["axum"] }
utoipa = { version = "4.2.3", features = ["axum_extras", "ulid"] }
+1
View File
@@ -0,0 +1 @@
# TODO: https://github.com/wader/static-ffmpeg
+214 -23
View File
@@ -1,26 +1,64 @@
use std::{
io::{Cursor, Read},
time::Duration,
};
use axum::{
extract::{DefaultBodyLimit, Multipart, Path},
http::StatusCode,
response::{IntoResponse, Response},
extract::{DefaultBodyLimit, Path, State},
http::header,
response::{IntoResponse, Redirect, Response},
routing::{get, post},
Json, Router,
};
use axum_typed_multipart::{FieldData, TryFromMultipart, TypedMultipart};
use image::ImageReader;
use lazy_static::lazy_static;
use revolt_config::config;
use revolt_database::{Database, FileHash, Metadata};
use revolt_files::{fetch_from_s3, AUTHENTICATION_TAG_SIZE_BYTES};
use revolt_result::{create_error, Result};
use serde::{Deserialize, Serialize};
use sha2::Digest;
use tempfile::NamedTempFile;
use tokio::time::Instant;
use utoipa::ToSchema;
use tempfile::NamedTempFile;
use crate::{exif::strip_metadata, metadata::generate_metadata, mime_type::determine_mime_type};
pub async fn router() -> Router {
/// Build the API router
pub async fn router() -> Router<Database> {
let config = config().await;
Router::new().route("/", get(root)).route(
"/:tag",
post(upload_file).layer(DefaultBodyLimit::max(
config.features.limits.global.body_limit_size,
)),
)
Router::new()
.route("/", get(root))
.route(
"/:tag",
post(upload_file).layer(DefaultBodyLimit::max(
config.features.limits.global.body_limit_size,
)),
)
.route("/:tag/:file_id", get(fetch_preview))
.route("/:tag/:file_id/:file_name", get(fetch_file))
}
lazy_static! {
/// Short-lived file cache to allow us to populate different CDN regions without increasing bandwidth to S3 provider
/// Uploads will also be stored here to prevent immediately queued downloads from doing the entire round-trip
static ref S3_CACHE: moka::future::Cache<String, Result<Vec<u8>>> = moka::future::Cache::builder()
.max_capacity(10_000) // TODO config
.time_to_live(Duration::from_secs(60)) // TODO config
.build();
}
/// Retrieve hash information and file data by given hash
async fn retrieve_file_by_hash(hash: &FileHash) -> Result<Vec<u8>> {
if let Some(data) = S3_CACHE.get(&hash.id).await {
data
} else {
let data = fetch_from_s3(&hash.bucket_id, &hash.path, &hash.iv).await;
S3_CACHE.insert(hash.id.to_owned(), data.clone()).await;
data
}
}
/// Successful root response
@@ -49,7 +87,7 @@ async fn root() -> Json<RootResponse> {
}
/// Available tags to upload to
#[derive(Deserialize, Debug, ToSchema)]
#[derive(Deserialize, Debug, ToSchema, strum_macros::IntoStaticStr)]
#[allow(non_camel_case_types)]
pub enum Tag {
attachments,
@@ -101,13 +139,79 @@ pub struct UploadResponse {
)]
async fn upload_file(
Path(tag): Path<Tag>,
TypedMultipart(UploadPayload { file }): TypedMultipart<UploadPayload>,
TypedMultipart(UploadPayload { mut file }): TypedMultipart<UploadPayload>,
) -> axum::response::Result<Json<UploadResponse>> {
// Keep track of processing time
let now = Instant::now();
// TODO: authenticate the user
// Extract the filename, or give it a generic name
let file_name = file.metadata.file_name.unwrap_or("unnamed-file".to_owned());
// Load file to memory
let mut buf = Vec::<u8>::new();
file.contents
.read_to_end(&mut buf)
.map_err(|_| create_error!(InternalError))?;
// Take note of original file size
let original_file_size = buf.len();
// TODO: check against tag
// Generate sha256 hash
let original_hash = {
let mut hasher = sha2::Sha256::new();
hasher.update(&buf);
hasher.finalize()
};
// TODO: find existing `hash` (or match `processed`) and use that if possible
// then: create attachment and return UploadResponse { id }
// Determine the mime type for the file
let mime_type = determine_mime_type(&mut file.contents, &buf, &file_name);
// TODO: block mime types here
// Determine metadata for the file
let metadata = generate_metadata(&file.contents, mime_type);
// Strip metadata
let (buf, metadata) = strip_metadata(file.contents, buf, metadata, mime_type).await?;
// TODO: Virus scan Metadata::File
// Print file information for debug purposes
let new_file_size = buf.len() + AUTHENTICATION_TAG_SIZE_BYTES;
let processed_hash = {
let mut hasher = sha2::Sha256::new();
hasher.update(&buf);
hasher.finalize()
};
let process_ratio = new_file_size as f32 / original_file_size as f32;
let time_to_process = Instant::now() - now;
tracing::info!("Received file {file_name}\nOriginal hash: {original_hash:02X}\nOriginal size: {original_file_size} bytes\nMime type: {mime_type}\nMetadata: {metadata:?}\nProcessed file size: {new_file_size} bytes ({:.2}%).\nProcessed hash: {processed_hash:02X}\nProcessing took {time_to_process:?}", process_ratio * 100.0);
// TODO: encrypt the file and generate FileHash
// note: file_size is processed file's size (incl. authTag in encrypted buffer)
// TODO: insert FileHash
// TODO: upload to S3
// TODO: create attachment
Ok(Json(UploadResponse { id: "aaa" }))
}
/// Header value used for cache control
pub static CACHE_CONTROL: &str = "public, max-age=604800, must-revalidate";
/// Fetch preview of file
///
/// This route will only return image content.
/// For all other file types, please use the fetch route (you will receive a redirect if you try to use this route anyways!).
///
/// Depending on the given tag, the file will be re-processed to fit the criteria:
///
/// | Tag | Image Resolution <sup>†</sup> |
@@ -132,18 +236,78 @@ async fn upload_file(
),
)]
async fn fetch_preview(
Path(tag): Path<Tag>,
Path(file_id): Path<String>,
) -> axum::response::Result<Response> {
todo!()
State(db): State<Database>,
Path((tag, file_id)): Path<(Tag, String)>,
) -> Result<Response> {
let tag: &'static str = tag.into();
let file = db.fetch_attachment(tag, &file_id).await?;
// Ignore deleted files
if file.deleted.is_some_and(|v| v) {
return Err(create_error!(NotFound));
}
// Ignore files that haven't been attached
if file.used_for.is_none() {
return Err(create_error!(NotFound));
}
let hash = file.as_hash(&db).await?;
// Only process image files
if !matches!(hash.metadata, Metadata::Image { .. }) {
return Ok(
Redirect::permanent(&format!("/{tag}/{file_id}/{}", file.filename)).into_response(),
);
}
// Original image data
let data = retrieve_file_by_hash(&hash).await?;
// Dimensions we need to resize to
let config = config().await;
let [w, h] = config.files.preview.get(tag).unwrap();
// Read the image and resize it
// TODO: use jxl_oxide to process image/jxl files
let image = ImageReader::new(Cursor::new(data))
.with_guessed_format()
.map_err(|_| create_error!(InternalError))?
.decode()
.map_err(|_| create_error!(InternalError))?
//.resize(width as u32, height as u32, image::imageops::FilterType::Gaussian)
// resize is about 2.5x slower,
// thumbnail doesn't have terrible quality
// so we use thumbnail
.thumbnail(*w as u32, *h as u32);
// Encode it into WEBP
let encoder = webp::Encoder::from_image(&image).expect("Could not create encoder.");
let data = if config.files.webp_quality != 100.0 {
encoder.encode(config.files.webp_quality).to_vec()
} else {
encoder.encode_lossless().to_vec()
};
Ok((
[
(header::CONTENT_TYPE, "image/webp"),
(header::CONTENT_DISPOSITION, "inline"),
(header::CACHE_CONTROL, CACHE_CONTROL),
],
data,
)
.into_response())
}
/// Fetch original file
///
/// Content disposition header will be set to 'attachment' to prevent browser from rendering anything.
#[utoipa::path(
get,
path = "/{tag}/{file_id}/{file_name}",
responses(
(status = 200, description = "Generated preview", body = Vec<u8>)
(status = 200, description = "Original file", body = Vec<u8>)
),
params(
("tag" = Tag, Path, description = "Tag to fetch from (e.g. attachments, icons, ...)"),
@@ -152,9 +316,36 @@ async fn fetch_preview(
),
)]
async fn fetch_file(
Path(tag): Path<Tag>,
Path(file_id): Path<String>,
Path(file_name): Path<String>,
) -> axum::response::Result<Response> {
todo!()
State(db): State<Database>,
Path((tag, file_id, file_name)): Path<(Tag, String, String)>,
) -> Result<Response> {
let file = db.fetch_attachment(tag.into(), &file_id).await?;
// Ignore deleted files
if file.deleted.is_some_and(|v| v) {
return Err(create_error!(NotFound));
}
// Ignore files that haven't been attached
if file.used_for.is_none() {
return Err(create_error!(NotFound));
}
// Ensure filename is correct
if file_name != file.filename {
return Err(create_error!(NotFound));
}
let hash = file.as_hash(&db).await?;
retrieve_file_by_hash(&hash).await.map(|data| {
(
[
(header::CONTENT_TYPE, hash.content_type),
(header::CONTENT_DISPOSITION, "attachment".to_owned()),
(header::CACHE_CONTROL, CACHE_CONTROL.to_owned()),
],
data,
)
.into_response()
})
}
+165
View File
@@ -0,0 +1,165 @@
use std::io::{Cursor, Read};
use exif::Reader;
use image::{ImageFormat, ImageReader};
use revolt_database::Metadata;
use revolt_result::{create_error, Result};
use tempfile::NamedTempFile;
use tokio::process::Command;
/// Strip EXIF data from given file and produce new file and metadata
pub async fn strip_metadata(
file: NamedTempFile,
buf: Vec<u8>,
metadata: Metadata,
mime: &str,
) -> Result<(Vec<u8>, Metadata)> {
match &metadata {
Metadata::Image { width, height } => match mime {
// little_exif does not appear to parse JPEGs correctly? had 2/2 files fail
/* "image/jpeg" | "image/png" => {
// use little_exif to strip metadata except for orientation and colour profile
// PNGs must also be re-encoded to mitigate CVE-2023-21036
let metadata = revolt_little_exif::metadata::Metadata::new_from_path_with_filetype(
file.path(),
match mime {
"image/jpeg" => revolt_little_exif::filetype::FileExtension::JPEG,
"image/png" => revolt_little_exif::filetype::FileExtension::PNG {
as_zTXt_chunk: true,
},
_ => unreachable!(),
},
)
.unwrap();
dbg!(metadata.data());
} */
// Apply orientation manually & strip all other EXIF data
"image/jpeg" | "image/png" | "image/avif" | "image/tiff" => {
// Create a reader
let mut cursor = Cursor::new(buf);
// Decode the image
let image = ImageReader::new(&mut cursor)
.with_guessed_format()
.map_err(|_| create_error!(InternalError))?
.decode()
.map_err(|_| create_error!(InternalError));
// Reset read position
cursor.set_position(0);
// Extract orientation data
let exif_reader = Reader::new();
let rotation = match exif_reader.read_from_container(&mut cursor) {
Ok(exif) => match exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY) {
Some(orientation) => orientation.value.get_uint(0).unwrap_or_default(),
_ => 0,
},
_ => 0,
};
// Create a buffer to write to
let mut bytes: Vec<u8> = Vec::new();
let mut writer = Cursor::new(&mut bytes);
// Apply the EXIF rotation
// See https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
match &rotation {
2 => image?.fliph(),
3 => image?.rotate180(),
4 => image?.rotate180().fliph(),
5 => image?.rotate90().fliph(),
6 => image?.rotate90(),
7 => image?.rotate270().fliph(),
8 => image?.rotate270(),
_ => image?,
}
.write_to(
&mut writer,
match mime {
"image/jpeg" => ImageFormat::Jpeg,
"image/png" => ImageFormat::Png,
"image/avif" => ImageFormat::Avif,
"image/tiff" => ImageFormat::Tiff,
_ => todo!(),
},
)
.map_err(|_| create_error!(InternalError))?;
// Calculate dimensions after rotation.
let (width, height) = match &rotation {
2 | 4 | 5 | 7 => (*height, *width),
_ => (*width, *height),
};
Ok((bytes, Metadata::Image { width, height }))
}
// JXLs store EXIF data but we don't have the ability to write them
"image/jxl" => Ok((buf, metadata)),
// All other images that cannot store EXIF data
_ => Ok((buf, metadata)),
},
// Use ffmpeg to copy video stream and probe new metadata
Metadata::Video { .. } => match mime {
// Strip EXIF data by copying video stream
"video/mp4" | "video/webm" | "video/quicktime" => {
// Pick the correct file format for ffmpeg
let ext = match mime {
"video/mp4" => "mp4",
"video/webm" => "webm",
"video/quicktime" => "mov",
_ => unreachable!(),
};
// Temporary output file
let mut out_file =
NamedTempFile::new().map_err(|_| create_error!(InternalError))?;
// Process the file with ffmpeg
Command::new("ffmpeg")
.args([
// Overwrite the temporary file
"-y",
// Read original uploaded file
"-i",
file.path().to_str().ok_or(create_error!(InternalError))?,
// Strip any metadata
"-map_metadata",
"-1",
// Copy video / audio data to new file
"-c:v",
"copy",
"-c:a",
"copy",
// Select correct file format
"-f",
ext,
// Save to new temporary file
out_file
.path()
.to_str()
.ok_or(create_error!(InternalError))?,
])
.output()
.await
.inspect_err(|err| tracing::error!("{err:?}"))
.map_err(|_| create_error!(InternalError))?;
// Probe the file again
let metadata = crate::metadata::generate_metadata(&out_file, mime);
// Read the file from disk
let mut buf = Vec::<u8>::new();
out_file
.read_to_end(&mut buf)
.map_err(|_| create_error!(InternalError))?;
Ok((buf, metadata))
}
// Assume all other video formats cannot store EXIF data
_ => Ok((buf, metadata)),
},
// all other file types don't store EXIF data
_ => Ok((buf, metadata)),
}
}
+12 -2
View File
@@ -2,6 +2,7 @@ use std::net::{Ipv4Addr, SocketAddr};
use axum::Router;
use revolt_database::DatabaseInfo;
use tokio::net::TcpListener;
use utoipa::{
openapi::security::{ApiKey, ApiKeyValue, SecurityScheme},
@@ -10,6 +11,9 @@ use utoipa::{
use utoipa_scalar::{Scalar, Servable as ScalarServable};
mod api;
pub mod exif;
pub mod metadata;
pub mod mime_type;
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
@@ -28,6 +32,8 @@ async fn main() -> Result<(), std::io::Error> {
),
components(
schemas(
revolt_result::Error,
revolt_result::ErrorType,
api::RootResponse,
api::Tag,
api::UploadPayload,
@@ -53,13 +59,17 @@ async fn main() -> Result<(), std::io::Error> {
}
}
// Connect to the database
let db = DatabaseInfo::Auto.connect().await.unwrap();
// Configure Axum and router
let app = Router::new()
.merge(Scalar::with_url("/scalar", ApiDoc::openapi()))
.nest("/", api::router().await);
.nest("/", api::router().await)
.with_state(db);
// Configure TCP listener and bind
let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 3000));
let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 14704));
let listener = TcpListener::bind(&address).await?;
axum::serve(listener, app.into_make_service()).await
}
+89
View File
@@ -0,0 +1,89 @@
use std::{io::Cursor, os::unix::fs::MetadataExt};
use revolt_database::Metadata;
use revolt_result::{create_error, Result};
use tempfile::NamedTempFile;
/// Intersection of what infer can detect and what image-rs supports
///
/// Note: imagesize crate also supports all of these, so we use that for quick size probing.
static SUPPORTED_IMAGE_MIME: [&str; 9] = [
"image/avif",
"image/bmp",
"image/gif",
"image/vnd.microsoft.icon",
"image/jpeg",
"image/jxl", // not supported by image-rs but we shim it
"image/png",
"image/tiff",
"image/webp",
];
/// Generate metadata from file, using mime type as a hint
pub fn generate_metadata(f: &NamedTempFile, mime_type: &str) -> Metadata {
if SUPPORTED_IMAGE_MIME.contains(&mime_type) {
if let Ok(size) = imagesize::size(f.path())
.inspect_err(|err| tracing::error!("Failed to generate image size! {err:?}"))
{
if let (Ok(width), Ok(height)) = (size.width.try_into(), size.height.try_into()) {
return Metadata::Image { width, height };
}
}
Metadata::File
} else if mime_type.starts_with("video/") {
if let Ok(data) = ffprobe::ffprobe(f.path())
.inspect_err(|err| tracing::error!("Failed to ffprobe file! {err:?}"))
{
// Use first valid stream
for stream in data.streams {
if let (Some(w), Some(h)) = (stream.width, stream.height) {
if let (Ok(width), Ok(height)) = (w.try_into(), h.try_into()) {
return Metadata::Video { width, height };
}
}
}
Metadata::File
} else {
Metadata::File
}
} else if mime_type.starts_with("audio/") {
Metadata::Audio
} else if mime_type == "plain/text" {
Metadata::Text
} else {
Metadata::File
}
}
/// Subroutine to ensure data isn't corrupted
pub fn validate_from_metadata(
reader: Cursor<Vec<u8>>,
metadata: Metadata,
mime_type: &str,
) -> Metadata {
if let Metadata::Image { .. } = &metadata {
if mime_type == "image/jxl" {
// Check if we can read using jxl-oxide crate
if jxl_oxide::JxlImage::builder()
.read(reader)
.inspect_err(|err| tracing::error!("Failed to read JXL! {err:?}"))
.is_err()
{
return Metadata::File;
}
} else if matches!(
// Check if we can read using image-rs crate
image::ImageReader::new(reader)
.with_guessed_format()
.inspect_err(|err| tracing::error!("Failed to read image! {err:?}"))
.map(|f| f.decode()),
Err(_) | Ok(Err(_))
) {
return Metadata::File;
}
}
metadata
}
+27
View File
@@ -0,0 +1,27 @@
use tempfile::NamedTempFile;
/// Determine the mime type of the given temporary file and filename
pub fn determine_mime_type(f: &mut NamedTempFile, buf: &Vec<u8>, file_name: &str) -> &'static str {
// Use magic signatures to determine mime type
let kind = infer::get_from_path(f.path()).expect("file read successfully");
let mime_type = if let Some(kind) = kind {
kind.mime_type()
} else {
"application/octet-stream"
};
// Map any known conflicts where appropriate
let mime_type = if mime_type == "application/zip" && file_name.to_lowercase().ends_with(".apk")
{
"application/vnd.android.package-archive"
} else {
mime_type
};
// See if the file is actually just plain Unicode/ASCII text
if mime_type == "application/octet-stream" && simdutf8::basic::from_utf8(buf).is_ok() {
return "plain/text";
}
mime_type
}
+40
View File
@@ -0,0 +1,40 @@
[package]
name = "revolt-january"
version = "0.7.14"
edition = "2021"
[dependencies]
# Utility
mime = "0.3.17"
lazy_static = "1.5.0"
moka = { version = "0.12.8", features = ["future"] }
# Serialisation
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.68"
# Async runtime
tokio = { version = "1.0", features = ["full"] }
# Web requests
reqwest = { version = "0.12", features = ["json"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Core crates
revolt-config = { version = "0.7.16", path = "../../core/config" }
revolt-models = { version = "0.7.16", path = "../../core/models" }
revolt-result = { version = "0.7.16", path = "../../core/result", features = [
"utoipa",
"axum",
] }
# Axum / web server
axum = { version = "0.7.5" }
axum-extra = { version = "0.9", features = ["typed-header"] }
# OpenAPI & documentation generation
utoipa-scalar = { version = "0.1.0", features = ["axum"] }
utoipa = { version = "4.2.3", features = ["axum_extras", "ulid"] }
+85
View File
@@ -0,0 +1,85 @@
use axum::{body::Bytes, extract::Query, routing::get, Json, Router};
use revolt_models::v0::Embed;
use revolt_result::Result;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use crate::requests::Request;
pub async fn router() -> Router {
Router::new()
.route("/", get(root))
.route("/proxy", get(proxy))
.route("/embed", get(embed))
}
/// Successful root response
#[derive(Serialize, Debug, ToSchema)]
pub struct RootResponse {
january: &'static str,
version: &'static str,
}
/// Capture crate version from Cargo
static CRATE_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Root response from service
#[utoipa::path(
get,
path = "/",
responses(
(status = 200, description = "Echo response", body = RootResponse)
)
)]
async fn root() -> Json<RootResponse> {
Json(RootResponse {
january: "Hello, I am a media proxy server!",
version: CRATE_VERSION,
})
}
#[derive(Deserialize)]
struct UrlQuery {
url: String,
}
/// Proxy a given URL and load media
#[utoipa::path(
get,
path = "/proxy",
responses(
(status = 200, description = "Requested media file", body = Vec<u8>)
),
params(
("url" = String, Query, description = "URL to fetch")
),
)]
async fn proxy(Query(UrlQuery { url }): Query<UrlQuery>) -> Result<Bytes> {
Request::proxy_file(&url).await
}
/// Generate embed for a given URL
#[utoipa::path(
get,
path = "/embed",
responses(
(status = 200, description = "Generated embed information", body = Embed)
),
params(
("url" = String, Query, description = "URL to fetch")
),
security(
("api_key" = [])
)
)]
async fn embed(
Query(UrlQuery { url }): Query<UrlQuery>,
TypedHeader(Authorization(_bearer)): TypedHeader<Authorization<Bearer>>,
) -> Result<Json<Embed>> {
Request::generate_embed(&url).await.map(Json)
}
+71
View File
@@ -0,0 +1,71 @@
use std::net::{Ipv4Addr, SocketAddr};
use axum::Router;
use tokio::net::TcpListener;
use utoipa::{
openapi::security::{Http, HttpAuthScheme, SecurityScheme},
Modify, OpenApi,
};
use utoipa_scalar::{Scalar, Servable as ScalarServable};
mod api;
pub mod requests;
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
// Configure logging and environment
revolt_config::configure!(api);
// Configure API schema
#[derive(OpenApi)]
#[openapi(
modifiers(&SecurityAddon),
paths(
api::root,
api::proxy,
api::embed
),
components(
schemas(
api::RootResponse,
revolt_result::Error,
revolt_result::ErrorType,
revolt_models::v0::ImageSize,
revolt_models::v0::Image,
revolt_models::v0::Video,
revolt_models::v0::TwitchType,
revolt_models::v0::LightspeedType,
revolt_models::v0::BandcampType,
revolt_models::v0::Special,
revolt_models::v0::WebsiteMetadata,
revolt_models::v0::Text,
revolt_models::v0::Embed
)
)
)]
struct ApiDoc;
struct SecurityAddon;
impl Modify for SecurityAddon {
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
if let Some(components) = openapi.components.as_mut() {
components.add_security_scheme(
"api_key",
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)),
)
}
}
}
// Configure Axum and router
let app = Router::new()
.merge(Scalar::with_url("/scalar", ApiDoc::openapi()))
.nest("/", api::router().await);
// Configure TCP listener and bind
let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 14705));
let listener = TcpListener::bind(&address).await?;
axum::serve(listener, app.into_make_service()).await
}
+91
View File
@@ -0,0 +1,91 @@
use std::time::Duration;
use axum::body::Bytes;
use lazy_static::lazy_static;
use mime::Mime;
use reqwest::{header::CONTENT_TYPE, redirect, Client, Response};
use revolt_models::v0::Embed;
use revolt_result::{create_error, Result};
lazy_static! {
static ref CLIENT: Client = reqwest::Client::builder()
.user_agent("Mozilla/5.0 (compatible; January/2.0; +https://github.com/revoltchat/backend)")
.timeout(Duration::from_secs(10)) // TODO config
.connect_timeout(Duration::from_secs(5)) // TODO config
.redirect(redirect::Policy::custom(|attempt| {
if attempt.previous().len() > 5 { // TODO config
attempt.error("too many redirects")
} else if attempt.url().host_str() == Some("jan.revolt.chat") { // TODO config
attempt.stop()
} else {
attempt.follow()
}
}))
.build()
.expect("reqwest Client");
/// Cache for proxy results
static ref PROXY_CACHE: moka::future::Cache<String, Result<Bytes>> = moka::future::Cache::builder()
.max_capacity(10_000) // TODO config
.time_to_live(Duration::from_secs(60)) // TODO config
.build();
/// Cache for embed results
static ref EMBED_CACHE: moka::future::Cache<String, Result<Embed>> = moka::future::Cache::builder()
.max_capacity(1_000) // TODO config
.time_to_live(Duration::from_secs(60)) // TODO config
.build();
}
/// Information about a successful request
pub struct Request {
response: Response,
mime: Mime,
}
impl Request {
/// Proxy a given URL
pub async fn proxy_file(url: &str) -> Result<Bytes> {
if let Some(hit) = PROXY_CACHE.get(url).await {
hit
} else {
todo!()
}
}
/// Generate embed for a given URL
pub async fn generate_embed(url: &str) -> Result<Embed> {
if let Some(hit) = EMBED_CACHE.get(url).await {
hit
} else {
todo!()
}
}
/// Send a new request to a service
pub async fn new(url: &str) -> Result<Request> {
let response = CLIENT
.get(url)
.send()
.await
.map_err(|_| create_error!(ProxyError))?;
if !response.status().is_success() {
tracing::error!("{:?}", response);
return Err(create_error!(ProxyError));
}
let content_type = response
.headers()
.get(CONTENT_TYPE)
.ok_or(create_error!(ProxyError))?
.to_str()
.map_err(|_| create_error!(ProxyError))?;
let mime: mime::Mime = content_type
.parse()
.map_err(|_| create_error!(ProxyError))?;
Ok(Request { response, mime })
}
}
-15
View File
@@ -1,15 +0,0 @@
version: "3.3"
services:
# Redis
redis:
image: eqalpha/keydb
ports:
- "6379:6379"
# MongoDB
database:
image: mongo
ports:
- "27017:27017"
volumes:
- ./.data/db:/data/db
-72
View File
@@ -1,72 +0,0 @@
version: "3.3"
services:
# Redis
redis:
image: eqalpha/keydb
ports:
- "6379:6379"
# MongoDB
database:
image: mongo
ports:
- "27017:27017"
volumes:
- ./.data/db:/data/db
# MinIO
minio:
image: minio/minio
command: server /data
env_file: .env
volumes:
- ./.data/minio:/data
ports:
- "10000:9000"
restart: always
# Mongo Express
mongo-express:
image: mongo-express
ports:
- "8081:8081"
environment:
- ME_CONFIG_MONGODB_SERVER=database
depends_on:
- database
# Create buckets for minio.
createbuckets:
image: minio/mc
depends_on:
- minio
env_file: .env
entrypoint: >
/bin/sh -c "
/usr/bin/mc config host add minio http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD;
while ! /usr/bin/mc ready minio; do echo 'Waiting minio...' && sleep 1; done;
/usr/bin/mc mb minio/attachments;
/usr/bin/mc mb minio/avatars;
/usr/bin/mc mb minio/backgrounds;
/usr/bin/mc mb minio/icons;
/usr/bin/mc mb minio/banners;
/usr/bin/mc mb minio/emojis;
exit 0;
"
# File server (autumn)
autumn:
image: ghcr.io/revoltchat/autumn:1.1.5
env_file: .env
depends_on:
- createbuckets
ports:
- "3000:3000"
restart: always
# Metadata and image proxy (january)
january:
image: ghcr.io/revoltchat/january:0.3.4
ports:
- "7000:7000"
restart: always
+4 -2
View File
@@ -32,11 +32,13 @@ deps() {
crates/core/permissions/src \
crates/core/presence/src \
crates/core/result/src \
crates/services/autumn/src
crates/services/autumn/src \
crates/services/january/src
echo 'fn main() { panic!("stub"); }' |
tee crates/bonfire/src/main.rs |
tee crates/delta/src/main.rs |
tee crates/services/autumn/src/main.rs
tee crates/services/autumn/src/main.rs |
tee crates/services/january/src/main.rs
echo '' |
tee crates/bindings/node/src/lib.rs |
tee crates/core/config/src/lib.rs |
+19 -6
View File
@@ -1,14 +1,22 @@
#!/bin/bash
#!/usr/bin/env bash
# fail asap
set -e
# Check if an argument was provided
if [ "$#" -eq 0 ]; then
if [ $# -eq 0 ]; then
echo "No arguments provided"
echo "Usage: scripts/build-current-arch.sh 20230826-1"
echo "Usage: scripts/publish-debug-image.sh 20230826-1 true"
echo ""
echo "Last argument specifies whether we should have a debug build as opposed to release build."
exit 1
fi
echo "[profile.release]" >> Cargo.toml
echo "debug = true" >> Cargo.toml
DEBUG=$2
if [ "$DEBUG" = "true" ]; then
echo "[profile.release]" >> Cargo.toml
echo "debug = true" >> Cargo.toml
fi
TAG=$1-debug
echo "Building images, will tag for ghcr.io with $TAG!"
@@ -16,4 +24,9 @@ docker build -t ghcr.io/revoltchat/base:latest -f Dockerfile.useCurrentArch .
docker build -t ghcr.io/revoltchat/server:$TAG - < crates/delta/Dockerfile
docker build -t ghcr.io/revoltchat/bonfire:$TAG - < crates/bonfire/Dockerfile
git restore Cargo.toml
if [ "$DEBUG" = "true" ]; then
git restore Cargo.toml
fi
docker push ghcr.io/revoltchat/server:$TAG
docker push ghcr.io/revoltchat/bonfire:$TAG