Skip to content

Commit

Permalink
Merge pull request #1633 from scpwiki/WJ-1161-messages
Browse files Browse the repository at this point in the history
[WJ-1161] [WJ-373] [WJ-770] [WJ-1128] Add message support
  • Loading branch information
emmiegit authored Oct 7, 2023
2 parents fc4ba7d + f1ca4cb commit c8667a5
Show file tree
Hide file tree
Showing 28 changed files with 1,557 additions and 11 deletions.
12 changes: 12 additions & 0 deletions deepwell/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,15 @@ minimum-name-bytes = 3
#
# Set to 0 to disable.
refill-name-change-days = 90

[message]

# The maximum size of a message's subject line, in bytes.
maximum-subject-bytes = 128

# The maximum size of a message wikitext, in bytes.
maximum-body-bytes = 200000

# The maximum number of recipients allowed in one message.
# This refers to the sum of direct recipients, CC, and BCC targets.
maximum-recipients = 6
98 changes: 98 additions & 0 deletions deepwell/migrations/20220906103252_deepwell.sql
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,104 @@ CREATE TABLE file_revision (
UNIQUE (file_id, page_id, revision_number)
);

--
-- Direct Messages
--

CREATE TYPE message_recipient_type AS ENUM (
'regular',
'cc',
'bcc'
);

-- A "record" is the underlying message data, with its contents, attachments,
-- and associated metadata such as sender and recipient(s).
CREATE TABLE message_record (
external_id TEXT PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
drafted_at TIMESTAMP WITH TIME ZONE NOT NULL,
retracted_at TIMESTAMP WITH TIME ZONE,
sender_id BIGINT NOT NULL REFERENCES "user"(user_id),

-- Text contents
subject TEXT NOT NULL,
wikitext_hash BYTEA NOT NULL REFERENCES text(hash),
compiled_hash BYTEA NOT NULL REFERENCES text(hash),
compiled_at TIMESTAMP WITH TIME ZONE NOT NULL,
compiled_generator TEXT NOT NULL,

-- Flags
reply_to TEXT REFERENCES message_record(external_id),
forwarded_from TEXT REFERENCES message_record(external_id),

CHECK (length(external_id) = 24) -- default length for a cuid2
);

