Files
payphone/src/database/models.rs
T

1556 lines
46 KiB
Rust

use scylla::{
DeserializeValue, SerializeValue,
client::session::Session,
deserialize::row::DeserializeRow,
errors::{ExecutionError, SingleRowError},
response::query_result::QueryRowsResult,
statement::{
Statement,
batch::{Batch, BatchType},
},
};
use thiserror::Error;
use time::{Duration, OffsetDateTime, UtcDateTime, UtcOffset};
use twilight_http::Client;
use twilight_model::{
channel::{Message as TwilightMessage, Webhook},
guild::Guild as TwilightGuild,
id::Id,
user::User as TwilightUser,
};
use crate::{
constants::DEFAULT_PREFIX,
groups::calls::DisplayProfile,
utils::{get_random_u64, i64_to_u64, u64_to_i64},
};
#[derive(Debug, Error)]
pub enum DatabaseOrDiscordError {
#[error("An error occurred when querying the database")]
Database(#[from] DatabaseError),
#[error("An error occurred when querying the Discord API")]
Discord(#[from] twilight_http::Error),
#[error("The Discord API returned a response that could not be deserialized")]
DiscordDeserializationFailed,
}
#[derive(Debug, Error)]
pub enum DatabaseError {
#[error("An error occurred when querying the database")]
QueryError(ExecutionError),
#[error("The database responded but the result of the query weren't rows")]
ResultNotRows,
#[error("The resulting row could not be deserialized into an object")]
DeserializationFailed,
}
trait Object<'a> {
type Row: DeserializeRow<'a, 'a>;
fn from_row(row: Self::Row) -> Self;
fn to_row(value: Self) -> Self::Row;
}
/// Returns the first row of the result set, if any.
///
/// Arguments:
/// * `result` - The DB result.
///
/// Returns:
/// [`Result<Option<T>, DatabaseError>`] - The first row of the result set, if any.
async fn get_single_row_from_result<'a, T: Object<'a>>(
result: &'a QueryRowsResult,
) -> Result<Option<T>, DatabaseError> {
match result.single_row::<T::Row>() {
Ok(row) => Ok(Some(T::from_row(row))),
Err(error) => match error {
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
Err(DatabaseError::DeserializationFailed)
}
SingleRowError::UnexpectedRowCount(_) => Ok(None),
},
}
}
#[derive(Debug, Clone)]
pub struct User {
pub id: u64,
pub name: String,
pub email: Option<String>,
pub is_masked: bool,
pub is_moderator: bool,
pub joined_at: UtcDateTime,
pub last_seen_at: UtcDateTime,
pub supporter_since: Option<UtcDateTime>,
pub supporter_until: Option<UtcDateTime>,
pub is_banned_from_leaderboard: Option<bool>,
}
impl<'a> Object<'a> for User {
type Row = (
i64,
String,
Option<String>,
bool,
bool,
OffsetDateTime,
OffsetDateTime,
Option<OffsetDateTime>,
Option<OffsetDateTime>,
Option<bool>,
);
fn from_row(row: Self::Row) -> Self {
Self {
id: i64_to_u64(row.0),
name: row.1,
email: row.2,
is_masked: row.3,
is_moderator: row.4,
joined_at: row.5.into(),
last_seen_at: row.6.into(),
supporter_since: row.7.map(|i| i.into()),
supporter_until: row.8.map(|i| i.into()),
is_banned_from_leaderboard: row.9,
}
}
fn to_row(value: Self) -> Self::Row {
(
u64_to_i64(value.id),
value.name,
value.email,
value.is_masked,
value.is_moderator,
value.joined_at.into(),
value.last_seen_at.into(),
value.supporter_since.map(|i| i.into()),
value.supporter_until.map(|i| i.into()),
Some(value.is_banned_from_leaderboard.unwrap_or(false)),
)
}
}
pub struct NewUserIncident {
pub counting_incidents: usize,
pub is_first_incident: bool,
}
impl User {
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
let id = u64_to_i64(id);
let result = session
.query_unpaged(
r#"
SELECT
user_id,
name,
email,
is_masked,
is_moderator,
joined_at,
last_seen_at,
supporter_since,
supporter_until,
is_banned_from_leaderboard
FROM
payphone.users
WHERE
user_id = ?
LIMIT 1;
"#,
(id,),
)
.await
.map_err(DatabaseError::QueryError)?
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
match result.single_row::<<User as Object>::Row>() {
Ok(row) => Ok(Some(Self::from_row(row))),
Err(error) => match error {
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
Err(DatabaseError::DeserializationFailed)
}
SingleRowError::UnexpectedRowCount(_) => Ok(None),
},
}
}
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
r#"
INSERT INTO payphone.users (
user_id,
name,
email,
is_masked,
is_moderator,
joined_at,
last_seen_at,
supporter_since,
supporter_until,
is_banned_from_leaderboard
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#,
Self::to_row(self.clone()),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
pub async fn incident(
&self,
reason: String,
message: Option<MessageData>,
created_by_id: Option<u64>,
session: &Session,
) -> Result<NewUserIncident, DatabaseError> {
let incidents = UserIncident::get_for_user(self.id, session).await?;
let counting_incidents = incidents
.iter()
.filter(|incident| incident.created_at + Duration::weeks(12) > UtcDateTime::now())
.count();
let incident_type = if counting_incidents != 0 {
UserIncidentType::Ban
} else {
UserIncidentType::Warning
};
let duration = match counting_incidents {
0 => None, // Warning
1 => Some(Duration::days(1)), // First ban
_ => None, // Permanent ban
};
let incident = UserIncident {
user_id: self.id,
incident_id: get_random_u64(),
report_id: None,
r#type: incident_type,
created_at: UtcDateTime::now(),
expires_at: duration.map(|d| UtcDateTime::now() + d),
message,
reason,
created_by_id,
duration: duration.map(|_| "1 day".into()),
};
incident.insert(session).await?;
Ok(NewUserIncident {
counting_incidents,
is_first_incident: incidents.is_empty(),
})
}
}
impl From<TwilightUser> for User {
fn from(value: TwilightUser) -> Self {
Self {
id: value.id.get(),
name: value.name,
email: value.email,
is_masked: false,
is_moderator: false,
joined_at: UtcDateTime::now(),
last_seen_at: UtcDateTime::now(),
supporter_since: None,
supporter_until: None,
is_banned_from_leaderboard: None,
}
}
}
/// Gets a user from the DB and sets their last seen timestamp or inserts a new user from Discord.
///
/// It also updates the user's username.
///
/// Arguments:
/// * `user` - The Twilight user to get or create.
/// * `session` - The Scylla session to use for database operations.
///
/// Returns:
/// [`Result<User, DatabaseError>`] - The user from the database or a new user created from
/// Discord.
pub async fn get_or_create_user_and_set_last_seen(
user: TwilightUser,
session: &Session,
) -> Result<User, DatabaseError> {
let db_user = User::get_by_id(user.id.get(), session).await?;
if let Some(mut db_user) = db_user {
db_user.last_seen_at = UtcDateTime::now();
db_user.name = user.name;
return Ok(db_user);
}
let new_user = User::from(user);
new_user.insert(session).await?;
Ok(new_user)
}
#[derive(Debug, Clone)]
pub struct Server {
pub id: u64,
pub name: String,
pub prefix: String,
pub language: String,
}
impl<'a> Object<'a> for Server {
type Row = (i64, String, String, String);
fn from_row(row: Self::Row) -> Self {
Self {
id: i64_to_u64(row.0),
name: row.1,
prefix: row.2,
language: row.3,
}
}
fn to_row(value: Self) -> Self::Row {
(
u64_to_i64(value.id),
value.name,
value.prefix,
value.language,
)
}
}
impl Server {
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
let id = u64_to_i64(id);
let result = session
.query_unpaged(
r#"
SELECT
server_id,
name,
prefix,
language
FROM
payphone.servers
WHERE
server_id = ?
LIMIT 1;
"#,
(id,),
)
.await
.map_err(DatabaseError::QueryError)?
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
match result.single_row::<<Server as Object>::Row>() {
Ok(row) => Ok(Some(Self::from_row(row))),
Err(error) => match error {
SingleRowError::DeserializationFailed(_) | SingleRowError::TypeCheckFailed(_) => {
Err(DatabaseError::DeserializationFailed)
}
SingleRowError::UnexpectedRowCount(_) => Ok(None),
},
}
}
pub async fn get_or_create_by_id(
id: u64,
session: &Session,
http: &Client,
) -> Result<Self, DatabaseOrDiscordError> {
let server = Server::get_by_id(id, session)
.await
.map_err(DatabaseOrDiscordError::Database)?;
if let Some(server) = server {
Ok(server)
} else {
let server: Server = http
.guild(Id::new(id))
.await
.map_err(DatabaseOrDiscordError::Discord)?
.model()
.await
.map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)?
.into();
server
.insert(session)
.await
.map_err(DatabaseOrDiscordError::Database)?;
Ok(server)
}
}
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
r#"
INSERT INTO payphone.servers (
server_id,
name,
prefix,
language
) VALUES (?, ?, ?, ?) IF NOT EXISTS;
"#,
Self::to_row(self.clone()),
)
.await
.inspect_err(|e| log::error!("{e:?}"))
.map_err(DatabaseError::QueryError)?;
Ok(())
}
pub async fn upsert(&self, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
r#"
INSERT INTO payphone.servers (
server_id,
name,
prefix,
language
) VALUES (
?,
?,
?,
?
);
"#,
Self::to_row(self.clone()),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
pub async fn delete_by_id(id: u64, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
"DELETE FROM payphone.servers WHERE server_id = ?;",
(u64_to_i64(id),),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
}
impl From<TwilightGuild> for Server {
fn from(value: TwilightGuild) -> Self {
Self {
id: value.id.get(),
name: value.name,
prefix: DEFAULT_PREFIX.into(),
language: value.preferred_locale,
}
}
}
#[derive(Debug, Clone)]
pub struct Channel {
pub id: u64,
pub server_id: u64,
pub call_id: Option<u64>,
pub is_searching: bool,
pub recently_called_server_ids: Vec<u64>,
pub webhook_id: Option<u64>,
pub webhook_token: Option<String>,
}
impl<'a> Object<'a> for Channel {
type Row = (
i64,
i64,
Option<i64>,
bool,
Vec<i64>,
Option<i64>,
Option<String>,
);
fn from_row(row: Self::Row) -> Self {
Self {
id: i64_to_u64(row.0),
server_id: i64_to_u64(row.1),
call_id: row.2.map(i64_to_u64),
is_searching: row.3,
recently_called_server_ids: row.4.iter().map(|&i| i64_to_u64(i)).collect(),
webhook_id: row.5.map(i64_to_u64),
webhook_token: row.6,
}
}
fn to_row(value: Self) -> Self::Row {
(
u64_to_i64(value.id),
u64_to_i64(value.server_id),
value.call_id.map(u64_to_i64),
value.is_searching,
value
.recently_called_server_ids
.iter()
.map(|&u| u64_to_i64(u))
.collect(),
value.webhook_id.map(u64_to_i64),
value.webhook_token,
)
}
}
async fn get_or_create_channel_webhook(
channel_id: u64,
http: &Client,
) -> Result<Webhook, DatabaseOrDiscordError> {
let webhooks = http
.channel_webhooks(Id::new(channel_id))
.await
.inspect_err(|e| log::error!("Failed to get webhooks from channel {channel_id}: {e:?}"))
.map_err(DatabaseOrDiscordError::Discord)?
.model()
.await
.inspect_err(|e| log::error!("Failed to get model for webhooks {channel_id}: {e:?}"))
.map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)?;
let webhook = if let Some(item) = webhooks.into_iter().find(|item| item.token.is_some()) {
item
} else {
http.create_webhook(Id::new(channel_id), "Payphone Messages")
.await
.inspect_err(|e| {
log::error!("Failed to create webhook for channel {channel_id}: {e:?}")
})
.map_err(DatabaseOrDiscordError::Discord)?
.model()
.await
.inspect_err(|e| log::error!("Failed to get model for webhook {channel_id}: {e:?}"))
.map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)?
};
Ok(webhook)
}
impl Channel {
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
let id = u64_to_i64(id);
let result = session
.query_unpaged(
r#"
SELECT
channel_id,
server_id,
call_id,
is_searching,
recently_called_server_ids,
webhook_id,
webhook_token
FROM
payphone.channels
WHERE
channel_id = ?
LIMIT 1;
"#,
(id,),
)
.await
.map_err(DatabaseError::QueryError)?
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
get_single_row_from_result(&result).await
}
pub async fn get_or_create_by_id(
id: u64,
session: &Session,
http: &Client,
) -> Result<Self, DatabaseOrDiscordError> {
if let Some(channel) = Channel::get_by_id(id, session)
.await
.map_err(DatabaseOrDiscordError::Database)?
{
Ok(channel)
} else {
let webhook = get_or_create_channel_webhook(id, http).await?;
let response = http
.channel(Id::new(id))
.await
.map_err(DatabaseOrDiscordError::Discord)?
.model()
.await
.inspect_err(|e| {
log::error!("The discord API didn't return a parseable channel! {e:?}")
})
.map_err(|_| DatabaseOrDiscordError::DiscordDeserializationFailed)?;
let channel = Channel {
id: response.id.get(),
server_id: response.guild_id.map(|i| i.get()).unwrap_or(0),
call_id: None,
is_searching: false,
recently_called_server_ids: vec![],
webhook_id: Some(webhook.id.get()),
webhook_token: webhook.token,
};
channel
.upsert(session)
.await
.inspect_err(|e| log::error!("Could not upsert channel in database! {e:?}"))
.map_err(DatabaseOrDiscordError::Database)?;
Ok(channel)
}
}
pub async fn upsert(&self, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
r#"
INSERT INTO payphone.channels (
channel_id,
server_id,
call_id,
is_searching,
recently_called_server_ids,
webhook_id,
webhook_token
) VALUES (?, ?, ?, ?, ?, ?, ?);
"#,
Self::to_row(self.clone()),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
pub async fn cancel_search(&self, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
"UPDATE payphone.channels SET is_searching = false WHERE channel_id = ?;",
(u64_to_i64(self.id),),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
pub async fn update_webhook(
&mut self,
session: &Session,
http: &Client,
) -> Result<(), DatabaseOrDiscordError> {
let webhook = get_or_create_channel_webhook(self.id, http).await?;
self.webhook_id = Some(webhook.id.get());
self.webhook_token = webhook.token;
self.upsert(session).await?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SearchingChannel {
pub id: u64,
pub server_id: u64,
pub recently_called_server_ids: Vec<u64>,
}
impl<'a> Object<'a> for SearchingChannel {
type Row = (i64, i64, Vec<i64>);
fn from_row(row: Self::Row) -> Self {
Self {
id: i64_to_u64(row.0),
server_id: i64_to_u64(row.1),
recently_called_server_ids: row.2.iter().map(|&i| i64_to_u64(i)).collect(),
}
}
fn to_row(value: Self) -> Self::Row {
(
u64_to_i64(value.id),
u64_to_i64(value.server_id),
value
.recently_called_server_ids
.iter()
.map(|&u| u64_to_i64(u))
.collect(),
)
}
}
impl SearchingChannel {
pub async fn get_all(session: &Session) -> Result<Vec<Self>, DatabaseError> {
let result = session
.query_unpaged(
r#"
SELECT
channel_id,
server_id,
recently_called_server_ids
FROM
payphone.searching_channels;
"#,
(),
)
.await
.map_err(DatabaseError::QueryError)?
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
Ok(result
.rows::<<SearchingChannel as Object>::Row>()
.map_err(|_| DatabaseError::ResultNotRows)?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| DatabaseError::DeserializationFailed)?
.into_iter()
.map(Self::from_row)
.collect::<Vec<_>>())
}
}
#[derive(Debug, Clone)]
pub struct Call {
pub id: u64,
pub channel_ids: Vec<u64>,
pub started_at: UtcDateTime,
pub finished_at: Option<UtcDateTime>,
}
impl<'a> Object<'a> for Call {
type Row = (i64, Vec<i64>, OffsetDateTime, Option<OffsetDateTime>);
fn from_row(row: Self::Row) -> Self {
Self {
id: i64_to_u64(row.0),
channel_ids: row.1.iter().map(|&i| i64_to_u64(i)).collect(),
started_at: row.2.into(),
finished_at: row.3.map(|i| i.into()),
}
}
fn to_row(value: Self) -> Self::Row {
(
u64_to_i64(value.id),
value.channel_ids.iter().map(|&u| u64_to_i64(u)).collect(),
value.started_at.into(),
value.finished_at.map(|i| i.into()),
)
}
}
fn push_and_cap<T>(vec: &mut Vec<T>, value: T, max_size: usize) {
if vec.len() >= max_size {
vec.remove(0);
}
vec.push(value);
}
impl Call {
pub async fn get_by_id(id: u64, session: &Session) -> Result<Option<Self>, DatabaseError> {
let id = u64_to_i64(id);
let result = session
.query_unpaged(
r#"
SELECT
call_id,
channel_ids,
started_at,
finished_at
FROM
payphone.calls
WHERE
call_id = ?
LIMIT 1;
"#,
(id,),
)
.await
.map_err(DatabaseError::QueryError)?
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
get_single_row_from_result(&result).await
}
/// Returns the opposite party's channel ID.
pub fn other_party_channel_id(&self, channel_id: u64) -> u64 {
if self.channel_ids[0] == channel_id {
self.channel_ids[1]
} else {
self.channel_ids[0]
}
}
pub fn is_active(&self) -> bool {
self.finished_at.is_none()
}
/// Starts a call, or marks the channel as searching if the call is not started.
///
/// Arguments:
/// * `channel` - The channel to start the call in.
/// * `session` - The Scylla session to use for database operations.
///
/// Returns:
/// [`Result<Option<Self>, DatabaseError>`] - `Ok(Some(Call))` if the call was started
/// successfully, or `Ok(None)` if the channel was marked as searching. If an error occurs, it
/// returns `Err(DatabaseError)`.
pub async fn start(
channel: &Channel,
session: &Session,
) -> Result<Option<Self>, DatabaseError> {
let searching_channels = SearchingChannel::get_all(session).await?;
let call_id = get_random_u64();
log::trace!("Call ID is {call_id}");
for searching_channel in searching_channels
.iter()
.filter(|sc| !sc.recently_called_server_ids.contains(&channel.server_id))
{
if searching_channel.server_id == channel.server_id {
return Ok(None);
}
if channel
.recently_called_server_ids
.contains(&searching_channel.server_id)
{
continue;
}
log::trace!("Found a searching channel: {searching_channel:?}");
let mut searching_channel_recently_called_server_ids = searching_channel
.recently_called_server_ids
.clone()
.into_iter()
.map(u64_to_i64)
.collect::<Vec<_>>();
push_and_cap(
&mut searching_channel_recently_called_server_ids,
u64_to_i64(channel.server_id),
5,
);
let (is_applied, ..) = session
.query_unpaged(
r#"
UPDATE payphone.channels
SET
is_searching = false,
call_id = ?,
recently_called_server_ids = ?
WHERE
channel_id = ?
IF call_id = NULL;
"#,
(
u64_to_i64(call_id),
searching_channel_recently_called_server_ids,
u64_to_i64(searching_channel.id),
),
)
.await
.inspect_err(|e| log::error!("Failed to add channel to the call! {e:?}"))
.map_err(DatabaseError::QueryError)?
.into_rows_result()
.inspect_err(|e| log::error!("Failed to get result of the query! {e:?}"))
.map_err(|_| DatabaseError::ResultNotRows)?
.first_row::<(bool, Option<i64>)>()
.inspect_err(|e| log::error!("Failed to get first row of the result! {e:?}"))
.map_err(|_| DatabaseError::DeserializationFailed)?;
log::trace!("Is applied: {is_applied:?}");
if is_applied {
let mut channel_recently_called_server_ids = channel
.recently_called_server_ids
.clone()
.into_iter()
.map(u64_to_i64)
.collect::<Vec<_>>();
push_and_cap(
&mut channel_recently_called_server_ids,
u64_to_i64(searching_channel.server_id),
5,
);
session
.query_unpaged(
"UPDATE payphone.channels SET call_id = ?, recently_called_server_ids = ? WHERE channel_id = ?",
(u64_to_i64(call_id), channel_recently_called_server_ids, u64_to_i64(channel.id)),
)
.await
.inspect_err(|e| log::error!("Failed to update channel {e:?}"))
.map_err(DatabaseError::QueryError)?;
let call = Call {
id: call_id,
channel_ids: vec![searching_channel.id, channel.id].into_iter().collect(),
started_at: UtcDateTime::now(),
finished_at: None,
};
call.insert(session)
.await
.inspect_err(|e| log::error!("Failed to insert call {e:?}"))?;
return Ok(Some(call));
} else {
// This channel already got a call, so we skip it, preventing race conditions.
continue;
}
}
session.query_unpaged(
"UPDATE payphone.channels SET is_searching = true WHERE channel_id = ? IF call_id = NULL;",
(u64_to_i64(channel.id),),
)
.await
.inspect_err(|e| log::error!("Failed to mark channel as searching! {e:?}"))
.map_err(DatabaseError::QueryError)?;
Ok(None)
}
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
r#"
INSERT INTO payphone.calls (
call_id,
channel_ids,
started_at,
finished_at
) VALUES (?, ?, ?, ?);
"#,
Self::to_row(self.clone()),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
pub async fn hangup(&self, session: &Session) -> Result<(), DatabaseError> {
let mut batch = Batch::new(BatchType::Unlogged);
batch.append_statement(Statement::new(
"UPDATE payphone.calls SET finished_at = ? WHERE call_id = ?;",
));
batch.append_statement(Statement::new(
"UPDATE payphone.channels SET call_id = NULL WHERE channel_id = ?;",
));
batch.append_statement(Statement::new(
"UPDATE payphone.channels SET call_id = NULL WHERE channel_id = ?;",
));
let call_id = u64_to_i64(self.id);
session
.batch(
&batch,
(
(OffsetDateTime::now_utc(), call_id),
(u64_to_i64(self.channel_ids[0]),),
(u64_to_i64(self.channel_ids[1]),),
),
)
.await
.map_err(DatabaseError::QueryError)?;
log::trace!("Hung up call {call_id}");
Ok(())
}
}
#[derive(Debug, Clone, SerializeValue, DeserializeValue)]
pub struct MessageData {
pub id: i64,
pub call_id: i64,
pub author_id: i64,
pub author_name: String,
pub author_avatar_url: String,
pub author_mask: Option<String>,
pub content: String,
pub attachments: Vec<String>,
pub sticker: Option<i64>,
pub propagation_message_id: Option<i64>,
pub propagation_channel_id: Option<i64>,
pub replying_to_message_id: Option<i64>,
pub is_deleted: bool,
pub is_edited: bool,
pub is_censored: bool,
}
#[derive(Debug, Error)]
#[error("The message was missing either the event's message, the user's profile, or the call ID")]
pub struct MessageBuilderError;
#[derive(Default)]
pub struct MessageBuilder {
message: Option<TwilightMessage>,
profile: Option<DisplayProfile>,
call_id: Option<u64>,
is_censored: bool,
propagation_message_id: Option<u64>,
propagation_channel_id: Option<u64>,
replying_to_message_id: Option<u64>,
}
impl MessageBuilder {
fn new() -> Self {
Self {
message: None,
profile: None,
call_id: None,
is_censored: false,
propagation_message_id: None,
propagation_channel_id: None,
replying_to_message_id: None,
}
}
pub fn message(mut self, message: TwilightMessage) -> Self {
self.message = Some(message);
self
}
pub fn profile(mut self, profile: DisplayProfile) -> Self {
self.profile = Some(profile);
self
}
pub fn call_id(mut self, call_id: u64) -> Self {
self.call_id = Some(call_id);
self
}
#[allow(clippy::wrong_self_convention)]
pub fn is_censored(mut self, is_censored: bool) -> Self {
self.is_censored = is_censored;
self
}
pub fn propagation_message_id(mut self, propagation_message_id: u64) -> Self {
self.propagation_message_id = Some(propagation_message_id);
self
}
pub fn propagation_channel_id(mut self, propagation_channel_id: u64) -> Self {
self.propagation_channel_id = Some(propagation_channel_id);
self
}
// Argument is made optional here because it happens to be more convenient for where this is
// used.
pub fn replying_to_message_id(mut self, replying_to_id: Option<u64>) -> Self {
self.replying_to_message_id = replying_to_id;
self
}
pub fn build(self) -> Result<Message, MessageBuilderError> {
let message = self.message.ok_or(MessageBuilderError)?;
let profile = self.profile.ok_or(MessageBuilderError)?;
let call_id = self.call_id.ok_or(MessageBuilderError)?;
let data = MessageData {
id: u64_to_i64(message.id.get()),
call_id: u64_to_i64(call_id),
author_id: u64_to_i64(profile.id),
author_name: profile.name.clone(),
author_avatar_url: profile.avatar_url.clone(),
author_mask: profile.mask,
content: message.content,
attachments: message.attachments.iter().map(|a| a.url.clone()).collect(),
sticker: message
.sticker_items
.first()
.map(|s| u64_to_i64(s.id.get())),
propagation_message_id: self.propagation_message_id.map(u64_to_i64),
propagation_channel_id: self.propagation_channel_id.map(u64_to_i64),
replying_to_message_id: self.replying_to_message_id.map(u64_to_i64),
// You can't create a deleted message, this is always going to be false when
// initializing
is_deleted: false,
is_edited: message.edited_timestamp.is_some(),
is_censored: self.is_censored,
};
Ok(Message {
call_id,
message_id: message.id.get(),
author_id: profile.id,
propagation_message_id: self.propagation_message_id,
data,
})
}
}
#[derive(Debug, Clone)]
pub struct Message {
pub call_id: u64,
pub message_id: u64,
pub author_id: u64,
pub propagation_message_id: Option<u64>,
pub data: MessageData,
}
impl<'a> Object<'a> for Message {
type Row = (i64, i64, i64, Option<i64>, MessageData);
fn from_row(row: Self::Row) -> Self {
Self {
call_id: i64_to_u64(row.0),
message_id: i64_to_u64(row.1),
author_id: i64_to_u64(row.2),
propagation_message_id: row.3.map(i64_to_u64),
data: row.4,
}
}
fn to_row(value: Self) -> Self::Row {
(
u64_to_i64(value.call_id),
u64_to_i64(value.message_id),
u64_to_i64(value.author_id),
value.propagation_message_id.map(u64_to_i64),
value.data,
)
}
}
impl Message {
pub fn build() -> MessageBuilder {
MessageBuilder::new()
}
pub async fn get_by_call_and_message_id(
call_id: u64,
message_id: u64,
session: &Session,
) -> Result<Option<Self>, DatabaseError> {
let call_id = u64_to_i64(call_id);
let message_id_i64 = u64_to_i64(message_id);
let result = session
.query_unpaged(
r#"
SELECT
call_id,
message_id,
author_id,
propagation_message_id,
data
FROM
payphone.messages
WHERE
call_id = ? AND message_id = ?
LIMIT 1;
"#,
(call_id, message_id_i64),
)
.await
.map_err(DatabaseError::QueryError)?
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
if result.rows_num() == 0 {
let message_by_propagation_id =
MessageByPropagationId::get_by_propagation_message_id(message_id, session).await?;
if let Some(message_by_propagation_id) = message_by_propagation_id {
let result = session
.query_unpaged(
r#"
SELECT
call_id,
message_id,
author_id,
propagation_message_id,
data
FROM
payphone.messages
WHERE
call_id = ? AND message_id = ?
LIMIT 1;
"#,
(call_id, u64_to_i64(message_by_propagation_id.message_id)),
)
.await
.map_err(DatabaseError::QueryError)?
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
return get_single_row_from_result(&result).await;
}
}
get_single_row_from_result(&result).await
}
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
r#"
INSERT INTO payphone.messages (
call_id,
message_id,
author_id,
propagation_message_id,
data
) VALUES (?, ?, ?, ?, ?);
"#,
Self::to_row(self.clone()),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
}
pub struct MessageByPropagationId {
pub call_id: u64,
pub message_id: u64,
pub propagation_message_id: u64,
}
impl<'a> Object<'a> for MessageByPropagationId {
type Row = (i64, i64, i64);
fn from_row(row: Self::Row) -> Self {
Self {
call_id: i64_to_u64(row.0),
message_id: i64_to_u64(row.1),
propagation_message_id: i64_to_u64(row.2),
}
}
fn to_row(value: Self) -> Self::Row {
(
u64_to_i64(value.call_id),
u64_to_i64(value.message_id),
u64_to_i64(value.propagation_message_id),
)
}
}
impl MessageByPropagationId {
pub async fn get_by_propagation_message_id(
propagation_message_id: u64,
session: &Session,
) -> Result<Option<Self>, DatabaseError> {
let propagation_message_id = u64_to_i64(propagation_message_id);
let result = session
.query_unpaged(
r#"
SELECT
call_id,
message_id,
propagation_message_id
FROM
payphone.messages_by_propagation_id
WHERE
propagation_message_id = ?
LIMIT 1;
"#,
(propagation_message_id,),
)
.await
.map_err(DatabaseError::QueryError)?
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
get_single_row_from_result(&result).await
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReportResolution {
Ban,
Warn,
Skip,
}
#[derive(Debug, Clone)]
pub struct Report {
pub id: u64,
pub user_id: u64,
pub message: MessageData,
pub replying_to: Option<MessageData>,
pub created_at: UtcDateTime,
pub reviewed_at: Option<UtcDateTime>,
pub created_by_ids: Vec<u64>,
pub reviewed_by_id: Option<u64>,
pub resolution: Option<ReportResolution>,
}
impl<'a> Object<'a> for Report {
type Row = (
i64,
i64,
MessageData,
Option<MessageData>,
OffsetDateTime,
Option<OffsetDateTime>,
Vec<i64>,
Option<i64>,
Option<String>,
);
fn from_row(row: Self::Row) -> Self {
Self {
id: i64_to_u64(row.0),
user_id: i64_to_u64(row.1),
message: row.2,
replying_to: row.3,
created_at: row.4.to_utc(),
reviewed_at: row.5.map(|i| i.to_utc()),
created_by_ids: row.6.into_iter().map(i64_to_u64).collect(),
reviewed_by_id: row.7.map(i64_to_u64),
resolution: row.8.map(|r| match r.as_str() {
"ban" => ReportResolution::Ban,
"warn" => ReportResolution::Warn,
"skip" => ReportResolution::Skip,
_ => unreachable!(),
}),
}
}
fn to_row(value: Self) -> Self::Row {
(
u64_to_i64(value.id),
u64_to_i64(value.user_id),
value.message,
value.replying_to,
value.created_at.to_offset(UtcOffset::UTC),
value.reviewed_at.map(|i| i.to_offset(UtcOffset::UTC)),
value.created_by_ids.into_iter().map(u64_to_i64).collect(),
value.reviewed_by_id.map(u64_to_i64),
value.resolution.map(|r| match r {
ReportResolution::Ban => "ban".into(),
ReportResolution::Warn => "warn".into(),
ReportResolution::Skip => "skip".into(),
}),
)
}
}
impl Report {
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
r#"
INSERT INTO payphone.reports (
report_id,
user_id,
message,
replying_to,
created_at,
reviewed_at,
created_by_ids,
reviewed_by_id,
resolution
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
"#,
Self::to_row(self.clone()),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserIncidentType {
Warning,
Ban,
}
#[derive(Debug, Clone)]
pub struct UserIncident {
pub user_id: u64,
pub incident_id: u64,
pub report_id: Option<u64>,
pub r#type: UserIncidentType,
pub message: Option<MessageData>,
pub reason: String,
pub created_by_id: Option<u64>,
pub created_at: UtcDateTime,
pub expires_at: Option<UtcDateTime>,
pub duration: Option<String>,
}
impl<'a> Object<'a> for UserIncident {
type Row = (
i64,
i64,
Option<i64>,
String,
Option<MessageData>,
String,
Option<i64>,
OffsetDateTime,
Option<OffsetDateTime>,
Option<String>,
);
fn from_row(row: Self::Row) -> Self {
Self {
user_id: i64_to_u64(row.0),
incident_id: i64_to_u64(row.1),
report_id: row.2.map(i64_to_u64),
r#type: match row.3.as_str() {
"warning" => UserIncidentType::Warning,
"ban" => UserIncidentType::Ban,
_ => unreachable!(),
},
message: row.4,
reason: row.5,
created_by_id: row.6.map(i64_to_u64),
created_at: row.7.to_utc(),
expires_at: row.8.map(|i| i.to_utc()),
duration: row.9,
}
}
fn to_row(value: Self) -> Self::Row {
(
u64_to_i64(value.user_id),
u64_to_i64(value.incident_id),
value.report_id.map(u64_to_i64),
match value.r#type {
UserIncidentType::Warning => "warning".into(),
UserIncidentType::Ban => "ban".into(),
},
value.message,
value.reason,
value.created_by_id.map(u64_to_i64),
value.created_at.to_offset(UtcOffset::UTC),
value.expires_at.map(|i| i.to_offset(UtcOffset::UTC)),
value.duration,
)
}
}
impl UserIncident {
pub fn is_active(&self) -> bool {
self.expires_at
.map(|e| e > UtcDateTime::now())
.unwrap_or(true)
&& self.r#type == UserIncidentType::Ban
}
pub async fn get_for_user(user_id: u64, session: &Session) -> Result<Vec<Self>, DatabaseError> {
let user_id = u64_to_i64(user_id);
let result = session
.query_unpaged(
r#"
SELECT
user_id,
incident_id,
report_id,
type,
message,
reason,
created_by_id,
created_at,
expires_at,
duration
FROM
payphone.user_incidents
WHERE
user_id = ?;
"#,
(user_id,),
)
.await
.map_err(DatabaseError::QueryError)?
.into_rows_result()
.map_err(|_| DatabaseError::ResultNotRows)?;
Ok(result
.rows::<<Self as Object>::Row>()
.map_err(|_| DatabaseError::ResultNotRows)?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| DatabaseError::DeserializationFailed)?
.into_iter()
.map(Self::from_row)
.collect::<Vec<_>>())
}
pub async fn insert(&self, session: &Session) -> Result<(), DatabaseError> {
session
.query_unpaged(
r#"
INSERT INTO payphone.user_incidents (
user_id,
incident_id,
report_id,
type,
message,
reason,
created_by_id,
created_at,
expires_at,
duration
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#,
Self::to_row(self.clone()),
)
.await
.map_err(DatabaseError::QueryError)?;
Ok(())
}
}