Compare commits

...
7 Commits
20 changed files with 140 additions and 136 deletions

No files matched your search

Generated
+9 -9
View File
@@ -3580,7 +3580,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.6.10" version = "0.6.12"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-tungstenite", "async-tungstenite",
@@ -3600,7 +3600,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-config" name = "revolt-config"
version = "0.6.10" version = "0.6.12"
dependencies = [ dependencies = [
"async-std", "async-std",
"cached", "cached",
@@ -3612,7 +3612,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-database" name = "revolt-database"
version = "0.6.10" version = "0.6.12"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"async-recursion", "async-recursion",
@@ -3657,7 +3657,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-delta" name = "revolt-delta"
version = "0.6.10" version = "0.6.12"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"async-std", "async-std",
@@ -3703,7 +3703,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-models" name = "revolt-models"
version = "0.6.10" version = "0.6.12"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"iso8601-timestamp 0.2.11", "iso8601-timestamp 0.2.11",
@@ -3719,7 +3719,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.6.10" version = "0.6.12"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3734,7 +3734,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-presence" name = "revolt-presence"
version = "0.6.10" version = "0.6.12"
dependencies = [ dependencies = [
"async-std", "async-std",
"log", "log",
@@ -3745,7 +3745,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-quark" name = "revolt-quark"
version = "0.6.10" version = "0.6.12"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"async-recursion", "async-recursion",
@@ -3798,7 +3798,7 @@ dependencies = [
[[package]] [[package]]
name = "revolt-result" name = "revolt-result"
version = "0.6.10" version = "0.6.12"
dependencies = [ dependencies = [
"revolt_okapi", "revolt_okapi",
"revolt_rocket_okapi", "revolt_rocket_okapi",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-bonfire" name = "revolt-bonfire"
version = "0.6.10" version = "0.6.12"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
edition = "2021" edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-config" name = "revolt-config"
version = "0.6.10" version = "0.6.12"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+6 -6
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-database" name = "revolt-database"
version = "0.6.10" version = "0.6.12"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -23,11 +23,11 @@ default = ["mongodb", "async-std-runtime", "tasks"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.6.10", path = "../config" } revolt-config = { version = "0.6.12", path = "../config" }
revolt-result = { version = "0.6.10", path = "../result" } revolt-result = { version = "0.6.12", path = "../result" }
revolt-models = { version = "0.6.10", path = "../models" } revolt-models = { version = "0.6.12", path = "../models" }
revolt-presence = { version = "0.6.10", path = "../presence" } revolt-presence = { version = "0.6.12", path = "../presence" }
revolt-permissions = { version = "0.6.10", path = "../permissions", features = [ revolt-permissions = { version = "0.6.12", path = "../permissions", features = [
"serde", "serde",
"bson", "bson",
] } ] }
@@ -212,6 +212,7 @@ impl Message {
author: MessageAuthor<'_>, author: MessageAuthor<'_>,
mut idempotency: IdempotencyKey, mut idempotency: IdempotencyKey,
generate_embeds: bool, generate_embeds: bool,
allow_mentions: bool,
) -> Result<Message> { ) -> Result<Message> {
let config = config().await; let config = config().await;
@@ -272,10 +273,12 @@ impl Message {
// Parse mentions in message. // Parse mentions in message.
let mut mentions = HashSet::new(); let mut mentions = HashSet::new();
if let Some(content) = &data.content { if allow_mentions {
for capture in RE_MENTION.captures_iter(content) { if let Some(content) = &data.content {
if let Some(mention) = capture.get(1) { for capture in RE_MENTION.captures_iter(content) {
mentions.insert(mention.as_str().to_string()); if let Some(mention) = capture.get(1) {
mentions.insert(mention.as_str().to_string());
}
} }
} }
} }
@@ -292,7 +295,7 @@ impl Message {
for ReplyIntent { id, mention } in entries { for ReplyIntent { id, mention } in entries {
let message = db.fetch_message(&id).await?; let message = db.fetch_message(&id).await?;
if mention { if mention && allow_mentions {
mentions.insert(message.author.to_owned()); mentions.insert(message.author.to_owned());
} }
+2 -11
View File
@@ -341,11 +341,7 @@ impl From<crate::Message> for Message {
embeds: value.embeds, embeds: value.embeds,
mentions: value.mentions, mentions: value.mentions,
replies: value.replies, replies: value.replies,
reactions: value reactions: value.reactions,
.reactions
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect(),
interactions: value.interactions.into(), interactions: value.interactions.into(),
masquerade: value.masquerade.map(|masq| masq.into()), masquerade: value.masquerade.map(|masq| masq.into()),
} }
@@ -369,12 +365,7 @@ impl From<crate::PartialMessage> for PartialMessage {
embeds: value.embeds, embeds: value.embeds,
mentions: value.mentions, mentions: value.mentions,
replies: value.replies, replies: value.replies,
reactions: value.reactions.map(|reactions| { reactions: value.reactions,
reactions
.into_iter()
.map(|(k, v)| (k, v.into_iter().collect()))
.collect()
}),
interactions: value.interactions.map(|interactions| interactions.into()), interactions: value.interactions.map(|interactions| interactions.into()),
masquerade: value.masquerade.map(|masq| masq.into()), masquerade: value.masquerade.map(|masq| masq.into()),
} }
@@ -444,6 +444,26 @@ impl<'a> DatabasePermissionQuery<'a> {
..self ..self
} }
} }
/// Access the underlying user
pub fn user_ref(&self) -> &Option<Cow<User>> {
&self.user
}
/// Access the underlying server
pub fn channel_ref(&self) -> &Option<Cow<Channel>> {
&self.channel
}
/// Access the underlying server
pub fn server_ref(&self) -> &Option<Cow<Server>> {
&self.server
}
/// Access the underlying member
pub fn member_ref(&self) -> &Option<Cow<Member>> {
&self.member
}
} }
/// Short-hand for creating a permission calculator /// Short-hand for creating a permission calculator
+4 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-models" name = "revolt-models"
version = "0.6.10" version = "0.6.12"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
@@ -18,8 +18,8 @@ default = ["serde", "partials"]
[dependencies] [dependencies]
# Core # Core
revolt-config = { version = "0.6.10", path = "../config" } revolt-config = { version = "0.6.12", path = "../config" }
revolt-permissions = { version = "0.6.10", path = "../permissions" } revolt-permissions = { version = "0.6.12", path = "../permissions" }
# Utility # Utility
regex = "1" regex = "1"
@@ -32,7 +32,7 @@ serde = { version = "1", features = ["derive"], optional = true }
iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] } iso8601-timestamp = { version = "0.2.11", features = ["schema", "bson"] }
# Spec Generation # Spec Generation
schemars = { version = "0.8.8", optional = true } schemars = { version = "0.8.8", optional = true, features = ["indexmap1"] }
# Validation # Validation
validator = { version = "0.16.0", optional = true, features = ["derive"] } validator = { version = "0.16.0", optional = true, features = ["derive"] }
+5 -7
View File
@@ -1,8 +1,6 @@
use std::{ use std::time::SystemTime;
collections::{HashMap, HashSet},
time::SystemTime,
};
use indexmap::{IndexMap, IndexSet};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use revolt_config::config; use revolt_config::config;
@@ -55,8 +53,8 @@ auto_derived_partial!(
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub replies: Option<Vec<String>>, pub replies: Option<Vec<String>>,
/// Hashmap of emoji IDs to array of user IDs /// Hashmap of emoji IDs to array of user IDs
#[serde(skip_serializing_if = "HashMap::is_empty", default)] #[serde(skip_serializing_if = "IndexMap::is_empty", default)]
pub reactions: HashMap<String, HashSet<String>>, pub reactions: IndexMap<String, IndexSet<String>>,
/// Information about how this message should be interacted with /// Information about how this message should be interacted with
#[serde(skip_serializing_if = "Interactions::is_default", default)] #[serde(skip_serializing_if = "Interactions::is_default", default)]
pub interactions: Interactions, pub interactions: Interactions,
@@ -119,7 +117,7 @@ auto_derived!(
pub struct Interactions { pub struct Interactions {
/// Reactions which should always appear and be distinct /// Reactions which should always appear and be distinct
#[serde(skip_serializing_if = "Option::is_none", default)] #[serde(skip_serializing_if = "Option::is_none", default)]
pub reactions: Option<HashSet<String>>, pub reactions: Option<IndexSet<String>>,
/// Whether reactions should be restricted to the given list /// Whether reactions should be restricted to the given list
/// ///
/// Can only be set to true if reactions list is of at least length 1 /// Can only be set to true if reactions list is of at least length 1
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-permissions" name = "revolt-permissions"
version = "0.6.10" version = "0.6.12"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-presence" name = "revolt-presence"
version = "0.6.10" version = "0.6.12"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-result" name = "revolt-result"
version = "0.6.10" version = "0.6.12"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <me@insrt.uk>"] authors = ["Paul Makles <me@insrt.uk>"]
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-delta" name = "revolt-delta"
version = "0.6.10" version = "0.6.12"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
authors = ["Paul Makles <paulmakles@gmail.com>"] authors = ["Paul Makles <paulmakles@gmail.com>"]
edition = "2018" edition = "2018"
@@ -1,3 +1,4 @@
use chrono::{Duration, Utc};
use revolt_database::util::permissions::DatabasePermissionQuery; use revolt_database::util::permissions::DatabasePermissionQuery;
use revolt_database::{ use revolt_database::{
util::idempotency::IdempotencyKey, util::reference::Reference, Database, User, util::idempotency::IdempotencyKey, util::reference::Reference, Database, User,
@@ -61,6 +62,23 @@ pub async fn message_send(
interactions.validate(db, &permissions).await?; interactions.validate(db, &permissions).await?;
} }
// Disallow mentions for new users (TRUST-0: <12 hours age) in public servers
let allow_mentions = if let Some(server) = query.server_ref() {
if server.discoverable {
if (Utc::now() - ulid::Ulid::from_string(&user.id).unwrap().datetime())
< Duration::hours(12)
{
false
} else {
true
}
} else {
true
}
} else {
true
};
// Create the message // Create the message
let author: v0::User = user.clone().into(db, Some(&user)).await; let author: v0::User = user.clone().into(db, Some(&user)).await;
Ok(Json( Ok(Json(
@@ -71,6 +89,7 @@ pub async fn message_send(
v0::MessageAuthor::User(&author), v0::MessageAuthor::User(&author),
idempotency, idempotency,
permissions.has_channel_permission(ChannelPermission::SendEmbeds), permissions.has_channel_permission(ChannelPermission::SendEmbeds),
allow_mentions,
) )
.await? .await?
.into(), .into(),
@@ -60,6 +60,7 @@ pub async fn webhook_execute(
v0::MessageAuthor::Webhook(&webhook.into()), v0::MessageAuthor::Webhook(&webhook.into()),
idempotency, idempotency,
true, true,
true,
) )
.await? .await?
.into(), .into(),
+42 -36
View File
@@ -180,7 +180,7 @@ impl Ratelimiter {
pub fn from( pub fn from(
identifier: &str, identifier: &str,
(bucket, resource): (&str, Option<&str>), (bucket, resource): (&str, Option<&str>),
) -> Result<Ratelimiter, u128> { ) -> Result<Ratelimiter, Ratelimiter> {
let mut key = DefaultHasher::new(); let mut key = DefaultHasher::new();
key.write(identifier.as_bytes()); key.write(identifier.as_bytes());
key.write(bucket.as_bytes()); key.write(bucket.as_bytes());
@@ -194,27 +194,29 @@ impl Ratelimiter {
let mut entry = Entry::from(key); let mut entry = Entry::from(key);
let remaining = entry.get_remaining(limit); let remaining = entry.get_remaining(limit);
if remaining > 0 { let reset = entry.left_until_reset();
entry.deduct(); let mut ratelimiter = Ratelimiter {
key,
let reset = entry.left_until_reset(); limit,
entry.save(key); remaining,
reset,
Ok(Ratelimiter { };
key, if remaining == 0 {
limit, return Err(ratelimiter);
remaining: remaining - 1,
reset,
})
} else {
Err(entry.left_until_reset())
} }
entry.deduct();
entry.save(key);
ratelimiter.remaining -= 1;
ratelimiter.reset = entry.left_until_reset();
Ok(ratelimiter)
} }
} }
#[async_trait] #[async_trait]
impl<'r> FromRequest<'r> for Ratelimiter { impl<'r> FromRequest<'r> for Ratelimiter {
type Error = u128; type Error = Ratelimiter;
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> { async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
let ratelimiter = request let ratelimiter = request
@@ -233,7 +235,7 @@ impl<'r> FromRequest<'r> for Ratelimiter {
match ratelimiter { match ratelimiter {
Ok(ratelimiter) => Outcome::Success(*ratelimiter), Ok(ratelimiter) => Outcome::Success(*ratelimiter),
Err(retry_after) => Outcome::Failure((Status::TooManyRequests, *retry_after)), Err(ratelimiter) => Outcome::Failure((Status::TooManyRequests, *ratelimiter)),
} }
} }
} }
@@ -275,23 +277,24 @@ impl Fairing for RatelimitFairing {
} }
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) { async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
use rocket::outcome::Outcome; let guard = request.guard::<Ratelimiter>().await;
match request.guard::<Ratelimiter>().await { let (Outcome::Success(ratelimiter) | Outcome::Failure((_, ratelimiter))) = guard else {
Outcome::Success(ratelimiter) => { unreachable!()
let Ratelimiter { };
key, let Ratelimiter {
limit, key,
remaining, limit,
reset, remaining,
} = ratelimiter; reset,
} = ratelimiter;
response.set_raw_header("X-RateLimit-Limit", limit.to_string()); response.set_raw_header("X-RateLimit-Limit", limit.to_string());
response.set_raw_header("X-RateLimit-Bucket", key.to_string()); response.set_raw_header("X-RateLimit-Bucket", key.to_string());
response.set_raw_header("X-RateLimit-Remaining", remaining.to_string()); response.set_raw_header("X-RateLimit-Remaining", remaining.to_string());
response.set_raw_header("X-RateLimit-Reset-After", reset.to_string()); response.set_raw_header("X-RateLimit-Reset-After", reset.to_string());
}
Outcome::Failure(_) => response.set_status(Status::TooManyRequests), if guard.is_failure() {
Outcome::Forward(_) => unreachable!(), response.set_status(Status::TooManyRequests);
} }
} }
} }
@@ -308,11 +311,14 @@ impl<'r> FromRequest<'r> for RatelimitInformation {
type Error = u128; type Error = u128;
async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> { async fn from_request(request: &'r rocket::Request<'_>) -> Outcome<Self, Self::Error> {
Outcome::Success(match request.guard::<Ratelimiter>().await { let info = match request.guard::<Ratelimiter>().await {
Outcome::Success(ratelimiter) => RatelimitInformation::Success(ratelimiter), Outcome::Success(ratelimiter) => RatelimitInformation::Success(ratelimiter),
Outcome::Failure((_, retry_after)) => RatelimitInformation::Failure { retry_after }, Outcome::Failure((_, ratelimiter)) => RatelimitInformation::Failure {
retry_after: ratelimiter.reset,
},
_ => unreachable!(), _ => unreachable!(),
}) };
Outcome::Success(info)
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "revolt-quark" name = "revolt-quark"
version = "0.6.10" version = "0.6.12"
edition = "2021" edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
@@ -5,7 +5,6 @@ use iso8601_timestamp::Timestamp;
use crate::{ use crate::{
events::client::EventV1, events::client::EventV1,
models::{ models::{
server::Role,
server_member::{FieldsMember, MemberCompositeKey, MemberWithRoles, PartialMember}, server_member::{FieldsMember, MemberCompositeKey, MemberWithRoles, PartialMember},
Member, Server, Member, Server,
}, },
@@ -84,30 +83,13 @@ impl Member {
FieldsMember::Timeout => self.timeout = None, FieldsMember::Timeout => self.timeout = None,
} }
} }
pub fn with_roles(&self, roles: HashMap<String, Role>) -> MemberWithRoles {
MemberWithRoles {
id: self.id.clone(),
joined_at: self.joined_at,
nickname: self.nickname.clone(),
avatar: self.avatar.clone(),
roles,
timeout: self.timeout,
}
}
} }
impl MemberWithRoles { impl MemberWithRoles {
pub fn new(server_id: String, user_id: String) -> Self { pub fn new(server_id: String, user_id: String) -> Self {
Self { Self {
id: MemberCompositeKey { member: Member::new(server_id, user_id),
server: server_id,
user: user_id,
},
joined_at: Timestamp::now_utc(),
nickname: None,
avatar: None,
roles: HashMap::new(), roles: HashMap::new(),
timeout: None,
} }
} }
} }
@@ -22,10 +22,14 @@ impl AbstractServerMember for MongoDb {
} }
async fn fetch_member_with_roles(&self, server: &str, user: &str) -> Result<MemberWithRoles> { async fn fetch_member_with_roles(&self, server: &str, user: &str) -> Result<MemberWithRoles> {
Ok(self let member = self.fetch_member(server, user).await?;
.fetch_member(server, user) let server_roles = self.fetch_server(server).await?.roles;
.await? let roles = member
.with_roles(self.fetch_server(server).await?.roles)) .roles
.iter()
.filter_map(|id| server_roles.get(id).map(|r| (id.clone(), r.clone())))
.collect();
Ok(MemberWithRoles { member, roles })
} }
async fn insert_member(&self, member: &Member) -> Result<()> { async fn insert_member(&self, member: &Member) -> Result<()> {
@@ -47,47 +47,27 @@ pub struct Member {
} }
/// Representation of a member of a server on Revolt With Role Data /// Representation of a member of a server on Revolt With Role Data
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone, OptionalStruct)] #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
#[optional_derive(Serialize, Deserialize, JsonSchema, Debug, Default, Clone)]
#[opt_skip_serializing_none]
#[opt_some_priority]
pub struct MemberWithRoles { pub struct MemberWithRoles {
/// Unique member id pub member: Member,
#[serde(rename = "_id")]
pub id: MemberCompositeKey,
/// Time at which this user joined the server
pub joined_at: Timestamp,
/// Member's nickname
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>,
/// Avatar attachment
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<File>,
/// Member's roles
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
pub roles: HashMap<String, Role>, pub roles: HashMap<String, Role>,
/// Timestamp this member is timed out until
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<Timestamp>,
} }
#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
#[serde(untagged)]
pub enum MemberResponse { pub enum MemberResponse {
Members(Member), Member(Member),
MembersWithRoles(MemberWithRoles), MemberWithRoles(MemberWithRoles),
} }
impl From<Member> for MemberResponse { impl From<Member> for MemberResponse {
fn from(value: Member) -> Self { fn from(value: Member) -> Self {
Self::Members(value) Self::Member(value)
} }
} }
impl From<MemberWithRoles> for MemberResponse { impl From<MemberWithRoles> for MemberResponse {
fn from(value: MemberWithRoles) -> Self { fn from(value: MemberWithRoles) -> Self {
Self::MembersWithRoles(value) Self::MemberWithRoles(value)
} }
} }
/// Optional fields on server member object /// Optional fields on server member object