-- A "message" is a particular copy of a record.
-- If Alice sends Bob a message and CC's Charlie, then
-- there is one message_record and three message rows
-- (one for each recipient, including the sender's "Sent" folder).
CREATE TABLE message (
internal_id BIGSERIAL PRIMARY KEY,
record_id TEXT NOT NULL REFERENCES message_record(external_id), -- The record this corresponds to
user_id BIGINT NOT NULL REFERENCES "user"(user_id), -- The user who owns the copy of this record

-- Folders and flags
flag_read BOOLEAN NOT NULL DEFAULT false, -- A user-toggleable flag for the "unread" status.
flag_inbox BOOLEAN NOT NULL,
flag_outbox BOOLEAN NOT NULL,
flag_self BOOLEAN NOT NULL, -- Messages sent to oneself, as a kind of "notes to self" section.
flag_trash BOOLEAN NOT NULL DEFAULT false,
flag_star BOOLEAN NOT NULL DEFAULT false,

-- User-customizable tagging
tags TEXT[] NOT NULL DEFAULT '{}',

UNIQUE (record_id, user_id),
CHECK (NOT (flag_self AND flag_inbox)) -- If something is sent to oneself, it cannot be in the inbox
);

CREATE TABLE message_recipient (
record_id TEXT NOT NULL REFERENCES message_record(external_id),
recipient_id BIGINT NOT NULL REFERENCES "user"(user_id),
recipient_type message_recipient_type NOT NULL,

PRIMARY KEY (record_id, recipient_id, recipient_type)
);

CREATE TABLE message_draft (
external_id TEXT PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE,
user_id BIGINT NOT NULL REFERENCES "user"(user_id),
recipients JSON NOT NULL,

-- Text contents
subject TEXT NOT NULL,
wikitext_hash BYTEA NOT NULL REFERENCES text(hash),
compiled_hash BYTEA NOT NULL REFERENCES text(hash),
compiled_at TIMESTAMP WITH TIME ZONE NOT NULL,
compiled_generator TEXT NOT NULL,

-- Flags
reply_to TEXT REFERENCES message_record(external_id),
forwarded_from TEXT REFERENCES message_record(external_id),

CHECK (length(external_id) = 24) -- default length for a cuid2
);

-- If a message has been reported, then a row for it is created here.
-- Messages can be reported per-site or globally (at the platform level).
CREATE TABLE message_report (
message_id BIGINT NOT NULL REFERENCES message(internal_id),
reported_to_site_id BIGINT REFERENCES site(site_id),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
updated_at TIMESTAMP WITH TIME ZONE,
reason TEXT NOT NULL,

PRIMARY KEY (message_id, reported_to_site_id)
);

--
-- Filters
--
Expand Down
13 changes: 10 additions & 3 deletions deepwell/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ use crate::config::{Config, Secrets};
use crate::database;
use crate::endpoints::{
auth::*, category::*, email::*, file::*, file_revision::*, link::*, locale::*,
misc::*, page::*, page_revision::*, parent::*, site::*, site_member::*, text::*,
user::*, user_bot::*, view::*, vote::*,
message::*, misc::*, page::*, page_revision::*, parent::*, site::*, site_member::*,
text::*, user::*, user_bot::*, view::*, vote::*,
};
use crate::locales::Localizations;
use crate::services::blob::spawn_magic_thread;
Expand Down Expand Up @@ -133,7 +133,7 @@ fn build_routes(mut app: ApiServer) -> ApiServer {

// Localization
app.at("/locale/:locale").get(locale_get);
app.at("/message/:locale/translate").put(translate_put);
app.at("/translate/:locale").put(translate_put);

// Routes for web server
app.at("/view/page").put(view_page);
Expand Down Expand Up @@ -241,6 +241,13 @@ fn build_routes(mut app: ApiServer) -> ApiServer {
.put(user_bot_owner_put)
.delete(user_bot_owner_delete);

// Message
app.at("/message/draft")
.post(message_draft_create)
.put(message_draft_update)
.delete(message_draft_delete);
app.at("/message").post(message_draft_send);

// Email
app.at("/email/validate").put(validate_email);

Expand Down
18 changes: 18 additions & 0 deletions deepwell/src/config/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub struct ConfigFile {
ftml: Ftml,
special_pages: SpecialPages,
user: User,
message: Message,
}

/// Structure containing extra fields not found in `ConfigFile`.
Expand Down Expand Up @@ -157,6 +158,14 @@ struct User {
minimum_name_bytes: usize,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "kebab-case")]
struct Message {
maximum_subject_bytes: usize,
maximum_body_bytes: usize,
maximum_recipients: usize,
}

impl ConfigFile {
pub fn load(path: PathBuf) -> Result<(Self, ExtraConfig)> {
// Read TOML
Expand Down Expand Up @@ -257,6 +266,12 @@ impl ConfigFile {
refill_name_change_days,
minimum_name_bytes,
},
message:
Message {
maximum_subject_bytes: maximum_message_subject_bytes,
maximum_body_bytes: maximum_message_body_bytes,
maximum_recipients: maximum_message_recipients,
},
} = self;

// Prefix domains with '.' so we can do easy subdomain checks
Expand Down Expand Up @@ -320,6 +335,9 @@ impl ConfigFile {
refill_name_change_days * 24 * 60 * 60,
),
minimum_name_bytes,
maximum_message_subject_bytes,
maximum_message_body_bytes,
maximum_message_recipients,
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions deepwell/src/config/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ pub struct Config {

/// Minimum length of bytes in a username.
pub minimum_name_bytes: usize,

/// Maximum size of the subject line allowed in a direct message.
pub maximum_message_subject_bytes: usize,

/// Maximum size of the wikitext body allowed in a direct message.
pub maximum_message_body_bytes: usize,

/// Maximum number of total recipients allowed in a direct message.
pub maximum_message_recipients: usize,
}

impl Config {
Expand Down
75 changes: 75 additions & 0 deletions deepwell/src/endpoints/message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* endpoints/message.rs
*
* DEEPWELL - Wikijump API provider and database manager
* Copyright (C) 2019-2023 Wikijump Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

use super::prelude::*;
use crate::services::message::{
CreateMessageDraft, DeleteMessageDraft, SendMessageDraft, UpdateMessageDraft,
};

pub async fn message_draft_create(mut req: ApiRequest) -> ApiResponse {
let txn = req.database().begin().await?;
let ctx = ServiceContext::new(&req, &txn);

let input: CreateMessageDraft = req.body_json().await?;
tide::log::info!("Creating new message draft for user ID {}", input.user_id);

let output = MessageService::create_draft(&ctx, input).await?;
txn.commit().await?;
build_json_response(&output, StatusCode::Ok)
}

pub async fn message_draft_update(mut req: ApiRequest) -> ApiResponse {
let txn = req.database().begin().await?;
let ctx = ServiceContext::new(&req, &txn);

let input: UpdateMessageDraft = req.body_json().await?;
tide::log::info!(
"Updating message draft for draft ID {}",
input.message_draft_id
);

let output = MessageService::update_draft(&ctx, input).await?;
txn.commit().await?;
build_json_response(&output, StatusCode::Ok)
}

pub async fn message_draft_send(mut req: ApiRequest) -> ApiResponse {
let txn = req.database().begin().await?;
let ctx = ServiceContext::new(&req, &txn);

let SendMessageDraft { message_draft_id } = req.body_json().await?;
tide::log::info!("Sending message draft with ID {message_draft_id}");

let output = MessageService::send(&ctx, &message_draft_id).await?;
txn.commit().await?;
build_json_response(&output, StatusCode::Ok)
}

pub async fn message_draft_delete(mut req: ApiRequest) -> ApiResponse {
let txn = req.database().begin().await?;
let ctx = ServiceContext::new(&req, &txn);

let DeleteMessageDraft { message_draft_id } = req.body_json().await?;
tide::log::info!("Deleting message draft with ID {message_draft_id}");

MessageService::delete_draft(&ctx, message_draft_id).await?;
txn.commit().await?;
Ok(Response::new(StatusCode::Ok))
}
10 changes: 6 additions & 4 deletions deepwell/src/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ mod prelude {
pub use crate::api::{ApiRequest, ApiResponse};
pub use crate::services::{
AliasService, BlobService, CategoryService, DomainService, Error as ServiceError,
FileRevisionService, FileService, InteractionService, LinkService, MfaService,
PageRevisionService, PageService, ParentService, RenderService,
RequestFetchService, ScoreService, ServiceContext, SessionService, SiteService,
TextService, UserService, ViewService, VoteService,
FileRevisionService, FileService, InteractionService, LinkService,
MessageReportService, MessageService, MfaService, PageRevisionService,
PageService, ParentService, RenderService, RequestFetchService, ScoreService,
ServiceContext, SessionService, SiteService, TextService, UserService,
ViewService, VoteService,
};
pub use crate::utils::error_response;
pub use crate::web::HttpUnwrap;
Expand All @@ -61,6 +62,7 @@ pub mod file;
pub mod file_revision;
pub mod link;
pub mod locale;
pub mod message;
pub mod misc;
pub mod page;
pub mod page_revision;
Expand Down
5 changes: 1 addition & 4 deletions deepwell/src/endpoints/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ pub async fn page_create(mut req: ApiRequest) -> ApiResponse {
tide::log::info!("Creating new page in site ID {}", input.site_id);

let output = PageService::create(&ctx, input).await?;
let body = Body::from_json(&output)?;
txn.commit().await?;

Ok(body.into())
build_json_response(&output, StatusCode::Ok)
}

pub async fn page_retrieve(mut req: ApiRequest) -> ApiResponse {
Expand Down
Loading

0 comments on commit c8667a5

Please sign in to comment.