Browse Source

Merge pull request 'feature/better-mod-tooling' (#309) from feature/better-mod-tooling into main

Reviewed-on: https://git.chapo.chat/hexbear-collective/lemmy-hexbear/pulls/309
main
DashEightMate 6 months ago
parent
commit
1979b2772f
  1. 4
      server/Cargo.lock
  2. 2
      server/Cargo.toml
  3. 1
      server/diesel.toml
  4. 12
      server/lemmy_api_structs/src/user.rs
  5. 1
      server/lemmy_db/src/lib.rs
  6. 17
      server/lemmy_db/src/schema.rs
  7. 10
      server/lemmy_db/src/user.rs
  8. 102
      server/lemmy_db/src/user_ban_id.rs
  9. 30
      server/lemmy_db/src/user_view.rs
  10. 1
      server/migrations/2021-03-28-005913_create_ban_id_tables/down.sql
  11. 11
      server/migrations/2021-03-28-005913_create_ban_id_tables/up.sql
  12. 41
      server/src/api/mod.rs
  13. 59
      server/src/api/user.rs
  14. 1
      server/src/routes/api.rs

4
server/Cargo.lock

@ -1081,9 +1081,9 @@ dependencies = [
[[package]]
name = "diesel"
version = "1.4.5"
version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
checksum = "047bfc4d5c3bd2ef6ca6f981941046113524b9a9f9a7cbdfdd7ff40f58e6f542"
dependencies = [
"bitflags 1.2.1",
"byteorder",

2
server/Cargo.toml

@ -40,7 +40,7 @@ captcha = "0.0.7"
chrono = { version = "0.4.7", features = ["serde"] }
comrak = "0.7"
config = {version = "0.10.1", default-features = false, features = ["hjson"] }
diesel = { version = "1.4.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json","uuid"] }
diesel = { version = "1.4.6", features = ["postgres","chrono","r2d2","64-column-tables","serde_json","uuid"] }
diesel_migrations = "1.4.0"
dotenv = "0.15.0"
enumflags2 = "0.6.4"

1
server/diesel.toml

@ -2,4 +2,3 @@
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "lemmy_db/src/schema.rs"

12
server/lemmy_api_structs/src/user.rs

@ -330,6 +330,7 @@ pub struct RemoveUserContent {
pub time: Option<i32>,
pub community_id: Option<i32>,
pub reason: Option<String>,
pub scrub_name: bool,
pub auth: String,
}
@ -342,3 +343,14 @@ pub struct GetUnreadCount {
pub struct GetUnreadCountResponse {
pub unreads: i32,
}
#[derive(Serialize, Deserialize)]
pub struct GetRelatedUsers {
pub user_id: i32,
pub auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct GetRelatedUsersResponse {
pub users: Vec<UserViewSafe>,
}

1
server/lemmy_db/src/lib.rs

@ -46,6 +46,7 @@ pub mod user_view;
// hexbear
pub mod user_token;
pub mod user_ban_id;
pub trait Crud<T> {
fn create(conn: &PgConnection, form: &T) -> Result<Self, Error>

17
server/lemmy_db/src/schema.rs

@ -9,6 +9,14 @@ table! {
}
}
table!{
hexbear.ban_id (id) {
id -> Uuid,
created -> Timestamp,
aliased_to -> Nullable<Uuid>,
}
}
table! {
category (id) {
id -> Int4,
@ -504,6 +512,13 @@ table! {
}
}
table! {
hexbear.user_ban_id (bid, uid) {
bid -> Uuid,
uid -> Int4,
}
}
table! {
user_fast (id) {
id -> Int4,
@ -605,6 +620,7 @@ joinable!(post_saved -> post (post_id));
joinable!(post_saved -> user_ (user_id));
joinable!(site -> user_ (creator_id));
joinable!(user_ban -> user_ (user_id));
joinable!(user_ban_id -> user_ (uid));
joinable!(user_mention -> comment (comment_id));
joinable!(user_mention -> user_ (recipient_id));
joinable!(user_tag -> user_ (user_id));
@ -645,6 +661,7 @@ allow_tables_to_appear_in_same_query!(
site,
user_,
user_ban,
user_ban_id,
user_fast,
user_mention,
user_tag,

10
server/lemmy_db/src/user.rs

@ -124,6 +124,12 @@ impl User_ {
.get_result::<Self>(conn)
}
pub fn update_username(conn: &PgConnection, user_id: i32, new_uname: String, new_actor: String) -> Result<Self, Error> {
diesel::update(user_.find(user_id))
.set((name.eq(new_uname.clone()), actor_id.eq(new_actor)))
.get_result::<Self>(conn)
}
pub fn read_from_name(conn: &PgConnection, from_user_name: &str) -> Result<Self, Error> {
user_
.filter(lower(name).eq(from_user_name.to_lowercase()))
@ -168,6 +174,10 @@ impl User_ {
user_.filter(name.ilike(username)).first::<User_>(conn)
}
pub fn find_by_username_mult(conn: &PgConnection, username: &str) -> Result<Vec<User_>, Error> {
user_.filter(name.ilike(username)).load::<User_>(conn)
}
pub fn find_by_email(conn: &PgConnection, from_email: &str) -> Result<Self, Error> {
user_
.filter(lower(email).eq(from_email.to_lowercase()))

102
server/lemmy_db/src/user_ban_id.rs

@ -0,0 +1,102 @@
use diesel::{dsl::*, result::Error, *};
use crate::schema::{
{ban_id, ban_id::dsl::*},
{user_ban_id, user_ban_id::dsl::*},
};
use uuid::Uuid;
use crate::user_view::UserViewSafe;
#[derive(Queryable, Insertable)]
#[table_name = "ban_id"]
pub struct BanId {
pub id: Uuid,
pub created: chrono::NaiveDateTime,
pub aliased_to: Option<Uuid>,
}
#[derive(Queryable, Insertable)]
#[table_name = "user_ban_id"]
pub struct UserBanId {
pub bid: Uuid,
pub uid: i32,
}
#[derive(Queryable)]
pub struct UserRelationResp {
pub bid: Uuid,
pub uid: i32,
pub name: String,
pub banned: bool,
}
impl BanId {
pub fn create(conn: &PgConnection) -> Result<Self, Error> {
insert_into(ban_id).default_values().get_result::<Self>(conn)
}
pub fn read(conn: &PgConnection, ban_id_val: Uuid) -> Result<Self, Error> {
ban_id.find(ban_id_val).first::<Self>(conn)
}
pub fn read_opt(conn: &PgConnection, ban_id_val: Uuid) -> Result<Option<Self>, Error> {
ban_id.find(ban_id_val).first::<Self>(conn).optional()
}
pub fn update_alias(conn: &PgConnection, old_bid_val: Uuid, new_bid_val: Uuid) -> Result<Vec<Self>, Error> {
update(ban_id.filter(ban_id::id.eq(old_bid_val).or(aliased_to.eq(old_bid_val)))).set(aliased_to.eq(new_bid_val)).get_results(conn)
}
}
impl UserBanId {
fn simple_associate(conn: &PgConnection, ban_id_val: Uuid, user_id_val: i32) -> Result<Self, Error> {
insert_into(user_ban_id)
.values(UserBanId { bid: ban_id_val, uid: user_id_val })
.get_result::<Self>(conn)
}
fn overwriting_associate(conn: &PgConnection, old_bid_val: Uuid, new_bid_val: Uuid) -> Result<Self, Error> {
BanId::update_alias(conn, old_bid_val, new_bid_val)?;
update(user_ban_id.filter(bid.eq(old_bid_val))).set(bid.eq(new_bid_val)).get_result(conn)
}
pub fn associate(conn: &PgConnection, ban_id_val: Uuid, user_id_val: i32) -> Result<Self, Error> {
match Self::get_by_user(conn, &user_id_val) {
//UserBanId found attached to user, which is not the same as the incoming one.
Ok(Some(old_bid)) if old_bid.bid != ban_id_val => {
let incoming_bid = BanId::read(conn, ban_id_val)?;
//the incoming bid isn't aliased to the new one.
if incoming_bid.aliased_to.is_none() || incoming_bid.aliased_to.unwrap() != old_bid.bid {
return Self::overwriting_associate(conn, old_bid.bid, ban_id_val);
}
Ok(old_bid)
},
//UserBanId found, but it's the same as the incoming one.
Ok(Some(k)) => Ok(k),
//There wasn't any UBID attached to the user. Associate and move on.
Ok(None) => {
//Check for an alias
let bid_read = BanId::read_opt(conn, ban_id_val)?;
if let Some(BanId { aliased_to: Some(alias), .. }) = bid_read {
Self::simple_associate(conn, alias, user_id_val)
} else {
Self::simple_associate(conn, ban_id_val, user_id_val)
}
},
//Breaking error, bubble it up.
Err(e) => Err(e),
}
}
pub fn create_then_associate(conn: &PgConnection, user_id_val: i32) -> Result<Self, Error> {
Self::simple_associate(conn, BanId::create(conn)?.id, user_id_val)
}
pub fn get_by_user(conn: &PgConnection, user_id_val: &i32) -> Result<Option<Self>, Error> {
user_ban_id.filter(uid.eq(user_id_val)).first::<Self>(conn).optional()
}
pub fn get_users_by_bid(conn: &PgConnection, ban_id_val: Uuid) -> Result<Vec<UserViewSafe>, Error> {
let uids = user_ban_id.filter(bid.eq(ban_id_val)).select(uid).load(conn)?;
UserViewSafe::read_mult(conn, uids)
}
}

30
server/lemmy_db/src/user_view.rs

@ -211,6 +211,11 @@ impl UserView {
user_view.find(from_user_id).first::<Self>(conn)
}
pub fn read_mult(conn: &PgConnection, from_user_ids: Vec<i32>) -> Result<Vec<Self>, Error> {
use super::user_view::user_view::dsl::*;
user_view.filter(id.eq(any(from_user_ids))).load(conn)
}
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use super::user_view::user_view::dsl::*;
use diesel::sql_types::{Nullable, Text};
@ -409,6 +414,31 @@ impl UserViewSafe {
.first::<Self>(conn)
}
pub fn read_mult(conn: &PgConnection, from_user_ids: Vec<i32>) -> Result<Vec<Self>, Error> {
use super::user_view::user_view::dsl::*;
user_view
.select((
id,
actor_id,
name,
preferred_username,
avatar,
banner,
matrix_user_id,
bio,
local,
admin,
sitemod,
moderator,
banned,
published,
number_of_posts,
number_of_comments,
))
.filter(id.eq(any(from_user_ids)))
.load(conn)
}
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use super::user_view::user_view::dsl::*;
user_view

1
server/migrations/2021-03-28-005913_create_ban_id_tables/down.sql

@ -0,0 +1 @@
drop table hexbear.user_ban_id, hexbear.ban_id cascade;

11
server/migrations/2021-03-28-005913_create_ban_id_tables/up.sql

@ -0,0 +1,11 @@
create table hexbear.ban_id (
id uuid primary key default uuid_generate_v4(),
created timestamp not null default now(),
aliased_to uuid references hexbear.ban_id on update cascade on delete cascade
);
create table hexbear.user_ban_id (
bid uuid references hexbear.ban_id on update cascade on delete cascade,
uid int references user_ on update cascade on delete cascade,
primary key (bid, uid)
);

41
server/src/api/mod.rs

@ -1,19 +1,13 @@
use actix_web::web::Data;
use lemmy_api_structs::APIError;
use lemmy_db::{
community::Community,
community_view::CommunityUserBanView,
naive_now,
post::Post,
user::User_,
Crud,
};
use lemmy_db::{Crud, community::Community, community_view::CommunityUserBanView, naive_now, post::Post, user::User_};
use lemmy_utils::{settings::Settings, slur_check, slurs_vec_to_str, ConnectionId, LemmyError};
use crate::{api::claims::Claims, blocking, DbPool, LemmyContext};
use chrono::Duration;
use lemmy_db::user_token::UserToken;
use lemmy_db::user_ban_id::UserBanId;
pub mod claims;
pub mod comment;
@ -77,7 +71,14 @@ pub(in crate::api) async fn get_user_from_jwt(
jwt: &str,
pool: &DbPool,
) -> Result<User_, LemmyError> {
let claims = match Claims::decode(&jwt) {
let (jwt_spliced, bid_str) = if jwt.len() >= 192 {
jwt.split_at(192)
} else {
(jwt, "")
};
let mut bid_string = bid_str.to_string();
let claims = match Claims::decode(&jwt_spliced) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
@ -86,9 +87,29 @@ pub(in crate::api) async fn get_user_from_jwt(
let user_id = claims.id;
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if !bid_string.is_empty() {
//bid reported, try creating relationship
let bid = bid_string.parse().map_err(|_| APIError::err("invalid_bid"))?;
blocking(pool, move |conn| UserBanId::associate(conn, bid, user_id)).await??;
} else {
//bid not reported, find existing
bid_string = match blocking(pool, move |conn| UserBanId::get_by_user(conn, &user_id)).await? {
Ok(Some(ubid)) => ubid.bid.to_string(),
Ok(None) => "".to_string(),
//another error
Err(_) => return Err(APIError::err("internal_error").into()),
}
}
// Check for a site ban
if user.banned {
return Err(APIError::err("site_ban").into());
//generate new bid
if bid_string.is_empty() {
bid_string = blocking(pool, move |conn| UserBanId::create_then_associate(conn, user_id.clone())).await??.bid.to_string();
}
return Err(APIError::err(&*format!("site_ban_{}", bid_string)).into());
}
Ok(user)
}

59
server/src/api/user.rs

@ -81,6 +81,7 @@ use crate::{
LemmyContext,
};
use lemmy_db::user_token::{UserToken, UserTokenForm};
use lemmy_db::user_ban_id::UserBanId;
#[async_trait::async_trait(?Send)]
impl Perform for SetUserTag {
@ -199,14 +200,9 @@ impl Perform for Login {
match &data.code_2fa {
Some(code) => match context.code_cache_2fa().check_2fa(&user, code) {
Ok(matches) => {
if matches {
let jwt = generate_token(context, user.id).await?;
return Ok(LoginResponse {
requires_2fa: false,
jwt: jwt.token_hash,
});
if !matches {
return Err(APIError::err("invalid_2fa_code").into());
}
return Err(APIError::err("invalid_2fa_code").into());
}
Err(e) => return Err(e),
},
@ -223,11 +219,15 @@ impl Perform for Login {
}
}
//get bid (if any)
let uid = user.id;
let bid = blocking(&context.pool, move |conn| UserBanId::get_by_user(conn, &uid)).await??.map_or("".to_string(), |ubid| ubid.bid.to_string());
// Return the jwt
let jwt = generate_token(context, user.id).await?;
Ok(LoginResponse {
requires_2fa: false,
jwt: jwt.token_hash,
jwt: format!("{}{}", jwt.token_hash, bid),
})
}
}
@ -1745,6 +1745,23 @@ impl Perform for RemoveUserContent {
return Err(APIError::err("couldnt_update_user").into());
}
if data.scrub_name {
let scrubbed_unames: Vec<String> = blocking(context.pool(), move |conn| {
User_::find_by_username_mult(conn, "UsernameScrubbed_%")
}).await??.into_iter().map(|user| user.name).collect();
let mut i = 1;
while scrubbed_unames.contains(&format!("UsernameScrubbed{}", i)){
i += 1;
}
let scrubbed_name = format!("UsernameScrubbed{}", i);
blocking(context.pool(), move |conn| {
User_::update_username(conn, target.id, scrubbed_name.clone(),
make_apub_endpoint(EndpointType::User, &*scrubbed_name).to_string())
}).await??;
}
// ban the user first, so when we query the db we won't miss anything
let banned_user_id = data.user_id;
let ban_user = move |conn: &'_ _| User_::ban_user(conn, banned_user_id, true);
@ -1835,6 +1852,32 @@ impl Perform for RemoveUserContent {
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetRelatedUsers {
type Response = GetRelatedUsersResponse;
async fn perform(&self, context: &Data<LemmyContext>, _websocket_id: Option<usize>) -> Result<Self::Response, LemmyError> {
let data: &GetRelatedUsers = &self;
// Permissions checks
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
// make sure they're an admin/sitemod
is_admin_or_sitemod(context.pool(), user.id).await?;
let userid = data.user_id;
let userbanid = blocking(context.pool(), move |conn| UserBanId::get_by_user(conn, &userid)).await??;
match userbanid {
Some(ubid) => {
let users = blocking(context.pool(), move |conn| UserBanId::get_users_by_bid(conn, ubid.bid)).await??;
Ok(GetRelatedUsersResponse { users })
},
None => Ok(GetRelatedUsersResponse { users: vec![] })
}
}
}
async fn generate_token(
context: &Data<LemmyContext>,
user_id: i32,

1
server/src/routes/api.rs

@ -197,6 +197,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
// Admin action. I don't like that it's in /user
.route("/ban", web::post().to(route_post::<BanUser>))
.route("/purge", web::post().to(route_post::<RemoveUserContent>))
.route("/relations", web::get().to(route_get::<GetRelatedUsers>))
// Account actions. I don't like that they're in /user maybe /accounts
.route("/login", web::post().to(route_post::<Login>))
.route("/logout", web::post().to(route_post::<Logout>))

Loading…
Cancel
Save