Skip to content

Commit

Permalink
Merge pull request #1692 from scpwiki/WJ-1200-site-user
Browse files Browse the repository at this point in the history
[WJ-1200] Add "site users"
  • Loading branch information
emmiegit authored Nov 9, 2023
2 parents 4bc5d71 + 008bb95 commit 25cd6ee
Show file tree
Hide file tree
Showing 12 changed files with 318 additions and 35 deletions.
3 changes: 2 additions & 1 deletion deepwell/migrations/20220906103252_deepwell.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
CREATE TYPE user_type AS ENUM (
'regular',
'system',
'site',
'bot'
);

Expand All @@ -25,7 +26,7 @@ CREATE TABLE "user" (
slug TEXT NOT NULL,
name_changes_left SMALLINT NOT NULL, -- Default set in runtime configuration.
last_renamed_at TIMESTAMP WITH TIME ZONE,
email TEXT NOT NULL,
email TEXT NOT NULL, -- Can be empty, for instance with system accounts.
email_is_alias BOOLEAN,
email_verified_at TIMESTAMP WITH TIME ZONE,
password TEXT NOT NULL,
Expand Down
7 changes: 5 additions & 2 deletions deepwell/src/database/seeder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ pub async fn seed(state: &ServerState) -> Result<()> {
{
info!("Creating seed site '{}' (slug {})", site.name, site.slug);

let CreateSiteOutput { site_id, slug: _ } = SiteService::create(
let CreateSiteOutput { site_id, .. } = SiteService::create(
&ctx,
CreateSite {
slug: site.slug,
Expand Down Expand Up @@ -176,7 +176,7 @@ pub async fn seed(state: &ServerState) -> Result<()> {
for page in pages {
info!("Creating page '{}' (slug {})", page.title, page.slug);

PageService::create(
let model = PageService::create(
&ctx,
CreatePage {
site_id,
Expand All @@ -190,6 +190,9 @@ pub async fn seed(state: &ServerState) -> Result<()> {
},
)
.await?;

// TODO add attribution with site_user as author
let _ = model;
}
}

Expand Down
2 changes: 2 additions & 0 deletions deepwell/src/models/sea_orm_active_enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ pub enum UserType {
Bot,
#[sea_orm(string_value = "regular")]
Regular,
#[sea_orm(string_value = "site")]
Site,
#[sea_orm(string_value = "system")]
System,
}
4 changes: 4 additions & 0 deletions deepwell/src/services/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ pub enum Error {
#[error("User slug cannot be empty")]
UserSlugEmpty,

#[error("User email cannot be empty")]
UserEmailEmpty,

#[error("Message subject cannot be empty")]
MessageSubjectEmpty,

Expand Down Expand Up @@ -362,6 +365,7 @@ impl Error {
Error::SiteSlugEmpty => 4013,
Error::UserNameTooShort => 4014,
Error::UserSlugEmpty => 4015,
Error::UserEmailEmpty => 4022,
Error::MessageSubjectEmpty => 4016,
Error::MessageSubjectTooLong => 4017,
Error::MessageBodyEmpty => 4018,
Expand Down
11 changes: 11 additions & 0 deletions deepwell/src/services/interaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ mod page_star;
mod page_watch;
mod site_ban;
mod site_member;
mod site_user;
mod structs;
mod user_block;
mod user_contact;
Expand All @@ -53,6 +54,7 @@ pub use self::page_star::*;
pub use self::page_watch::*;
pub use self::site_ban::*;
pub use self::site_member::*;
pub use self::site_user::*;
pub use self::structs::*;
pub use self::user_block::*;
pub use self::user_contact::*;
Expand Down Expand Up @@ -158,6 +160,7 @@ impl InteractionService {
.filter(
Condition::all()
.add(reference.condition())
.add(interaction::Column::OverwrittenAt.is_null())
.add(interaction::Column::DeletedAt.is_null()),
)
.one(txn)
Expand Down Expand Up @@ -199,6 +202,10 @@ impl InteractionService {
}

// TODO paginate
/// Gets the history of this `dest` / `from` interaction.
///
/// This includes all all edits of the interaction (`overwritten_at`)
/// and deleted / remade versions of the interaction (`deleted_at`).
pub async fn get_history(
ctx: &ServiceContext<'_>,
interaction_type: InteractionType,
Expand All @@ -220,6 +227,10 @@ impl InteractionService {
}

// TODO paginate
/// Gets all interactions from the starting object in the given direction.
///
/// For instance, this can be used to get all blocked users, or all users who are blocking
/// someone depending on the `InteractionDirection`.
pub async fn get_entries(
ctx: &ServiceContext<'_>,
interaction_type: InteractionType,
Expand Down
161 changes: 161 additions & 0 deletions deepwell/src/services/interaction/site_user.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* services/interaction/site_user.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/>.
*/

//! Governs the interaction which tracks "site users".
//!
//! These are special users (of type `site`) which represent a site as a whole.
//! They can be messaged to send messages to staff, and can be utilized to send
//! messages on behalf of a site (for instance, a ban notification).
//!
//! This interaction describes which site a site-user corresponds to.
//! As such, it is an invariant that all users linked here are of the type `site`.
use super::prelude::*;
use crate::models::sea_orm_active_enums::UserType;
use crate::services::UserService;

impl_interaction!(SiteUser, Site, site_id, User, user_id, (), NO_CREATE_IMPL,);

impl InteractionService {
pub async fn create_site_user(
ctx: &ServiceContext<'_>,
CreateSiteUser {
site_id,
user_id,
metadata: (),
created_by,
}: CreateSiteUser,
) -> Result<()> {
// User to be added must of type 'site'
let user = UserService::get(ctx, Reference::Id(user_id)).await?;
if user.user_type != UserType::Site {
error!(
"Can only create site user interactions if the user is of type 'site', not {:?}",
user.user_type,
);
return Err(Error::BadRequest);
}

// Site <--> User must be 1:1
//
// This means there should be no results for both
// this site_id -> anything and this user_id -> anything.

let sites = InteractionService::get_entries(
ctx,
InteractionType::SiteUser,
InteractionObject::Site(site_id),
InteractionDirection::Dest,
)
.await?;

if !sites.is_empty() {
error!("Found a different interaction with this site, cannot create interaction: {sites:?}");
return Err(Error::BadRequest);
}

let users = InteractionService::get_entries(
ctx,
InteractionType::SiteUser,
InteractionObject::User(user_id),
InteractionDirection::From,
)
.await?;

if !users.is_empty() {
error!("Found a different interaction with this user, cannot create interaction: {users:?}");
return Err(Error::BadRequest);
}

// Checks done, create
create_operation!(
ctx,
SiteMember,
Site,
site_id,
User,
user_id,
created_by,
&()
)
}

pub async fn get_site_user_id_for_site(
ctx: &ServiceContext<'_>,
site_id: i64,
) -> Result<i64> {
info!("Getting site user for site ID {site_id}");

let model = get_interaction(
ctx,
Condition::all()
.add(interaction::Column::DestType.eq(InteractionObjectType::Site))
.add(interaction::Column::DestId.eq(site_id)),
)
.await?;

Ok(model.from_id)
}

pub async fn get_site_id_for_site_user(
ctx: &ServiceContext<'_>,
user_id: i64,
) -> Result<i64> {
let model = get_interaction(
ctx,
Condition::all()
.add(interaction::Column::FromType.eq(InteractionObjectType::User))
.add(interaction::Column::FromId.eq(user_id)),
)
.await?;

Ok(model.dest_id)
}
}

async fn get_interaction(
ctx: &ServiceContext<'_>,
condition: Condition,
) -> Result<InteractionModel> {
// We implement our own query since it's 1:1 and we
// don't have to worry about multiple results like
// for get_entries().

let txn = ctx.transaction();
let model = Interaction::find()
.filter(
Condition::all()
.add(
interaction::Column::InteractionType
.eq(InteractionType::SiteUser.value()),
)
.add(condition)
.add(interaction::Column::OverwrittenAt.is_null())
.add(interaction::Column::DeletedAt.is_null()),
)
.order_by_asc(interaction::Column::CreatedAt)
.one(txn)
.await?;

match model {
Some(model) => Ok(model),
None => Err(Error::InteractionNotFound),
}
}
3 changes: 3 additions & 0 deletions deepwell/src/services/interaction/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pub enum InteractionDirection {

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum InteractionType {
SiteUser,
SiteBan,
SiteMember,
PageStar,
Expand All @@ -145,6 +146,7 @@ impl InteractionType {
/// of `(dest_type, from_type, interaction_value)`.
pub fn value(self) -> &'static str {
match self {
InteractionType::SiteUser => "site-user",
InteractionType::SiteBan => "ban",
InteractionType::SiteMember => "member",
InteractionType::PageStar => "star",
Expand All @@ -167,6 +169,7 @@ impl InteractionType {
}

match self {
InteractionType::SiteUser => t!(Site, User),
InteractionType::SiteBan => t!(Site, User),
InteractionType::SiteMember => t!(Site, User),
InteractionType::PageStar => t!(Page, User),
Expand Down
21 changes: 19 additions & 2 deletions deepwell/src/services/message/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use crate::models::message_recipient::{self, Entity as MessageRecipient};
use crate::models::message_record::{
self, Entity as MessageRecord, Model as MessageRecordModel,
};
use crate::models::sea_orm_active_enums::MessageRecipientType;
use crate::models::sea_orm_active_enums::{MessageRecipientType, UserType};
use crate::services::render::{RenderOutput, RenderService};
use crate::services::{InteractionService, TextService, UserService};
use cuid2::cuid;
Expand Down Expand Up @@ -208,7 +208,7 @@ impl MessageService {
let config = ctx.config();
let draft = Self::get_draft(ctx, draft_id).await?;
let wikitext = TextService::get(ctx, &draft.wikitext_hash).await?;
let recipients: DraftRecipients = serde_json::from_value(draft.recipients)?;
let mut recipients: DraftRecipients = serde_json::from_value(draft.recipients)?;

// Message validation checks
if draft.subject.is_empty() {
Expand Down Expand Up @@ -253,15 +253,32 @@ impl MessageService {
return Err(Error::MessageTooManyRecipients);
}

let mut recipients_to_add = Vec::new();
for recipient_user_id in recipients.iter() {
// Ensure user is not blocked
InteractionService::check_user_block(
ctx,
draft.user_id,
recipient_user_id,
"send a direct message to",
)
.await?;

// If recipient is a site user, then forward to corresponding site staff.
let user = UserService::get(ctx, Reference::Id(recipient_user_id)).await?;
if user.user_type == UserType::Site {
// TODO what to do if user is banned from site? needs to be possible to block
// permabanned bad actors, but also allow normal banned users to message
// to appeal bans etc
// TODO get the listed site staff, add them to recipients
let _site_id =
InteractionService::get_site_id_for_site_user(ctx, user.user_id)
.await?;

let _ = &recipients_to_add;
}
}
recipients.carbon_copy.append(&mut recipients_to_add);

// The message sending process:
// * Insert message_draft row to message_record
Expand Down
Loading

0 comments on commit 25cd6ee

Please sign in to comment.